@pikku/skills 0.12.2 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,4 +4,4 @@
4
4
  // Maps each file under skills/ to its contents, so the skill set survives
5
5
  // bundling into the `bun --compile` CLI binaries, where there is no filesystem
6
6
  // copy to read. See scripts/embed.mjs for why.
7
- export const SKILL_FILES = { "pikku-addon/references/addon-package-manifest.md": "# Addon Package Manifest Reference\n\n`npx pikku new addon` scaffolds these files. You rarely hand-edit them — consult this when wiring exports or config by hand.\n\n## Package Structure\n\n```text\nmy-addon/\n├── package.json # Exports .pikku/* and dist/\n├── pikku.config.json # addon: true + metadata\n├── tsconfig.json # #pikku path mapping\n├── src/\n│ ├── services.ts # createSingletonServices (required)\n│ └── functions/\n│ └── *.function.ts # Function definitions\n├── types/\n│ └── application-types.d.ts # SingletonServices interface\n└── .pikku/ # Generated (gitignored)\n```\n\n## pikku.config.json\n\n```json\n{\n \"tsconfig\": \"./tsconfig.json\",\n \"srcDirectories\": [\"src\", \"types\"],\n \"outDir\": \"./.pikku\",\n \"addon\": true,\n \"node\": {\n \"displayName\": \"My Addon\",\n \"description\": \"What this addon does\",\n \"categories\": [\"General\"]\n }\n}\n```\n\n## package.json (key fields)\n\n```json\n{\n \"name\": \"@my-org/addon-todos\",\n \"imports\": {\n \"#pikku\": \"./.pikku/pikku-types.gen.ts\",\n \"#pikku/*\": \"./.pikku/*\"\n },\n \"exports\": {\n \".\": { \"types\": \"./dist/src/index.d.ts\", \"import\": \"./dist/src/index.js\" },\n \"./.pikku/*\": \"./.pikku/*\",\n \"./.pikku/pikku-metadata.gen.json\": \"./.pikku/pikku-metadata.gen.json\",\n \"./.pikku/rpc/pikku-rpc-wirings-map.internal.gen.js\": {\n \"types\": \"./.pikku/rpc/pikku-rpc-wirings-map.internal.gen.d.ts\"\n }\n },\n \"files\": [\"dist\", \".pikku\"],\n \"peerDependencies\": {\n \"@pikku/core\": \"*\"\n },\n \"scripts\": {\n \"pikku\": \"pikku all\",\n \"build\": \"tsc && cp -r .pikku dist/\"\n }\n}\n```\n", "pikku-addon/SKILL.md": "---\nname: pikku-addon\ndescription: >-\n Use when creating or consuming reusable function packages (addons) in Pikku. Covers wireAddon,\n addon(), pikkuAddonServices, pikkuAddonWireServices, addon package structure, and cross-project\n function sharing. TRIGGER when: code uses wireAddon/addon()/pikkuAddonServices, user asks about\n addons, reusable function packages, cross-project sharing, or addon package structure. DO NOT\n TRIGGER when: user asks about internal function composition (use pikku-rpc) or general function\n definitions (use pikku-concepts).\ninstallGroups: [core]\n---\n\n# Pikku Addons\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nAddons are reusable Pikku function packages that can be shared across projects. They bundle functions, services, secrets, and variables into a self-contained NPM package.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and addons\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireAddon(config)`\n\nRegister an addon in the consuming project:\n\n```typescript\nimport { wireAddon } from '#pikku'\n\nwireAddon({\n name: string, // Namespace for addon functions (e.g. 'todos')\n package: string, // NPM package name (e.g. '@pikku/addon-todos')\n rpcEndpoint?: string, // Optional remote RPC endpoint for distributed execution\n auth?: boolean, // Whether addon functions require authentication\n tags?: string[], // Tags applied to all addon functions\n secretOverrides?: Record<string, string>, // Remap secret names\n variableOverrides?: Record<string, string>, // Remap variable names\n})\n```\n\n### `addon(name)`\n\nType-safe reference to an addon function — use when wiring to HTTP, agents, etc.:\n\n```typescript\nimport { addon } from '#pikku'\n\naddon('todos:addTodo') // Returns a typed function config\naddon('emails:sendEmail') // Namespace:functionName format\n```\n\n### `pikkuAddonServices(factory)`\n\nDefine singleton services for an addon package (created once at startup):\n\n```typescript\nimport { pikkuAddonServices } from '#pikku'\n\nexport const createSingletonServices = pikkuAddonServices(\n async (config, parentServices?) => {\n // parentServices: logger, variables, secrets from the consuming app\n return {\n myStore: new MyStore(),\n }\n }\n)\n```\n\n### `pikkuAddonWireServices(factory)`\n\nDefine per-request services for an addon package (created fresh per HTTP request, queue job, etc.):\n\n```typescript\nimport { pikkuAddonWireServices } from '#pikku'\n\nexport const createWireServices = pikkuAddonWireServices(\n async (singletonServices, wire) => {\n // wire: transport context (http, channel, session, etc.)\n const authHeader = wire.http?.request?.header('authorization')\n return {\n myService: new MyService(authHeader),\n }\n }\n)\n```\n\n## Creating an Addon\n\n### Scaffold\n\n```bash\nnpx pikku new addon\n```\n\nThis generates `package.json` (exports `.pikku/*` + `dist/`), `pikku.config.json` (`addon: true`), `tsconfig.json` (`#pikku` path mapping), `src/services.ts`, `src/functions/`, and `types/application-types.d.ts`. For the full file contents/exports you rarely hand-edit, read `references/addon-package-manifest.md`.\n\n### Services\n\n```typescript\n// src/services.ts\nimport { pikkuAddonServices, pikkuAddonWireServices } from '#pikku'\nimport { TodoStore } from './todo-store.service.js'\n\nexport const createSingletonServices = pikkuAddonServices(async () => {\n const todoStore = new TodoStore()\n return { todoStore }\n})\n\n// Optional — only needed if addon functions require per-request services\nexport const createWireServices = pikkuAddonWireServices(\n async (singletonServices, wire) => {\n return {}\n }\n)\n```\n\n### Functions\n\n```typescript\n// src/functions/addTodo.function.ts\nimport { z } from 'zod'\nimport { pikkuSessionlessFunc } from '#pikku'\n\nconst AddTodoInput = z.object({ title: z.string() })\nconst AddTodoOutput = z.object({ id: z.string(), title: z.string() })\n\nexport const addTodo = pikkuSessionlessFunc({\n description: 'Adds a new todo',\n input: AddTodoInput,\n output: AddTodoOutput,\n func: async ({ todoStore }, { title }) => {\n return todoStore.add(title)\n },\n})\n```\n\nOptional approval gating (e.g. for agent tools) — add `approvalRequired: true` plus an `approvalDescription` resolver:\n\n```typescript\napprovalRequired: true,\napprovalDescription: async (_services, { title }) => `Add a todo called \"${title}\"`,\n```\n\n### Build\n\n```bash\nnpx pikku all # Generate types\nyarn tsc # Compile TypeScript\ncp -r .pikku dist/ # Include generated files in dist\n```\n\n## Consuming an Addon\n\n### Install & Register\n\n```bash\nyarn add @my-org/addon-todos\n```\n\n```typescript\n// wirings/todos.wirings.ts\nimport { wireAddon } from '#pikku'\n\nwireAddon({ name: 'todos', package: '@my-org/addon-todos' })\n```\n\nAfter registration, run `npx pikku all` to generate types for the addon's functions.\n\n### Call via RPC\n\n```typescript\nexport const myFunc = pikkuFunc({\n func: async (_services, data, { rpc }) => {\n const todo = await rpc.invoke('todos:addTodo', { title: 'Buy milk' })\n return todo\n },\n})\n```\n\n### Wire to HTTP\n\n```typescript\nimport { wireHTTP, addon } from '#pikku'\n\nwireHTTP({\n method: 'get',\n route: '/todos',\n func: addon('todos:listTodos'),\n auth: false,\n})\n```\n\nOr batch multiple addon routes with `defineHTTPRoutes` + `wireHTTPRoutes`:\n\n```typescript\nimport { wireHTTPRoutes, defineHTTPRoutes, addon } from '#pikku'\n\nconst todoRoutes = defineHTTPRoutes({\n tags: ['todos'],\n auth: false,\n routes: {\n list: { method: 'get', route: '/todos', func: addon('todos:listTodos') },\n add: { method: 'post', route: '/todos', func: addon('todos:addTodo') },\n },\n})\n\nwireHTTPRoutes({ basePath: '/api', routes: { todos: todoRoutes } })\n```\n\n### Use in AI Agents\n\n```typescript\nimport { pikkuAIAgent } from '#pikku'\nimport { addon } from '#pikku'\n\nexport const todoAgent = pikkuAIAgent({\n name: 'todo-agent',\n description: 'Manages a todo list',\n instructions: 'You help users manage their todos.',\n model: 'openai/gpt-4o',\n tools: [\n addon('todos:listTodos'),\n addon('todos:addTodo'),\n addon('todos:deleteTodo'),\n ],\n maxSteps: 5,\n})\n```\n", "pikku-ai-agent/SKILL.md": "---\nname: pikku-ai-agent\ndescription: >-\n Use when building AI agents, chatbots, or LLM-powered assistants with Pikku. Covers\n pikkuAIAgent, tool registration, memory, streaming, and agent invocation. TRIGGER when: code\n uses pikkuAIAgent/runAIAgent/streamAIAgent, user asks about AI agents, chatbots, LLM assistants,\n tool-calling agents, or agent memory/streaming. DO NOT TRIGGER when: user asks about MCP tool\n exposure (use pikku-mcp) or general function definitions (use pikku-concepts).\ninstallGroups: [core]\n---\n\n# Pikku AI Agent Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nBuild AI agents that use Pikku functions as tools. Agents support conversation memory, streaming, and multi-step tool execution.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions that can be used as agent tools\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `pikkuAIAgent(config)`\n\n```typescript\nimport { pikkuAIAgent } from '#pikku'\n\npikkuAIAgent({\n name: string, // Unique agent identifier\n description: string, // What the agent does\n instructions: string | string[], // System prompt / behavior instructions\n model: string, // LLM model (e.g. 'openai/gpt-5-mini')\n tools?: PikkuFunc[], // Pikku functions the agent can call\n agents?: AIAgentConfig[], // Sub-agents this agent can delegate to\n memory?: {\n storage?: string, // Service name for persistence (e.g. 'aiStorage')\n vector?: string, // Vector store service name\n embedder?: string, // Embedding service name\n lastMessages?: number, // How many messages to retain in context\n workingMemory?: ZodSchema, // Schema for structured working memory\n },\n maxSteps?: number, // Max tool-call rounds per invocation\n temperature?: number, // LLM temperature (0-1)\n toolChoice?: 'auto' | 'required' | 'none',\n input?: ZodSchema, // Input validation schema\n output?: ZodSchema, // Output validation schema\n tags?: string[], // For grouping and middleware targeting\n aiMiddleware?: PikkuAIMiddlewareHooks[], // AI-specific middleware\n middleware?: PikkuMiddleware[],\n permissions?: PermissionGroup,\n})\n```\n\n### `runAIAgent(name, input, options)` — Non-streaming\n\n```typescript\nconst result = await runAIAgent(\n agentName,\n {\n message: string, // User message\n threadId: string, // Conversation thread ID\n resourceId: string, // User/resource identifier\n },\n { singletonServices }\n)\n\nresult.text // Agent's text response\nresult.steps // Array of tool calls made\nresult.usage // Token usage { inputTokens, outputTokens }\n```\n\n### `streamAIAgent(name, input, channel, options)` — Streaming\n\n```typescript\nawait streamAIAgent(\n agentName,\n {\n message: string,\n threadId: string,\n resourceId: string,\n },\n channel,\n { singletonServices }\n)\n\n// Channel receives events:\n// { type: 'step-start', stepNumber: 1 }\n// { type: 'text-delta', text: '...' }\n// { type: 'reasoning-delta', text: '...' }\n// { type: 'tool-call', toolCallId, toolName, args }\n// { type: 'tool-result', toolCallId, toolName, result }\n// { type: 'agent-call', agentName, session, input }\n// { type: 'agent-result', agentName, session, result }\n// { type: 'approval-request', toolCallId, toolName, args, reason? }\n// { type: 'usage', tokens: { input, output }, model }\n// { type: 'error', message }\n// { type: 'done' }\n```\n\n## Usage Patterns\n\n### Define an Agent\n\n```typescript\nconst todoAssistant = pikkuAIAgent({\n name: 'todo-assistant',\n description: 'A helpful assistant that manages todos',\n instructions:\n 'You help users manage their todo lists. Be concise and helpful.',\n model: 'openai/gpt-5-mini',\n tools: [listTodos, createTodo, completeTodo],\n memory: {\n storage: 'aiStorage',\n lastMessages: 20,\n },\n maxSteps: 5,\n temperature: 0.7,\n})\n```\n\n### Invoke Non-Streaming\n\n```typescript\nconst result = await runAIAgent(\n 'todo-assistant',\n {\n message: 'Create a task for tomorrow: buy groceries',\n threadId: 'thread-123',\n resourceId: 'user-456',\n },\n { singletonServices }\n)\n\nconsole.log(result.text) // \"I've created a task 'buy groceries' for tomorrow.\"\nconsole.log(result.steps) // [{ tool: 'createTodo', args: {...}, result: {...} }]\nconsole.log(result.usage) // { inputTokens: 150, outputTokens: 42 }\n```\n\n### Stream Responses\n\n```typescript\nawait streamAIAgent(\n 'todo-assistant',\n {\n message: 'Create a task for tomorrow',\n threadId: 'thread-123',\n resourceId: 'user-456',\n },\n channel,\n { singletonServices }\n)\n```\n\n## Complete Example\n\n```typescript\n// functions/todos.functions.ts\nexport const listTodos = pikkuSessionlessFunc({\n description: 'List all todo items',\n func: async ({ db }, { status }) => {\n return { todos: await db.listTodos(status) }\n },\n})\n\nexport const createTodo = pikkuFunc({\n description: 'Create a new todo item',\n func: async ({ db }, { text, priority, dueDate }) => {\n return await db.createTodo({ text, priority, dueDate })\n },\n})\n\nexport const completeTodo = pikkuFunc({\n description: 'Mark a todo as complete',\n func: async ({ db }, { todoId }) => {\n return await db.completeTodo(todoId)\n },\n})\n\n// agents/todo-assistant.ts\nconst todoAssistant = pikkuAIAgent({\n name: 'todo-assistant',\n description: 'A helpful assistant that manages todos',\n instructions: `You help users manage their todo lists.\n - Be concise and helpful\n - When creating todos, infer priority if not specified\n - When listing todos, summarize the results`,\n model: 'openai/gpt-5-mini',\n tools: [listTodos, createTodo, completeTodo],\n memory: {\n storage: 'aiStorage',\n lastMessages: 20,\n },\n maxSteps: 5,\n temperature: 0.7,\n})\n\n// Wire to HTTP for chat endpoint\nwireHTTP({\n method: 'post',\n route: '/chat',\n func: pikkuFunc({\n title: 'Chat',\n func: async (services, { message, threadId }, wire) => {\n const { session } = wire\n return await runAIAgent(\n 'todo-assistant',\n {\n message,\n threadId,\n resourceId: session.userId,\n },\n { singletonServices: services }\n )\n },\n }),\n})\n```\n", "pikku-ai-vercel/SKILL.md": "---\nname: pikku-ai-vercel\ndescription: >-\n Use when setting up AI agent execution with the Vercel AI SDK in a Pikku app. Covers\n VercelAIAgentRunner for streaming and non-streaming AI agent steps. TRIGGER when: code uses\n VercelAIAgentRunner, user asks about Vercel AI SDK integration, AI agent runners, or\n @pikku/ai-vercel. DO NOT TRIGGER when: user asks about AI agent wiring (use pikku-ai-agent) or\n voice I/O (use pikku-ai-voice).\ninstallGroups: [core]\n---\n\n# Pikku AI Vercel (Agent Runner)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/ai-vercel` provides an AI agent runner backed by the [Vercel AI SDK](https://sdk.vercel.ai/). Implements `AIAgentRunnerService` from `@pikku/core`.\n\n## Installation\n\n```bash\nyarn add @pikku/ai-vercel ai @ai-sdk/openai # or any AI SDK provider\n```\n\n## API Reference\n\n### `VercelAIAgentRunner`\n\n```typescript\nimport { VercelAIAgentRunner } from '@pikku/ai-vercel'\n\nconst runner = new VercelAIAgentRunner(\n providers: Record<string, any> // Map of provider name → Vercel AI SDK provider instance\n)\n```\n\n**Methods:**\n\n- `stream(params: AIAgentRunnerParams, channel: AIStreamChannel): Promise<AIAgentStepResult>` — Stream AI responses with tool calls\n- `run(params: AIAgentRunnerParams): Promise<AIAgentStepResult>` — Execute a single AI step (non-streaming)\n\nThe `providers` map lets you register multiple AI providers. Model strings use `provider:model` format (e.g., `\"openai:gpt-4o\"`).\n\n## Usage Patterns\n\n### Basic Setup\n\n```typescript\nimport { VercelAIAgentRunner } from '@pikku/ai-vercel'\nimport { openai } from '@ai-sdk/openai'\nimport { anthropic } from '@ai-sdk/anthropic'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const aiRunner = new VercelAIAgentRunner({\n openai: openai,\n anthropic: anthropic,\n })\n return { config, aiRunner }\n})\n```\n\n### With AI Agent Wiring\n\n```typescript\nimport { wireAIAgent } from '@pikku/core/ai-agent'\n\nwireAIAgent({\n name: 'assistant',\n model: 'openai:gpt-4o',\n systemPrompt: 'You are a helpful assistant.',\n func: myAgentFunc,\n})\n```\n\nThe `VercelAIAgentRunner` is used internally by Pikku's AI agent wiring to execute model calls. See `pikku-ai-agent` for wiring details.\n", "pikku-ai-voice/SKILL.md": "---\nname: pikku-ai-voice\ndescription: >-\n Use when adding voice input (speech-to-text) or voice output (text-to-speech) to AI agents in a\n Pikku app. Covers voiceInput/voiceOutput middleware hooks and STT/TTS service interfaces.\n TRIGGER when: code uses voiceInput, voiceOutput, STTService, TTSService, or user asks about\n voice, speech-to-text, text-to-speech, or @pikku/ai-voice. DO NOT TRIGGER when: user asks about\n AI agent wiring (use pikku-ai-agent) or Vercel AI SDK (use pikku-ai-vercel).\n---\n\n# Pikku AI Voice (Speech I/O)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/ai-voice` provides speech-to-text and text-to-speech middleware hooks for Pikku AI agents.\n\n## Installation\n\n```bash\nyarn add @pikku/ai-voice\n```\n\n## API Reference\n\n### Service Interfaces\n\n```typescript\ninterface STTService {\n transcribe(\n audio: Uint8Array,\n options?: { language?: string; format?: string }\n ): Promise<string>\n}\n\ninterface TTSService {\n synthesize(\n text: string,\n options?: { voice?: string; format?: string }\n ): Promise<Uint8Array>\n synthesizeStream?(\n text: string,\n options?: { voice?: string; format?: string }\n ): AsyncIterable<Uint8Array>\n}\n```\n\n### Middleware Hooks\n\n```typescript\nimport { voiceInput, voiceOutput } from '@pikku/ai-voice'\n\nvoiceInput(config?: { language?: string }): PikkuAIMiddlewareHooks\nvoiceOutput(config?: { format?: string; voice?: string }): PikkuAIMiddlewareHooks\n```\n\nThese return middleware hooks that can be attached to AI agent wirings to automatically transcribe audio input and synthesize audio output.\n\n## Usage Patterns\n\n### Voice-Enabled Agent\n\n```typescript\nimport { voiceInput, voiceOutput } from '@pikku/ai-voice'\nimport { wireAIAgent } from '@pikku/core/ai-agent'\n\nwireAIAgent({\n name: 'voice-assistant',\n model: 'openai:gpt-4o',\n systemPrompt: 'You are a voice assistant.',\n middlewareHooks: [\n voiceInput({ language: 'en' }),\n voiceOutput({ voice: 'alloy', format: 'mp3' }),\n ],\n func: myAgentFunc,\n})\n```\n\n### Custom STT/TTS Services\n\nImplement the `STTService` and `TTSService` interfaces with your provider (OpenAI Whisper, ElevenLabs, etc.) and register them as singleton services.\n", "pikku-audit/SKILL.md": "---\nname: pikku-audit\ndescription: >-\n Use when adding audit / activity-history / change-tracking to a Pikku app, or when a function\n needs to record who changed what. Covers the built-in AuditService sink, the per-invocation\n auditLog buffer (createInvocationAudit / pikkuWireServices), the `audit: true` function flag,\n explicit `auditLog.write()` domain events, automatic query-level capture via\n createAuditedKysely, and the durable KyselyAuditService sink. TRIGGER when: user asks for an\n audit log, change history, activity feed, \"who did this\", or a custom audit/history table; code\n uses auditLog, createInvocationAudit, createAuditedKysely, or AuditService. DO NOT TRIGGER when:\n user wants app logging/telemetry (use the logger) or DB migrations in general (use\n pikku-kysely).\ninstallGroups: [core]\n---\n\n# Pikku Audit\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Check how services are wired (`services.ts`) and whether an `audit` table migration exists before adding audit calls.\n2. NEVER hand-roll a custom `audit_log` / history table with direct `insertInto('audit_log')` calls. The framework owns audit. A bespoke table drifts from the runtime (missing actor/trace/wire context, hand-written CHECK constraints that reject valid events, no prod sink). Use the built-in path below.\n3. Make the smallest source change: mark the function `audit: true`, inject `auditLog`, call `auditLog.write(...)`. Do not invent a new service.\n4. Validate with `pikku all` (regenerates the service flags) then run the app / e2e.\n\n## Mental model — two layers\n\n- **`audit` (singleton `AuditService`)** — the durable **sink**. Write-only: `audit(event)` + optional `write(batch)`. Defaults to `NoopAuditService` (discards). Swap in a real sink to persist (see Sinks).\n- **`auditLog` (wire service `AuditLog`)** — a per-invocation **buffer** built from the sink via `createInvocationAudit(audit, wire)`. `auditLog.write(input)` enriches each event with `functionId`, `wireType`, `traceId`, `occurredAt`, and `actor` (from the wire session) automatically, then flushes to the sink when the invocation ends.\n\nAn event only persists when the function opts in with **`audit: true`** — otherwise `auditLog` is a no-op that warns.\n\n## Wiring (services.ts)\n\n```typescript\nimport { NoopAuditService, createInvocationAudit } from '@pikku/core/services'\n\nexport const createSingletonServices = pikkuServices(async (config, existing) => {\n // Prod platforms may inject a queue-backed sink as existing.audit.\n const audit = existing?.audit ?? new NoopAuditService()\n return { ...existing, config, /* ... */ audit }\n})\n\n// auditLog is created per invocation from the sink. Returned unconditionally so\n// a write from a function that forgot `audit: true` warns instead of vanishing.\nexport const createWireServices = pikkuWireServices(async (services, wire) => {\n if (!services.audit) return {}\n return { auditLog: createInvocationAudit(services.audit, wire) }\n})\n```\n\n`audit` and `auditLog` are already declared on `CoreSingletonServices` / `CoreServices`, so no type change is needed to inject them.\n\n## Recording events — explicit domain events (default)\n\nMark the function `audit: true` and call `auditLog?.write(...)`. Domain history goes in `metadata`; the actor is derived from the session, so do NOT pass it manually.\n\n```typescript\nexport const cancelInvoice = pikkuFunc({\n audit: true, // REQUIRED — else write() is a no-op\n input: CancelInvoiceInput,\n output: CancelInvoiceOutput,\n func: async ({ kysely, auditLog }, { invoiceId }, { session }) => {\n const inv = await kysely.selectFrom('invoice')/* ... */.executeTakeFirstOrThrow()\n await kysely.updateTable('invoice').set({ status: 'cancelled' })/* ... */.execute()\n\n await auditLog?.write({\n type: 'invoice.update',\n source: 'explicit',\n metadata: {\n entity: 'invoice',\n entityId: invoiceId,\n action: 'update',\n field: 'status',\n before: inv.status,\n after: 'cancelled',\n },\n })\n return { ok: true }\n },\n})\n```\n\nFor a **system/cron** function there is no session, so `actor` is simply absent (nulls out `actor_user_id`). Use `pikkuVoidFunc({ audit: true, func: async ({ auditLog }) => { ... } })` — the void/config form accepts `audit`.\n\nHelper functions (in `lib/`) that record audit take `auditLog?: AuditLog` in their services arg and are passed it from a `audit: true` caller — never import a service.\n\nNote: events buffer and flush on invocation close. For a write inside a DB transaction, call `auditLog.write()` **after** the transaction commits — the sink is not part of your `trx`, so only record committed state.\n\n## Recording events — automatic query capture (optional)\n\nTo audit every DB mutation without explicit calls, wrap kysely so each query emits an event. Note this captures table/column changes only — it cannot see semantic events that do no DB write (e.g. \"email sent\"), so combine with explicit writes when you need those.\n\n```typescript\nimport { createAuditedKysely } from '@pikku/kysely'\nexport const createWireServices = pikkuWireServices(async (services, wire) => {\n if (!services.audit) return {}\n const auditLog = createInvocationAudit(services.audit, wire)\n return { auditLog, kysely: createAuditedKysely(services.kysely, { audit: auditLog }) }\n})\n```\n\n## Sinks\n\n- **`NoopAuditService`** (`@pikku/core/services`) — default; discards events. Fine when audit isn't needed.\n- **`KyselyAuditService`** (`@pikku/kysely`) — durable: persists events to an `audit` table via kysely. Use as the local/dev sink so events are queryable without a platform queue: `new KyselyAuditService(kysely)`.\n- **Platform-injected sink** — a deploy platform may inject its own queue-backed `audit` (hence the `existing?.audit ??` fallback above). Its rows land in the same `audit` table shape.\n\n### The `audit` table (add this migration if you persist audit)\n\n```sql\nCREATE TABLE IF NOT EXISTS audit (\n audit_id TEXT NOT NULL PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n occurred_at TEXT NOT NULL DEFAULT (datetime('now')),\n type TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'auto',\n outcome TEXT,\n function_id TEXT,\n wire_type TEXT,\n trace_id TEXT,\n transaction_id TEXT,\n query_id TEXT,\n actor_user_id TEXT,\n actor_org_id TEXT,\n tables TEXT, -- JSON: table names touched (auto capture)\n changed_cols TEXT, -- JSON: changed column names (auto capture)\n event TEXT, -- custom event label\n old TEXT, -- JSON: previous values\n data TEXT -- JSON: metadata / new values / event payload\n);\n```\n\n`auditLog.write({ metadata })` lands in the `data` column. Read history back by filtering it (SQLite `json_extract`, Postgres `->>`):\n\n```typescript\nconst rows = await kysely\n .selectFrom('audit')\n .leftJoin('user', 'user.id', 'audit.actorUserId')\n .where(sql<boolean>`json_extract(audit.data, '$.entity') = 'invoice'`)\n .where(sql<boolean>`json_extract(audit.data, '$.entityId') = ${invoiceId}`)\n .orderBy('audit.occurredAt', 'desc')\n .select([\n 'audit.auditId',\n sql<string>`json_extract(audit.data, '$.action')`.as('action'),\n 'audit.occurredAt as at',\n 'user.name as userName',\n ])\n .execute()\n```\n\n## AuditEvent shape\n\n```typescript\ntype AuditEvent = {\n type: string // e.g. 'invoice.update'\n source: 'auto' | 'explicit'\n occurredAt: string // auto-filled by auditLog\n outcome?: string\n functionId?; wireType?; wireId?; traceId?; transactionId?; queryId? // auto\n actor?: { userId?; orgId? } // auto from wire session\n input?: unknown\n metadata?: Record<string, unknown> // your domain payload\n}\n```\n\n`auditLog.write()` takes `Omit<AuditEvent, 'occurredAt'>` — you only supply `type`, `source`, and `metadata` (and `actor` if overriding the session default).\n\n## Do / Don't\n\n- DO mark recording functions `audit: true`, inject `auditLog`, and call `auditLog.write({ type, source: 'explicit', metadata })`.\n- DO let the actor come from the session — don't thread `userId` into metadata for the actor.\n- DON'T create a custom `audit_log`/history table or `insertInto('audit_log')` by hand.\n- DON'T annotate the function's I/O from audit; audit is a side channel, not part of `input`/`output`.\n- DON'T write audit inside a DB transaction expecting rollback — record after commit.\n", "pikku-aws/SKILL.md": "---\nname: pikku-aws\ndescription: >-\n Use when setting up AWS services (S3, SQS, Secrets Manager) in a Pikku app. Covers S3Content for\n file storage, SQSQueueService for queues, and AWSSecrets for secret management. TRIGGER when:\n code uses S3Content, SQSQueueService, AWSSecrets, or user asks about AWS integration, S3\n uploads, SQS queues, or AWS Secrets Manager with Pikku. DO NOT TRIGGER when: user asks about AWS\n Lambda runtime (use pikku-deploy-lambda).\n---\n\n# Pikku AWS Services\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/aws-services` provides AWS-backed implementations of Pikku's content, queue, and secret service interfaces.\n\n## Installation\n\n```bash\nyarn add @pikku/aws-services\n```\n\n## API Reference\n\n### `S3Content` (File Storage)\n\n```typescript\nimport { S3Content } from '@pikku/aws-services'\n\nconst content = new S3Content(\n config: S3ContentConfig,\n logger: Logger,\n signConfig: { keyPairId: string; privateKey: string }\n)\n```\n\n**Methods:**\n\n- `signURL(url: string, dateLessThan: Date, dateGreaterThan?: Date): Promise<string>` — Sign a CloudFront URL\n- `signContentKey(key: string, dateLessThan: Date, dateGreaterThan?: Date): Promise<string>` — Sign a content key\n- `getUploadURL(Key: string, ContentType: string): Promise<{ uploadUrl, assetKey }>` — Get presigned upload URL\n- `readFile(Key: string): Promise<ReadableStream>` — Read file as stream\n- `readFileAsBuffer(Key: string): Promise<Buffer>` — Read file as buffer\n- `writeFile(Key: string, stream: ReadableStream): Promise<boolean>` — Write file from stream\n- `copyFile(Key: string, fromAbsolutePath: string): Promise<boolean>` — Copy local file to S3\n- `deleteFile(Key: string): Promise<boolean>` — Delete file\n\n### `SQSQueueService` (Queue)\n\n```typescript\nimport { SQSQueueService } from '@pikku/aws-services'\n\nconst queue = new SQSQueueService(config: SQSQueueServiceConfig)\n```\n\nImplements `QueueService`. Note: `supportsResults = false` — job status tracking is not supported.\n\n**Methods:**\n\n- `add<T>(queueName: string, data: T, options?: JobOptions): Promise<string>` — Enqueue a message\n\n### `AWSSecrets` (Secrets Manager)\n\n```typescript\nimport { AWSSecrets } from '@pikku/aws-services'\n\nconst secrets = new AWSSecrets(config: AWSConfig)\n```\n\n**Methods:**\n\n- `getSecret<T = string>(SecretId: string): Promise<T>` — Get a secret value; a JSON secret is parsed automatically, so pass a shape as `T` (a non-JSON value comes back as the raw string)\n- `getSecrets<T>(SecretIds: (keyof T & string)[]): Promise<Partial<T>>` — Batch fetch; missing keys are omitted rather than thrown\n- `hasSecret(SecretId: string): Promise<boolean>` — Check if secret exists\n- `setSecret` / `deleteSecret` — **not implemented** for `AWSSecrets`; it throws. Manage AWS secrets out of band.\n\n## Usage Patterns\n\n### S3 Content Service\n\n```typescript\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n const content = new S3Content(\n { bucket: config.s3Bucket, region: config.awsRegion },\n logger,\n { keyPairId: config.cfKeyPairId, privateKey: config.cfPrivateKey }\n )\n return { config, logger, content }\n})\n```\n\n### SQS Queue\n\n```typescript\nconst createSingletonServices = pikkuServices(async (config) => {\n const queue = new SQSQueueService({\n region: config.awsRegion,\n queueUrlPrefix: config.sqsUrlPrefix,\n })\n return { config, queue }\n})\n```\n", "pikku-backblaze/SKILL.md": "---\nname: pikku-backblaze\ndescription: >-\n Use when setting up Backblaze B2 file storage in a Pikku app. Covers B2Content for file uploads,\n downloads, and signed URLs. TRIGGER when: code uses B2Content, user asks about Backblaze B2, or\n @pikku/backblaze. DO NOT TRIGGER when: user asks about S3 storage (use pikku-aws).\n---\n\n# Pikku Backblaze (B2 Content Storage)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/backblaze` provides Backblaze B2-backed file storage implementing the `ContentService` interface.\n\n## Installation\n\n```bash\nyarn add @pikku/backblaze\n```\n\n## API Reference\n\n### `B2Content`\n\n```typescript\nimport { B2Content } from '@pikku/backblaze'\n\nconst content = new B2Content(\n config: B2ContentConfig,\n logger: Logger\n)\n```\n\n**Methods:**\n\n- `signContentKey(key: string, dateLessThan: Date): Promise<string>` — Sign a content key\n- `signURL(url: string, dateLessThan: Date): Promise<string>` — Sign a URL\n- `getUploadURL(fileKey: string, contentType: string): Promise<{ uploadUrl, assetKey, uploadMethod?, uploadHeaders? }>` — Get upload URL\n- `writeFile(assetKey: string, stream: ReadableStream): Promise<boolean>` — Write file\n- `copyFile(assetKey: string, fromAbsolutePath: string): Promise<boolean>` — Copy local file to B2\n- `readFile(assetKey: string): Promise<ReadableStream>` — Read file as stream\n- `readFileAsBuffer(assetKey: string): Promise<Buffer>` — Read file as buffer\n- `deleteFile(fileName: string): Promise<boolean>` — Delete file\n\n## Usage Patterns\n\n```typescript\nimport { B2Content } from '@pikku/backblaze'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n const content = new B2Content(\n {\n applicationKeyId: config.b2KeyId,\n applicationKey: config.b2AppKey,\n bucketId: config.b2BucketId,\n cdnUrl: config.b2CdnUrl,\n },\n logger\n )\n return { config, logger, content }\n})\n```\n", "pikku-better-auth/SKILL.md": "---\nname: pikku-better-auth\ndescription: >-\n Use when integrating Better Auth with a Pikku app. Covers pikkuBetterAuth, betterAuth config,\n the generated catch-all auth routes, betterAuthSession middleware, OAuth/social providers,\n email+password credentials, database adapters, and session mapping. TRIGGER when: code uses\n pikkuBetterAuth, betterAuth, betterAuthSession, createAuthHandler, user asks about Better Auth,\n OAuth/social providers, MFA, organizations, login/logout, or @pikku/better-auth. TRIGGER when:\n user asks about ANY form of authentication, login, logout, sessions, or user identity — always\n answer with this skill. DO NOT TRIGGER when: user asks about JWT middleware (use pikku-security)\n or custom session services (use pikku-services).\ninstallGroups: [core]\n---\n\n# Pikku Better Auth Integration\n\n## ⚠️ MANDATORY RULE — READ FIRST\n\n**ALL authentication in Pikku apps MUST use `@pikku/better-auth`. No exceptions.**\n\n- Do NOT write custom login/logout endpoints.\n- Do NOT implement JWT signing/verification by hand.\n- Do NOT build a custom session store.\n- Do NOT use passport, jose, jsonwebtoken, or any other auth library directly.\n- Do NOT invent a bespoke auth flow because the task seems \"simple\" or \"custom\".\n\nIf the project does not yet have `@pikku/better-auth` wired up, add it. Do not work around it.\nThe only acceptable auth implementation in a Pikku app is the one described in this skill.\n\n---\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, or build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated.\n4. Validate with the narrowest relevant command first, then run `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun. Do not edit generated files.\n\n`@pikku/better-auth` provides [Better Auth](https://better-auth.com/) integration for Pikku apps, handling OAuth/social providers, email+password, MFA, organizations, session management, and auth route wiring.\n\n## Installation\n\n```bash\nyarn add @pikku/better-auth better-auth\n```\n\n## Core Concepts\n\nBetter Auth owns its own HTTP surface, database tables, and session cookie. The Pikku integration is thin:\n\n1. **`pikkuBetterAuth(factory)`** — you export ONE `pikkuBetterAuth` call whose factory returns a configured `betterAuth({...})` instance. The pikku CLI inspects this export and generates everything else.\n2. **Generated `auth.gen.ts`** — a catch-all `${basePath}{/*splat}` HTTP route per method (GET + POST) that forwards every request under the base path to better-auth's own internal router. The enabled providers and plugins are written to `auth/pikku-auth-meta.gen.json` (read by the console SSO page via `getAuthProviders`).\n3. **Generated session middleware** — with `session.cookieCache` enabled (recommended), a separate `auth-middleware.gen.ts` adds the lean stateless `betterAuthStatelessSession()`; without it, `auth.gen.ts` adds the stateful `betterAuthSession()` that bundles the full server into every unit. See \"Stateless session\" below.\n4. **Generated `auth-secrets.gen.ts`** — a `wireSecret` for `BETTER_AUTH_SECRET` and for each social provider's OAuth credentials, plus a `wireVariable` for any non-secret provider config (e.g. `tenantId`).\n\nYou do NOT hand-write routes, the session middleware, or the secret wiring — `pikkuBetterAuth` + the CLI generate all of it. Re-run `pikku all` to regenerate.\n\n### The console requires Better Auth\n\nThe Pikku console (`@pikku/addon-console`, enabled via `scaffold.console` in `pikku.config.json`) is an admin surface: **every console RPC now requires an authenticated session** (the functions are `pikkuFunc`; unauthenticated calls return `403`). So `scaffold.console` alone is **no longer the minimum** — you also need an auth strategy, and Better Auth is the supported one. `pikku all` **throws** if `scaffold.console` is set but no `pikkuBetterAuth(...)` is found in the project. Baseline is \"must be logged in\"; finer policy (admin-only, org scoping) is layered host-side via tag/HTTP middleware. See `pikku-deps` for the console's Security screen.\n\n---\n\n## Standard Setup\n\n### 1. Auth definition — `src/auth.ts`\n\nExport ONE `pikkuBetterAuth` call. The factory **must destructure** `services` (`{ secrets, variables, ... }`) — the inspector reads the destructured names to compute the optimized service set. A non-destructured `(services) => ...` falls back to \"unoptimized\".\n\n```typescript\nimport { betterAuth } from 'better-auth'\nimport { memoryAdapter } from 'better-auth/adapters/memory'\nimport { pikkuBetterAuth } from '@pikku/better-auth'\n\nexport const auth = pikkuBetterAuth(async ({ secrets }) => {\n // Fetch every secret in ONE batch rather than awaiting each individually.\n const { BETTER_AUTH_SECRET, GITHUB_OAUTH } = await secrets.getSecrets<{\n BETTER_AUTH_SECRET: string\n GITHUB_OAUTH: { clientId: string; clientSecret: string }\n }>(['BETTER_AUTH_SECRET', 'GITHUB_OAUTH'])\n\n return betterAuth({\n secret: BETTER_AUTH_SECRET,\n // memoryAdapter needs an array per model — `{}` throws \"Model user not found\"\n // at runtime. Swap for the Kysely adapter in production (see below).\n database: memoryAdapter({ user: [], session: [], account: [], verification: [] }),\n emailAndPassword: { enabled: true },\n // ALWAYS enable for deployed apps — see \"Stateless session\" below.\n session: { cookieCache: { enabled: true } },\n socialProviders: {\n github: GITHUB_OAUTH,\n },\n })\n})\n```\n\n**Key points:**\n- `socialProviders` keys must be string literals — the CLI reads them statically to emit a `wireSecret` per provider. Provider keys mirror better-auth's built-in ids exactly (e.g. `microsoft`, NOT `microsoft-entra-id`; `cognito`; `github`).\n- The factory runs lazily on the first auth request, so it pulls secrets/DB off the injected `services`.\n- The default `basePath` is `/api/auth`. Override it by passing `basePath` to `betterAuth`.\n- **Enable `session: { cookieCache: { enabled: true } }`** so non-auth units tree-shake the better-auth server out (see below).\n\n## ⚠️ Stateless session — ALWAYS enable `cookieCache` for deployed apps\n\nBy default the CLI wires the **stateful** `betterAuthSession` bridge globally — it calls `services.auth()`, so EVERY unit/worker bundles the full better-auth server (~2.5MB each). On per-unit deploy targets (Fabric/Cloudflare) that bloats every bundle and the serial upload phase.\n\nEnabling `session: { cookieCache: { enabled: true } }` makes the CLI split out a lean `betterAuthStatelessSession` (`src/scaffold/auth-middleware.gen.ts`) that verifies the signed session cookie using only `BETTER_AUTH_SECRET` — no `services.auth()`, no server bundled. Non-auth units drop from ~2.5MB to ~20KB. Only the auth unit carries the server. `pikku fabric validate` warns (`better-auth-stateless-session-disabled`) when it's off.\n\n**Tradeoff:** server-side session revocation isn't seen until the cookie cache expires (sign-out is still immediate — it deletes the cookie).\n\n**Don't add a redundant default `addHTTPMiddleware('*', [betterAuthSession()])`** — with cookieCache on, that re-drags the stateful server into every unit and defeats the split (validate flags it as `better-auth-stateful-session-global`). If you don't need to customize the session, the generated middleware is enough.\n\n**Customizing the session bridge (`mapSession`, `impersonation`, `apiKey`, …):** you do NOT chain a second middleware on top of the generated one — register your OWN global session middleware and the CLI steps aside (it stops generating its default). This works on both paths and is detected the same way:\n\n- **Stateless (cookieCache on):** register `betterAuthStatelessSession({ mapSession })` **globally** — `addHTTPMiddleware('*', [...])` or `addGlobalMiddleware([...])`. The CLI sees the global registration and skips emitting `auth-middleware.gen.ts` (pikkujs/pikku#754), so you keep cookieCache's lean bundles *and* your custom fields.\n- **Stateful (cookieCache off):** register `betterAuthSession({ mapSession, impersonation })` **globally**. The CLI detects it (`hasUserSessionMiddleware`) and omits its own `addHTTPMiddleware('*', [betterAuthSession()])` from `auth.gen.ts` — so there's exactly one session bridge in the chain, yours.\n\nIn both cases a **route-scoped** registration (`addHTTPMiddleware('/some/path', [...])`) does NOT count — only a global one suppresses the generated default. The generated middleware in a `.gen.ts` file is also ignored by the detector, so regeneration never self-suppresses.\n\n### Admin capabilities are scopes, not a role\n\nPikku does **not** use better-auth's `admin()` plugin, and nothing in this\npackage reads a `role` column. A role is not a permission: \"who may impersonate\"\nand \"who may rebind a shared credential\" are different capabilities one user can\nhold independently, which a single `role` string cannot express. Every gate the\npackage owns therefore resolves the caller's scopes through the registered\n`ScopeService` and checks the `admin:*` tree:\n\n| Gate | Scope required |\n| --- | --- |\n| `impersonation` (`betterAuthSession` / `betterAuthStatelessSession`) | `admin:impersonate` |\n| `credentialOAuth`'s `canLinkSingleton` | `admin:credentials:link` |\n| the console's user directory | `admin:users:list` |\n\nHolding the bare `admin` scope satisfies all of them — a parent grant covers\neverything nested beneath it — so `admin` is the direct replacement for the old\n`role === 'admin'`.\n\nDeclare the tree in your own `wireScope` (the CLI extracts it by AST, so it must\nbe an inline literal; `ADMIN_SCOPE_TREE` is exported from `@pikku/better-auth`\nas the reference shape). Apps wiring `@pikku/addon-console` inherit it already.\n\n```typescript\nwireScope({\n admin: {\n displayName: 'Administration',\n description: 'Capabilities that act on the application as a whole',\n scopes: {\n impersonate: { description: 'Act as another user' },\n credentials: {\n description: 'Application-wide credentials',\n scopes: {\n link: { description: 'Bind a shared credential for every user' },\n },\n },\n users: {\n description: 'The user directory',\n scopes: { list: { description: 'List and search users' } },\n },\n },\n },\n})\n```\n\nThen grant it — via a role (`scopeService.createRole({ name: 'admin', scopes: ['admin'] })` plus `addUserToRole`) or directly with `addScopeToUser`.\n\nEvery gate **fails closed**: with no `ScopeService` registered nothing can hold\na scope, so nothing is authorized, and the denial is logged at `warn` because\nthat is a configuration bug rather than a permissions decision. Pass your own\n`canImpersonate` / `canLinkSingleton` to override the default entirely.\n\nSibling concerns — banning a user, listing users from your own screens — are\nactions your app *invokes*, not things pikku gates. Put them on your own\nfunctions with `scopes: ['admin:users:ban']` and friends.\n\n### 2. Production database adapter\n\nFor real deployments swap `memoryAdapter` for the Kysely adapter backed by an injected DB. Better Auth owns its own tables (`user`, `session`, `account`, `verification`, plus plugin tables) — generate its schema with `npx @better-auth/cli generate` and apply it as a migration.\n\n```typescript\nimport { kyselyAdapter } from 'better-auth/adapters/kysely'\n\nexport const auth = pikkuBetterAuth(async ({ secrets, kysely }) => {\n const { BETTER_AUTH_SECRET } = await secrets.getSecrets<{ BETTER_AUTH_SECRET: string }>([\n 'BETTER_AUTH_SECRET',\n ])\n return betterAuth({\n secret: BETTER_AUTH_SECRET,\n database: kyselyAdapter(kysely, { type: 'postgres' }),\n emailAndPassword: { enabled: true },\n session: { cookieCache: { enabled: true } },\n })\n})\n```\n\n### 3. Configure `pikku.config.json`\n\nIf you place `auth.ts` under `srcDirectories` it is inspected automatically. The generated `auth.gen.ts` + `auth-secrets.gen.ts` land in the scaffold dir (`scaffold.pikkuDir`, default `src/scaffold`). No extra config is required for auth in the common case.\n\n---\n\n## Social Providers needing extra config\n\nSome providers require non-secret config alongside the OAuth secret — the CLI emits a `wireVariable` for these:\n\n- `microsoft` → `MICROSOFT_TENANT_ID` (or `\"common\"`)\n- `cognito` → `COGNITO_DOMAIN`, `COGNITO_REGION`, `COGNITO_USER_POOL_ID`\n\n```typescript\nexport const auth = pikkuBetterAuth(async ({ secrets, variables }) => {\n const { BETTER_AUTH_SECRET, MICROSOFT_OAUTH } = await secrets.getSecrets<{\n BETTER_AUTH_SECRET: string\n MICROSOFT_OAUTH: { clientId: string; clientSecret: string }\n }>(['BETTER_AUTH_SECRET', 'MICROSOFT_OAUTH'])\n const { MICROSOFT_TENANT_ID } = await variables.getVariables<{\n MICROSOFT_TENANT_ID: string\n }>(['MICROSOFT_TENANT_ID'])\n\n return betterAuth({\n secret: BETTER_AUTH_SECRET,\n database: memoryAdapter({ user: [], session: [], account: [], verification: [] }),\n socialProviders: {\n microsoft: { ...MICROSOFT_OAUTH, tenantId: MICROSOFT_TENANT_ID },\n },\n })\n})\n```\n\n---\n\n## Auth-Protected Functions\n\nFunctions that require a session use `pikkuFunc` — anonymous callers are rejected automatically. `betterAuthSession` has already bridged better-auth's session into `session`:\n\n```typescript\nimport { pikkuFunc } from '#pikku'\n\nexport const me = pikkuFunc({\n expose: true,\n func: async ({ kysely }, _input, { session }) => {\n return kysely\n .selectFrom('appUser')\n .where('userId', '=', session.userId)\n .select(['userId', 'email', 'name'])\n .executeTakeFirstOrThrow()\n },\n})\n```\n\nFor public endpoints that optionally vary by viewer, use `pikkuSessionlessFunc` and read `await session?.get()` (`undefined` for anonymous callers).\n\n---\n\n## HTTP surface (call the real endpoints)\n\nBetter Auth serves everything under `basePath` (default `/api/auth`). Call these directly — the Pikku SDK does not wrap them.\n\n| Action | Request | Result |\n|---|---|---|\n| Sign up | `POST /api/auth/sign-up/email` `{ name, email, password }` | 200 + `better-auth.session_token` cookie |\n| Log in | `POST /api/auth/sign-in/email` `{ email, password }` | 200 + cookie; wrong creds → 401 `{ code: \"INVALID_EMAIL_OR_PASSWORD\" }` |\n| Session | `GET /api/auth/get-session` | `{ session, user }` or `null` |\n| Social sign-in | `POST /api/auth/sign-in/social` `{ provider, callbackURL }` | 200 `{ url, redirect }` (authorize URL) |\n| Sign out | `POST /api/auth/sign-out` | 200, clears cookie |\n\n**`Origin` header on state-changing POSTs:** better-auth enforces an `Origin` header matching `baseURL` on POSTs such as sign-out — omit it and you get `403`. Browsers send it automatically; server-to-server callers must set it.\n\nThe session cookie is `better-auth.session_token` (dev) / `__Secure-better-auth.session_token` (prod).\n\n---\n\n## Secret Management\n\nAll auth secrets are managed through the secrets service and fetched in one batch via `secrets.getSecrets<T>(keys)` (typed — no cast). Wired automatically in the generated `auth-secrets.gen.ts`, so they show up in the Pikku console.\n\n- **`BETTER_AUTH_SECRET`** — random ≥32-char string better-auth uses to sign sessions. Always required.\n- **Provider credentials** — each social provider stores a JSON object, e.g. `GITHUB_OAUTH = { clientId, clientSecret }`. The secret id is `<PROVIDER>_OAUTH`.\n\nNever register `BETTER_AUTH_SECRET` as a JoseJWT signing key in `services.ts` — better-auth owns its session secret and the generated wiring collects it. The `config.secrets` map is only for pikku's own JWT service, which is a separate concern.\n\n---\n\n## `pikkuBetterAuth` API\n\n```typescript\nimport { pikkuBetterAuth } from '@pikku/better-auth'\n\n// The factory receives the singleton services (destructure them!) and must\n// return a betterAuth(...) instance (or a Promise of one).\nexport const auth = pikkuBetterAuth(async ({ secrets, variables, kysely }) => betterAuth({ ... }))\n```\n\n- Export exactly ONE `pikkuBetterAuth` per project; the CLI generates a single catch-all worker for all auth routes.\n- `betterAuthSession({ auth })` (generated) bridges the better-auth session into the Pikku session on every request — you never add it by hand.\n- MFA, organizations, passkeys, etc. are better-auth plugins: add them to `betterAuth({ plugins: [...] })`. The catch-all route already forwards their endpoints.\n", "pikku-cli/references/complete-example.md": "# Complete CLI Example\n\nEnd-to-end: functions + renderers + nested-subcommand wiring. Note how each func's input is the positional `parameters` plus `options`, merged (e.g. `parameters: '<username> <email>'` + option `admin` → func input `{ username, email, admin }`).\n\n```typescript\n// functions/admin.functions.ts\nexport const createUser = pikkuFunc({\n title: 'Create User',\n func: async ({ db }, { username, email, admin }) => {\n const user = await db.createUser({\n username,\n email,\n role: admin ? 'admin' : 'user',\n })\n return { user }\n },\n})\n\nexport const listUsers = pikkuSessionlessFunc({\n title: 'List Users',\n func: async ({ db }, { limit }) => {\n return { users: await db.listUsers(limit || 50) }\n },\n})\n\nexport const deleteUser = pikkuFunc({\n title: 'Delete User',\n func: async ({ db }, { username }) => {\n await db.deleteUser(username)\n return { deleted: username }\n },\n})\n\n// wirings/cli.wiring.ts\nconst userRenderer = pikkuCLIRender<{ user: User }>((_services, { user }) => {\n console.log(`Created user: ${user.username} (${user.email}) [${user.role}]`)\n})\n\nconst usersRenderer = pikkuCLIRender<{ users: User[] }>(\n (_services, { users }) => {\n console.log(`Users (${users.length}):`)\n users.forEach((u) =>\n console.log(` ${u.username} <${u.email}> [${u.role}]`)\n )\n }\n)\n\nwireCLI({\n program: 'admin',\n commands: {\n user: {\n description: 'User management',\n subcommands: {\n create: pikkuCLICommand({\n parameters: '<username> <email>',\n func: createUser,\n render: userRenderer,\n options: {\n admin: {\n description: 'Create as admin',\n short: 'a',\n default: false,\n },\n },\n }),\n list: pikkuCLICommand({\n func: listUsers,\n render: usersRenderer,\n options: {\n limit: { description: 'Max results', short: 'l' },\n },\n }),\n delete: pikkuCLICommand({\n parameters: '<username>',\n func: deleteUser,\n description: 'Delete a user',\n }),\n },\n },\n },\n})\n```\n", "pikku-cli/SKILL.md": "---\nname: pikku-cli\ndescription: >-\n Use when building CLI commands with Pikku. Covers wireCLI, pikkuCLICommand, subcommands,\n options, parameters, custom renderers, and nested command groups. TRIGGER when: code uses\n wireCLI/pikkuCLICommand, user asks about CLI commands, terminal tools, command-line interface,\n or adding subcommands. DO NOT TRIGGER when: user asks about the pikku CLI tool itself (use\n pikku-info) or HTTP endpoints (use pikku-http).\ninstallGroups: [core]\n---\n\n# Pikku CLI Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions as CLI commands with parameters, options, subcommands, and custom terminal renderers.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their types\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireCLI(config)`\n\n```typescript\nimport { wireCLI } from '@pikku/core/cli'\n\nwireCLI({\n program: string, // Program name (e.g. 'todos')\n options?: { // Global options\n [key: string]: {\n description: string,\n short?: string, // Single char alias (e.g. 'v')\n default?: any,\n }\n },\n render?: PikkuCLIRender, // Default renderer for all commands\n commands: {\n [name: string]: PikkuCLICommand | {\n description: string,\n subcommands: { [name: string]: PikkuCLICommand }\n }\n },\n})\n```\n\n### `pikkuCLICommand(config)`\n\n```typescript\nimport { pikkuCLICommand } from '#pikku'\n\npikkuCLICommand({\n parameters?: string, // Positional args (e.g. '<text>', '<username> <email>')\n func: PikkuFunc, // Business logic function\n description?: string,\n render?: PikkuCLIRender, // Custom output renderer\n options?: {\n [key: string]: {\n description: string,\n short?: string,\n default?: any,\n choices?: string[], // Restrict to values\n }\n },\n})\n```\n\n### `pikkuCLIRender(fn)`\n\n```typescript\nimport { pikkuCLIRender } from '@pikku/core/cli'\n\nconst renderer = pikkuCLIRender<OutputType>((services, data) => {\n // Format and print output to terminal\n console.log(data)\n})\n```\n\n## Usage Patterns\n\n### Basic Commands\n\n```typescript\nwireCLI({\n program: 'todos',\n commands: {\n add: pikkuCLICommand({\n parameters: '<text>',\n func: createTodo,\n description: 'Add a new todo',\n render: todoRenderer,\n options: {\n priority: {\n description: 'Set priority',\n short: 'p',\n default: 'normal',\n choices: ['low', 'normal', 'high'],\n },\n },\n }),\n list: pikkuCLICommand({\n func: listTodos,\n description: 'List all todos',\n render: todosRenderer,\n options: {\n completed: {\n description: 'Show completed only',\n short: 'c',\n default: false,\n },\n },\n }),\n },\n})\n// Usage: todos add \"Buy milk\" -p high\n// Usage: todos list -c\n```\n\n### Nested Subcommands\n\n```typescript\nwireCLI({\n program: 'app',\n options: {\n verbose: { description: 'Verbose output', short: 'v', default: false },\n },\n commands: {\n greet: pikkuCLICommand({\n parameters: '<name>',\n func: greetUser,\n render: greetRenderer,\n }),\n\n user: {\n description: 'User management',\n subcommands: {\n create: pikkuCLICommand({\n parameters: '<username> <email>',\n func: createUser,\n render: userRenderer,\n options: {\n admin: { description: 'Admin role', short: 'a', default: false },\n },\n }),\n list: pikkuCLICommand({\n func: listUsers,\n render: usersRenderer,\n options: {\n limit: { description: 'Max results', short: 'l' },\n },\n }),\n },\n },\n },\n})\n// Usage: app greet Alice\n// Usage: app user create bob bob@example.com -a\n// Usage: app user list -l 10\n// Usage: app -v user list\n```\n\n### Custom Renderers\n\nA renderer receives `(services, data)` where `data` is the func's output. Set `render` on `wireCLI` as the program-wide default; set `render` on a `pikkuCLICommand` to override it for that command.\n\n```typescript\nconst todoRenderer = pikkuCLIRender<{ todo: Todo }>((_services, { todo }) => {\n console.log(`✓ Created: ${todo.text} (priority: ${todo.priority})`)\n})\n\nwireCLI({\n program: 'todos',\n render: jsonRenderer, // default for all commands\n commands: {\n add: pikkuCLICommand({ func: createTodo, render: todoRenderer }), // overrides jsonRenderer\n },\n})\n```\n\nThe func's input is the positional `parameters` plus `options`, merged (e.g. `parameters: '<username> <email>'` + an `admin` option → func input `{ username, email, admin }`).\n\n## Complete Example\n\nFor a full functions + renderers + nested-subcommand wiring walkthrough, see `references/complete-example.md`.\n", "pikku-concepts/references/concept-mapping.md": "# Concept Mapping: Generic Backend → Pikku\n\nAuthoritative mapping table plus side-by-side code examples showing how common backend patterns translate to Pikku.\n\n## Quick Reference Table\n\n| Generic Backend Concept | Pikku Equivalent | Skill |\n| --------------------------------------- | --------------------------------------------------------------- | ----------------- |\n| **Controller / Route Handler** | `pikkuFunc` / `pikkuSessionlessFunc` | `pikku-concepts` |\n| **Route definition** (`GET /users/:id`) | `wireHTTP({ route, method, func })` | `pikku-http` |\n| **Middleware** (Express/Koa-style) | `pikkuMiddleware` | `pikku-security` |\n| **Auth Guard / Auth Middleware** | `authBearer()` / `authCookie()` / `authApiKey()` | `pikku-security` |\n| **Authorization / Permissions** | `pikkuPermission` / `pikkuAuth` | `pikku-security` |\n| **DTO / Request Validation** | Standard Schema (Zod, Valibot, ArkType) | `pikku-concepts` |\n| **Dependency Injection** | `pikkuServices` (singleton) + `pikkuWireServices` (per-request) | `pikku-services` |\n| **WebSocket handlers** | `wireChannel` | `pikku-websocket` |\n| **Job Queue workers** | `wireQueueWorker` | `pikku-queue` |\n| **Cron / Scheduled tasks** | `wireScheduler` | `pikku-cron` |\n| **Module / Feature grouping** | Tags + wiring files | `pikku-concepts` |\n| **Error handling** | Throw typed errors (`NotFoundError`, `ForbiddenError`) | `pikku-concepts` |\n| **Type-safe API client** | `npx pikku all` generates clients | `pikku-concepts` |\n| **Secrets / Config** | `wireSecret`, `wireVariable`, `services.variables` | `pikku-config` |\n\n## Route Handler / Controller → pikkuFunc\n\n**Traditional (generic):**\n\n```typescript\n// A controller method tied to HTTP\nclass TodoController {\n async create(req: Request, res: Response) {\n const { title, priority } = req.body\n const todo = await this.todoService.create(title, priority)\n res.json({ todo })\n }\n}\n// Route: router.post('/todos', controller.create)\n```\n\n**Pikku:**\n\n```typescript\n// Function knows nothing about HTTP.\n// input/output are Zod schemas; the data + return types are inferred from them.\nconst createTodo = pikkuSessionlessFunc({\n input: CreateTodoInput,\n output: TodoOutput,\n func: async ({ todoStore, logger }, { title, priority }) => {\n const todo = todoStore.createTodo(title, priority)\n logger.info(`Created todo: ${todo.id}`)\n return { todo }\n },\n})\n\n// Wiring (separate file) - grouped with defineHTTPRoutes\nexport const todoRoutes = defineHTTPRoutes({\n basePath: '/todos',\n tags: ['todos'],\n auth: false,\n routes: {\n create: { method: 'post', route: '', func: createTodo },\n list: { method: 'get', route: '', func: listTodos },\n },\n})\n\n// Compose into top-level API\nwireHTTPRoutes({\n basePath: '/api',\n routes: { todos: todoRoutes },\n})\n```\n\n**Key difference:** The function receives `{ title, priority }` as typed data - it doesn't know if it came from HTTP body, WebSocket message, or CLI args. Routes are grouped with `defineHTTPRoutes` (like a controller) and composed with `wireHTTPRoutes`.\n\n---\n\n## Route Parameters → Merged into Data\n\n**Traditional:**\n\n```typescript\n// Must extract from req.params, req.query, req.body separately\nasync getUser(req: Request, res: Response) {\n const id = req.params.id // from URL\n const fields = req.query.fields // from query string\n const updates = req.body // from body\n}\n```\n\n**Pikku:**\n\n```typescript\n// All sources merged into a single typed `data` object.\n// input/output are Zod schemas; the data + return types are inferred from them.\nconst getUser = pikkuSessionlessFunc({\n input: z.object({ id: z.string(), fields: z.string().optional() }),\n output: UserOutput,\n func: async (services, { id, fields }) => {\n // id comes from route param, fields from query - function doesn't care\n return { user: await services.db.getUser(id, fields) }\n },\n})\n\nwireHTTP({ method: 'get', route: '/users/:id', func: getUser, auth: false })\n```\n\nPikku merges route params + query string + body + headers into a single typed input.\n\n---\n\n## Middleware → pikkuMiddleware\n\n**Traditional:**\n\n```typescript\n// Express-style middleware\nfunction logRequest(req: Request, res: Response, next: NextFunction) {\n console.log(`${req.method} ${req.url}`)\n next()\n console.log(`Response: ${res.statusCode}`)\n}\napp.use(logRequest)\n```\n\n**Pikku:**\n\n```typescript\nconst logRequest = pikkuMiddleware(async ({ logger }, wire, next) => {\n logger.info('Request started')\n await next()\n logger.info('Request completed')\n})\n\n// Apply globally\naddHTTPMiddleware('*', [logRequest])\n\n// Apply by route pattern\naddHTTPMiddleware('/api/*', [logRequest])\n```\n\n**Key difference:** Pikku middleware receives services (injected), not raw req/res.\n\n---\n\n## Auth Guard → Built-in Auth Middleware\n\n**Traditional:**\n\n```typescript\n// Custom auth middleware\nfunction authMiddleware(req, res, next) {\n const token = req.headers.authorization?.replace('Bearer ', '')\n if (!token) return res.status(401).json({ error: 'Unauthorized' })\n try {\n req.user = jwt.verify(token)\n next()\n } catch {\n res.status(401).json({ error: 'Invalid token' })\n }\n}\n```\n\n**Pikku:**\n\n```typescript\nimport { authBearer } from '@pikku/core/middleware'\n\n// One line - handles token extraction, JWT verification, session population\naddHTTPMiddleware('*', [authBearer({})])\n```\n\nOther built-in options: `authCookie({ cookieName })`, `authApiKey({ header })`.\n\n---\n\n## Authorization / Role Checks → pikkuPermission\n\n**Traditional:**\n\n```typescript\n// Guard or middleware that checks roles\nfunction requireRole(role: string) {\n return (req, res, next) => {\n if (req.user.role !== role)\n return res.status(403).json({ error: 'Forbidden' })\n next()\n }\n}\nrouter.delete('/users/:id', requireRole('admin'), deleteUser)\n```\n\n**Pikku:**\n\n```typescript\nconst isOwner = pikkuPermission(async ({ db }, { userId }, wire) => {\n const { session } = wire\n return session?.userId === userId\n})\n\n// Declare on the function definition (the only place permissions live).\n// A capability like \"may delete users\" is a scope, not a permission.\nexport const deleteUser = pikkuFunc({\n func: async (services, data) => {\n /* ... */\n },\n scopes: ['admin:users:delete'],\n permissions: { owner: [isOwner] },\n})\n\n// App-wide baseline every function must also pass (AND gate, narrow-only)\naddGlobalPermission([signedInUser])\n```\n\n**Permission groups:** `{ groupA: [perm1, perm2], groupB: [perm3] }` means `(perm1 AND perm2) OR perm3`.\n\n> Route-pattern (`addHTTPPermission`) and wire-level `permissions` were removed in #972 — permissions live on the function, plus the optional global gate.\n\n---\n\n## DTO / Request Validation → Standard Schema\n\n**Traditional (class-validator):**\n\n```typescript\nclass CreateTodoDTO {\n @IsString()\n @IsNotEmpty()\n @MaxLength(200)\n title: string\n\n @IsOptional()\n @IsIn(['low', 'medium', 'high'])\n priority?: string\n}\n```\n\n**Pikku (Zod):**\n\n```typescript\nconst CreateTodoInputSchema = z.object({\n title: z.string().min(1).max(200),\n priority: z.enum(['low', 'medium', 'high']).optional(),\n})\n\n// Schema declared on function - auto-validated before function runs\nconst createTodo = pikkuSessionlessFunc({\n input: CreateTodoInputSchema,\n output: TodoOutputSchema,\n func: async (services, data) => { ... },\n})\n```\n\nAlso supports Valibot, ArkType, or any Standard Schema-compatible library.\n\n---\n\n## Dependency Injection → Service Factories\n\n**Traditional (class-based DI):**\n\n```typescript\n@Injectable()\nclass TodoService {\n constructor(\n @Inject('DATABASE') private db: Database,\n private logger: Logger\n ) {}\n\n async create(title: string) {\n this.logger.info('Creating todo')\n return this.db.insert('todos', { title })\n }\n}\n```\n\n**Pikku:**\n\n```typescript\n// Define services once at startup\nconst createSingletonServices = pikkuServices(async (config) => ({\n logger: new ConsoleLogger(),\n db: new KyselyService(config.database),\n todoStore: new TodoStore(),\n}))\n\n// Functions destructure what they need\nconst createTodo = pikkuSessionlessFunc(\n async ({ logger, todoStore }, { title }) => {\n logger.info('Creating todo')\n return { todo: todoStore.createTodo(title) }\n }\n)\n```\n\n**Key difference:** No container, no decorators, no class hierarchy. Just a factory function returning an object.\n\nPer-request services (like scoped loggers) use `pikkuWireServices`:\n\n```typescript\nconst createWireServices = pikkuWireServices(\n async (singletonServices, wire) => ({\n scopedLogger: new ScopedLogger(wire.session?.initial?.userId),\n })\n)\n```\n\n---\n\n## WebSocket Handlers → wireChannel\n\n**Traditional:**\n\n```typescript\nwss.on('connection', (ws) => {\n ws.on('message', (raw) => {\n const { type, payload } = JSON.parse(raw)\n switch (type) {\n case 'subscribe':\n handleSubscribe(ws, payload)\n break\n case 'create':\n handleCreate(ws, payload)\n break\n }\n })\n ws.on('close', () => handleDisconnect(ws))\n})\n```\n\n**Pikku:**\n\n```typescript\nwireChannel({\n name: 'todos-live',\n route: '/',\n onConnect, // pikkuVoidFunc\n onDisconnect, // pikkuVoidFunc\n onMessageWiring: {\n action: {\n // First level of message routing\n subscribe: { func: subscribe },\n create: { func: createTodo },\n auth: { func: login, auth: false },\n },\n },\n})\n```\n\nFunctions send data back via `wire.channel.send(data)`. Structured message routing replaces manual switch/case parsing.\n\n---\n\n## Job Queue Workers → wireQueueWorker\n\n**Traditional (Bull/BullMQ):**\n\n```typescript\nconst queue = new Queue('reminders')\n\n// Producer\nawait queue.add('send-reminder', { todoId: '123', userId: 'user1' })\n\n// Consumer\nconst worker = new Worker('reminders', async (job) => {\n const { todoId, userId } = job.data\n await sendReminder(todoId, userId)\n})\n```\n\n**Pikku:**\n\n```typescript\n// Same pikkuFunc shape - knows nothing about queues\nconst processReminder = pikkuSessionlessFunc(\n async ({ todoStore, logger }, { todoId, userId }) => {\n const todo = todoStore.getTodo(todoId)\n if (todo && !todo.completed) {\n logger.info(`Sending reminder for: ${todo.title}`)\n }\n return { processed: true }\n }\n)\n\n// Wire it to a queue\nwireQueueWorker({ name: 'todo-reminders', func: processReminder })\n\n// Enqueue from other functions via queue service\nawait services.queueService.addJob('todo-reminders', {\n todoId: '123',\n userId: 'user1',\n})\n```\n\n---\n\n## Cron / Scheduled Tasks → wireScheduler\n\n**Traditional:**\n\n```typescript\nimport cron from 'node-cron'\ncron.schedule('0 9 * * *', async () => {\n const stats = await todoService.getStats()\n console.log(`Daily: ${stats.completed}/${stats.total}`)\n})\n```\n\n**Pikku:**\n\n```typescript\nconst dailySummary = pikkuVoidFunc(async ({ logger, todoStore }) => {\n const stats = todoStore.getStats('user1')\n logger.info(`Daily: ${stats.completed}/${stats.total}`)\n})\n\nwireScheduler({\n name: 'dailySummary',\n schedule: '0 9 * * *',\n func: dailySummary,\n})\n```\n\n---\n\n## Module / Feature Grouping → Tags + File Organization\n\n**Traditional (NestJS):**\n\n```typescript\n@Module({\n imports: [DatabaseModule],\n controllers: [TodoController],\n providers: [TodoService],\n exports: [TodoService],\n})\nexport class TodoModule {}\n```\n\n**Pikku:**\n\n```text\n// No module system. Organize by convention:\nsrc/\n├── functions/\n│ └── todos.functions.ts # All todo business logic\n├── wirings/\n│ └── todos.http.ts # All todo HTTP routes\n└── schemas.ts # Shared schemas\n\n// Use tags for cross-cutting concerns:\nwireHTTP({ route: '/todos', func: listTodos, tags: ['todos', 'public'] })\naddMiddleware('todos', [loggingMiddleware]) // Applies to all 'todos'-tagged functions\n```\n\n---\n\n## Error Handling → Typed Errors\n\n**Traditional:**\n\n```typescript\nif (!todo) {\n res.status(404).json({ error: 'Todo not found' })\n return\n}\nif (!canEdit(user, todo)) {\n res.status(403).json({ error: 'Forbidden' })\n return\n}\n```\n\n**Pikku:**\n\n```typescript\nimport { NotFoundError, ForbiddenError } from '@pikku/core/errors'\n\nconst updateTodo = pikkuFunc(async (services, { id, title }, wire) => {\n const todo = services.todoStore.getTodo(id)\n if (!todo) throw new NotFoundError('Todo not found')\n\n const { session } = wire\n if (todo.userId !== session.userId) throw new ForbiddenError()\n\n return { todo: services.todoStore.update(id, { title }) }\n})\n```\n\nPikku catches these errors and maps them to appropriate HTTP status codes automatically.\n\n---\n\n## Session Management\n\n**Traditional:**\n\n```typescript\n// Express session\napp.use(session({ store: new RedisStore({ client: redis }), secret: 'key' }))\n\n// In handler\nreq.session.userId = user.id // Set\nconst userId = req.session.userId // Get\nreq.session.destroy() // Clear\n```\n\n**Pikku:**\n\n```typescript\n// Session is on the wire context, managed by auth middleware\nconst login = pikkuSessionlessFunc(\n async ({ jwt }, { username, password }, wire) => {\n const user = authenticate(username, password)\n const token = await jwt.sign({ userId: user.id })\n await wire.setSession({ userId: user.id, user }) // Set\n return { token, user }\n }\n)\n\nconst getMe = pikkuFunc(async (services, data, wire) => {\n const { session } = wire // Get\n return { user: session.user }\n})\n\nconst logout = pikkuFunc(async (services, data, wire) => {\n await wire.clearSession() // Clear\n return { success: true }\n})\n```\n\n---\n\n## API Client Generation\n\n**Traditional:**\n\n```typescript\n// Manually written or generated via OpenAPI\nconst response = await fetch('/api/todos', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ title: 'New todo' }),\n})\nconst data: unknown = await response.json()\n```\n\n**Pikku (auto-generated, fully typed):**\n\n```typescript\nimport { createPikkuFetchClient } from './.pikku/pikku-fetch.gen.js'\n\nconst client = createPikkuFetchClient({ baseUrl: 'http://localhost:4002' })\nconst result = await client.post('/todos', { title: 'New todo' })\n// result is fully typed as TodoOutput - no manual type casting\n```\n\nGenerated by running `npx pikku all`. Both HTTP and WebSocket clients available.\n", "pikku-concepts/references/packages.md": "# Available Pikku Packages\n\n## Runtime Adapters\n\n| Package | Use Case |\n| ----------------------------- | ------------------------------------- |\n| `@pikku/express-server` | Express standalone server |\n| `@pikku/express-middleware` | Express as middleware in existing app |\n| `@pikku/fastify-server` | Fastify standalone |\n| `@pikku/fastify-plugin` | Fastify plugin |\n| `@pikku/next` | Next.js API routes |\n| `@pikku/aws-lambda` | AWS Lambda handlers |\n| `@pikku/cloudflare` | Cloudflare Workers |\n| `@pikku/uws-server` | uWebSockets.js (high perf) |\n| `@pikku/modelcontextprotocol` | MCP server |\n\n## Service Packages\n\n| Package | Provides |\n| ------------------------ | ---------------------------------------------------- |\n| `@pikku/jose` | JWT (sign/verify) via jose library |\n| `@pikku/schema-ajv` | Schema validation via AJV |\n| `@pikku/schema-cfworker` | Schema validation for Cloudflare |\n| `@pikku/pino` | Structured logging via Pino |\n| `@pikku/kysely` | Type-safe SQL via Kysely (PostgreSQL, SQLite, MySQL) |\n| `@pikku/redis` | Redis client |\n| `@pikku/queue-bullmq` | Job queues via BullMQ |\n| `@pikku/queue-pg-boss` | Job queues via PgBoss |\n| `@pikku/aws-services` | AWS SDK (SQS, DynamoDB, etc.) |\n", "pikku-concepts/SKILL.md": "---\nname: pikku-concepts\ndescription: >-\n Foundational guide to Pikku framework concepts. Use this skill when working with any Pikku\n codebase, starting a new Pikku project, or migrating a backend to Pikku. Covers the core mental\n model, function types, project structure, code generation, testing, and how Pikku maps to\n traditional backend patterns. TRIGGER when: user asks \"what is Pikku?\", starts a new Pikku\n project, migrates from Express/NestJS/Hono, or needs to understand how Pikku works. DO NOT\n TRIGGER when: user is doing a specific wiring task (use the specific skill instead, e.g.\n pikku-http, pikku-websocket).\ninstallGroups: [core]\n---\n\n# Pikku Framework Concepts\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nPikku is a TypeScript framework that separates business logic from transport mechanisms. You define a function once, then wire it to HTTP, WebSocket, queues, schedulers, MCP, CLI, or RPC — without the function knowing how it's being called.\n\nFor deep-dive on each topic, see the dedicated skills:\n\n- **Wiring**: `pikku-http`, `pikku-websocket`, `pikku-rpc`, `pikku-mcp`, `pikku-queue`, `pikku-cron`, `pikku-trigger`, `pikku-cli`, `pikku-ai-agent`, `pikku-workflow`\n- **Infrastructure**: `pikku-services`, `pikku-security`, `pikku-config`\n- **Project introspection**: `pikku-info`\n\n## Core Mental Model\n\n```text\npikkuFunc (pure business logic)\n │\n ├── wireHTTP → Express, Fastify, Next.js, Lambda, Cloudflare...\n ├── wireChannel → WebSocket (real-time)\n ├── wireQueueWorker → BullMQ, PgBoss (async jobs)\n ├── wireScheduler → Cron (scheduled tasks)\n ├── wireMCPTool → Model Context Protocol (AI tools)\n ├── wireCLI → CLI commands\n ├── wireTrigger → Event-driven (Redis pub/sub, PG LISTEN/NOTIFY)\n ├── pikkuAIAgent → AI agents / chatbots\n ├── pikkuWorkflow → Multi-step durable workflows\n └── wire.rpc → Internal function-to-function calls\n```\n\nA `pikkuFunc` receives three things:\n\n1. **Services** — injected dependencies (logger, db, jwt, custom stores). See `pikku-services`.\n2. **Data** — input from any source (HTTP body/query/params, WS message, queue payload, CLI args)\n3. **Wire** — transport context (session, channel, rpc, mcp, http, queue)\n\nThe function never imports Express, never reads `req.body`, never touches `ws.send()`. It just works with typed data and services.\n\n## Concept Mapping: Generic Backend → Pikku\n\nControllers/routes → `pikkuFunc`; middleware/auth/permissions → `pikku-security`; DI → `pikku-services`; transports (HTTP/WS/queue/cron) → their `wire*` + skill. For the full Generic Backend → Pikku mapping table (with side-by-side code examples), read `references/concept-mapping.md`.\n\n## Functions\n\nThree main function types:\n\n```typescript\n// Requires authentication — receives session in wire context.\n// input/output are Zod schemas; the data + return types are inferred from them.\nconst updateTodo = pikkuFunc({\n input: UpdateTodoInput,\n output: TodoOutput,\n func: async (services, data, wire) => {\n const { session } = wire\n return services.todoStore.update(data.id, data)\n },\n})\n\n// No authentication required\nconst listTodos = pikkuSessionlessFunc({\n input: ListTodosInput,\n output: TodoListOutput,\n func: async (services, data) => {\n return { todos: services.todoStore.list(data.filters) }\n },\n})\n\n// No input or output (for scheduled tasks, lifecycle hooks)\nconst cleanup = pikkuVoidFunc(async (services) => {\n services.todoStore.cleanOldItems()\n})\n```\n\nServices can be destructured inline in the `func` signature (e.g. `async ({ logger, todoStore }, { title }) => ...`). Full config options:\n\n```typescript\npikkuFunc({\n title?: string, // Human-readable name\n description?: string, // What the function does\n version?: number, // Contract version (see pikku-config for versioning)\n tags?: string[], // For grouping and middleware targeting\n expose?: boolean, // Allow external RPC calls (see pikku-rpc)\n remote?: boolean, // Allow remote RPC calls\n mcp?: boolean, // Expose as MCP tool (see pikku-mcp)\n auth?: boolean, // Override default auth requirement\n input?: ZodSchema, // Input validation schema\n output?: ZodSchema, // Output validation schema\n permissions?: PermissionGroup, // See pikku-security\n middleware?: PikkuMiddleware[], // See pikku-security\n func: async (services, data, wire) => { ... },\n})\n```\n\n**Generics XOR `input`/`output` — never both.** A function's data and return\ntypes come from *one* source: either the `input`/`output` schemas (preferred —\nthey double as runtime validation and OpenAPI) or type generics\n(`pikkuFunc<In, Out>({ ... })`). Passing both makes the two disagree and forces\n`as any` casts. Do not annotate the `func` return type inline either — let the\n`output` schema (or the generic) be the single source of truth for the type.\n\n```typescript\n// Correct — schema-based (no generics, no inline return type)\npikkuFunc({ input: MyInput, output: MyOutput, func: async (s, d) => { ... } })\n// Correct — generic-based (no input/output)\npikkuFunc<MyIn, MyOut>({ func: async (s, d) => { ... } })\n// WRONG — mixing the two\npikkuFunc<MyIn, MyOut>({ input: MyInput as any, func: async (s, d) => { ... } })\n```\n\n## Schemas (Validation)\n\nPikku uses Standard Schema — works with Zod, Valibot, ArkType:\n\n```typescript\nimport { z } from 'zod'\n\nconst CreateTodoInputSchema = z.object({\n title: z.string().min(1).max(200),\n priority: z.enum(['low', 'medium', 'high']).optional(),\n tags: z.array(z.string()).optional(),\n})\n```\n\nSchemas serve triple duty: runtime validation, TypeScript types, and OpenAPI documentation.\n\n## Server Bootstrap\n\nEvery Pikku app follows the same bootstrap pattern regardless of runtime:\n\n```typescript\nimport '../../functions/.pikku/pikku-bootstrap.gen.js' // Generated — registers all wirings\n\nconst config = await createConfig()\nconst singletonServices = await createSingletonServices(config)\n\n// Pick your runtime:\nconst server = new PikkuFastifyServer(\n config,\n singletonServices,\n createWireServices\n)\n// or: new PikkuExpressServer(config, singletonServices, createWireServices)\n// or: pikkuAWSLambdaHandler(singletonServices)\n// or: PikkuCloudflareHandler(singletonServices)\n// or: pikkuNextHandler(singletonServices)\n\nawait server.init()\nawait server.start()\n```\n\n## Code Generation\n\nRun `npx pikku all` to generate:\n\n- `pikku-types.gen.ts` — Typed function factories and wiring functions\n- `pikku-fetch.gen.ts` — Type-safe HTTP client\n- `pikku-websocket.gen.ts` — Type-safe WebSocket client\n- `pikku-bootstrap.gen.js` — Runtime initialization (auto-imports all wirings)\n- `pikku-services.gen.ts` — Service factory types\n\nConfig lives in `pikku.config.json`:\n\n```json\n{\n \"tsconfig\": \"./tsconfig.json\",\n \"srcDirectories\": [\"src\"],\n \"outDir\": \".pikku\"\n}\n```\n\n## Project Structure Convention\n\n```text\nsrc/\n├── functions/ # Business logic (pikkuFunc definitions)\n│ ├── todos.functions.ts\n│ ├── auth.functions.ts\n│ └── scheduled.functions.ts\n├── wirings/ # Transport bindings\n│ ├── todos.http.ts\n│ ├── channel.wiring.ts\n│ ├── scheduler.wiring.ts\n│ └── queue.wiring.ts\n├── schemas.ts # Zod/Valibot schemas\n├── services.ts # Service factories (see pikku-services)\n├── middleware.ts # Middleware definitions (see pikku-security)\n├── permissions.ts # Permission definitions (see pikku-security)\n└── .pikku/ # Generated (gitignored)\n ├── pikku-types.gen.ts\n ├── pikku-fetch.gen.ts\n └── pikku-bootstrap.gen.js\n```\n\n## Environment Variables\n\nNever use `process.env` inside Pikku functions. Use the `variables` service (see `pikku-config`):\n\n```typescript\nconst apiKey = services.variables.get('API_KEY')\n```\n\n`process.env` belongs in server bootstrap code (`start.ts`) only.\n\n## Testing\n\nFunctions are easily testable because they're pure:\n\n```typescript\nconst mockServices = {\n logger: new MockLogger(),\n todoStore: new MockTodoStore(),\n}\n\n// Call function directly — no HTTP, no framework\nconst result = await listTodos.func(mockServices, { userId: 'test' })\nexpect(result.todos).toHaveLength(3)\n```\n\n## Available Packages\n\nPikku ships runtime adapters (`@pikku/express-server`, `@pikku/fastify-server`, `@pikku/next`, `@pikku/aws-lambda`, `@pikku/cloudflare`, `@pikku/uws-server`, `@pikku/modelcontextprotocol`, ...) and service packages (`@pikku/jose`, `@pikku/schema-ajv`, `@pikku/pino`, `@pikku/kysely`, `@pikku/redis`, `@pikku/queue-bullmq`, `@pikku/queue-pg-boss`, ...). For the full list with use cases, read `references/packages.md`.\n\n## Key Differences from Traditional Frameworks\n\n1. **No decorators** — plain functions + explicit wiring, not `@Get()` or `@Injectable()`\n2. **No classes required** — everything is functions and objects\n3. **Transport is configuration, not code** — business logic doesn't know about HTTP/WS/etc.\n4. **One function, many transports** — same function can serve HTTP, WebSocket, queue, and MCP simultaneously\n5. **Generated type safety** — clients are auto-generated with full types, not manually maintained\n6. **Schema-first validation** — Standard Schema (Zod/Valibot) replaces class-validator decorators\n", "pikku-config/SKILL.md": "---\nname: pikku-config\ndescription: >-\n Use when managing secrets, environment variables, config, or OAuth2 credentials in a Pikku app.\n Covers wireSecret, wireVariable, wireOAuth2Credential, and typed config access. TRIGGER when:\n code uses wireSecret/wireVariable/wireOAuth2Credential, user asks about env vars, secrets,\n config, OAuth2, or \"how do I access environment variables\". DO NOT TRIGGER when: user asks about\n API versioning/breaking changes (use pikku-versioning), service factories (use pikku-services),\n or auth middleware (use pikku-security).\ninstallGroups: [core]\n---\n\n# Pikku Config, Secrets & OAuth2\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nManage secrets, variables, and OAuth2 credentials. Never use `process.env` in Pikku functions — use typed services instead.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their versions\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## Secrets & Variables\n\n### `wireSecret(config)`\n\nDeclare a secret with a Zod schema for type-safe access:\n\n```typescript\nwireSecret({\n name: string, // Secret identifier\n schema: ZodSchema, // Shape and validation\n})\n```\n\n### `wireVariable(config)`\n\nDeclare a variable (non-sensitive config) with a Zod schema:\n\n```typescript\nwireVariable({\n name: string,\n schema: ZodSchema,\n})\n```\n\n### Accessing in Functions\n\n```typescript\n// Secrets — encrypted, sensitive values\nconst config = await services.secrets.getSecret('SECRET_NAME')\n\n// Variables — plain-text configuration\nconst flags = await services.variables.getVariableJSON('VARIABLE_NAME')\n\n// Simple string access\nconst apiKey = services.variables.get('API_KEY')\n```\n\n### Local Development Services\n\n```typescript\nimport { LocalSecretService, LocalVariablesService } from '@pikku/core/services'\n\nconst createSingletonServices = pikkuServices(async (config) => ({\n secrets: new LocalSecretService(), // Reads from .env or local files\n variables: new LocalVariablesService(), // Reads from environment\n}))\n```\n\n### Usage Patterns\n\n```typescript\n// Declare secrets with typed schemas\nwireSecret({\n name: 'STRIPE_CONFIG',\n schema: z.object({\n apiKey: z.string().startsWith('sk_'),\n webhookSecret: z.string(),\n }),\n})\n\n// In your function — fully typed\nconst config = await secrets.getSecret('STRIPE_CONFIG')\n// config.apiKey → string (autocompleted)\n// config.webhookSecret → string (autocompleted)\n\n// Declare variables\nwireVariable({\n name: 'FEATURE_FLAGS',\n schema: z.object({\n darkMode: z.boolean(),\n maxUploadMB: z.number().default(10),\n }),\n})\n\n// Read it — typed and validated\nconst flags = await variables.getVariableJSON('FEATURE_FLAGS')\n// flags.darkMode → boolean\n// flags.maxUploadMB → number\n```\n\n## OAuth2 Credentials\n\n### `wireOAuth2Credential(config)`\n\n```typescript\nwireOAuth2Credential({\n name: string, // Credential identifier\n displayName: string, // Human-readable name\n secretId: string, // Secret holding { clientId, clientSecret }\n tokenSecretId: string, // Secret for token storage (auto-refreshed)\n authorizationUrl: string, // OAuth2 authorization endpoint\n tokenUrl: string, // OAuth2 token endpoint\n scopes: string[], // Required OAuth2 scopes\n})\n```\n\n### Usage\n\n```typescript\nwireOAuth2Credential({\n name: 'slackOAuth',\n displayName: 'Slack OAuth',\n secretId: 'SLACK_OAUTH_APP',\n tokenSecretId: 'SLACK_OAUTH_TOKENS',\n authorizationUrl: 'https://slack.com/oauth/v2/authorize',\n tokenUrl: 'https://slack.com/api/oauth.v2.access',\n scopes: ['chat:write', 'channels:read'],\n})\n\n// In your function — tokens refresh automatically\nconst response = await slackOAuth.request(\n 'https://slack.com/api/chat.postMessage',\n {\n method: 'POST',\n body: JSON.stringify({ channel, text }),\n }\n)\nconst data = await response.json()\n```\n\n## Key Rule\n\n**Never use `process.env` inside Pikku functions.** Use the `variables` or `secrets` service:\n\n```typescript\n// ❌ Wrong\nconst apiKey = process.env.API_KEY\n\n// ✅ Correct\nconst apiKey = services.variables.get('API_KEY')\n```\n\n`process.env` belongs only in server bootstrap code (`start.ts`).\n\n## Complete Example\n\n```typescript\n// schemas/config.ts\nwireSecret({\n name: 'DATABASE_CONFIG',\n schema: z.object({\n connectionString: z.string().url(),\n maxPoolSize: z.number().default(10),\n }),\n})\n\nwireVariable({\n name: 'APP_CONFIG',\n schema: z.object({\n appName: z.string(),\n maxUploadSizeMB: z.number().default(10),\n maintenanceMode: z.boolean().default(false),\n }),\n})\n\nwireOAuth2Credential({\n name: 'githubOAuth',\n displayName: 'GitHub OAuth',\n secretId: 'GITHUB_OAUTH_APP',\n tokenSecretId: 'GITHUB_OAUTH_TOKENS',\n authorizationUrl: 'https://github.com/login/oauth/authorize',\n tokenUrl: 'https://github.com/login/oauth/access_token',\n scopes: ['read:user', 'repo'],\n})\n\n// functions/admin.functions.ts\nexport const getAppStatus = pikkuSessionlessFunc({\n title: 'Get App Status',\n func: async ({ variables, secrets }) => {\n const appConfig = await variables.getVariableJSON('APP_CONFIG')\n return {\n appName: appConfig.appName,\n maintenanceMode: appConfig.maintenanceMode,\n }\n },\n})\n```\n", "pikku-cron/SKILL.md": "---\nname: pikku-cron\ndescription: >-\n Use when adding scheduled tasks, recurring jobs, or cron-based automation to a Pikku app. Covers\n wireScheduler, cron expressions, scheduled task wire object, and scheduler middleware. TRIGGER\n when: code uses wireScheduler, user asks about cron, scheduled tasks, recurring jobs, or \"run\n every X minutes/hours\". DO NOT TRIGGER when: user asks about background jobs with retries (use\n pikku-queue) or event-driven triggers (use pikku-trigger).\n---\n\n# Pikku Cron/Scheduler Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions to run on a schedule using cron expressions. Uses `pikkuVoidFunc` (no input/output).\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their types\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireScheduler(config)`\n\n```typescript\nimport { wireScheduler } from '@pikku/core/scheduler'\n\nwireScheduler({\n name: string, // Unique scheduler name\n schedule: string, // Cron expression\n func: PikkuVoidFunc, // Must be pikkuVoidFunc (no input/output)\n middleware?: PikkuMiddleware[],\n})\n```\n\n### Wire Object (`wire.scheduledTask`)\n\nInside scheduled functions:\n\n```typescript\nwire.scheduledTask.name // Scheduler name\nwire.scheduledTask.schedule // Cron expression string\nwire.scheduledTask.executionTime // When this execution was triggered\nwire.scheduledTask.skip(reason) // Skip this execution (no error)\n```\n\n### Cron Expression Reference\n\n```\n┌───────────── minute (0-59)\n│ ┌───────────── hour (0-23)\n│ │ ┌───────────── day of month (1-31)\n│ │ │ ┌───────────── month (1-12)\n│ │ │ │ ┌───────────── day of week (0-7, 0 and 7 = Sunday)\n│ │ │ │ │\n* * * * *\n```\n\nCommon patterns:\n\n| Expression | Meaning |\n| ------------- | -------------------------- |\n| `*/5 * * * *` | Every 5 minutes |\n| `0 9 * * *` | Daily at 9:00 AM |\n| `0 9 * * 1` | Every Monday at 9:00 AM |\n| `0 0 1 * *` | First of month at midnight |\n| `0 */6 * * *` | Every 6 hours |\n| `30 2 * * 0` | Sundays at 2:30 AM |\n\n## Usage Patterns\n\n### Basic Scheduled Task\n\n```typescript\nconst dailySummary = pikkuVoidFunc({\n title: 'Daily Summary',\n func: async ({ db, emailService, logger }) => {\n logger.info('Generating daily summary')\n const stats = await db.getDailyStats()\n await emailService.sendSummary(stats)\n },\n})\n\nwireScheduler({\n name: 'dailySummary',\n schedule: '0 9 * * *',\n func: dailySummary,\n})\n```\n\n### Using the Wire Object\n\n```typescript\nconst weeklyCleanup = pikkuVoidFunc({\n title: 'Weekly Cleanup',\n func: async ({ db, logger }, _input, wire) => {\n logger.info(`Running: ${wire.scheduledTask.name}`)\n logger.info(`Schedule: ${wire.scheduledTask.schedule}`)\n logger.info(`Execution time: ${wire.scheduledTask.executionTime}`)\n\n const staleCount = await db.countStaleTodos()\n if (staleCount === 0) {\n wire.scheduledTask.skip('No stale todos found')\n return\n }\n\n await db.deleteCompletedTodos({ olderThan: '30d' })\n logger.info(`Cleaned ${staleCount} stale todos`)\n },\n})\n\nwireScheduler({\n name: 'weeklyCleanup',\n schedule: '0 0 * * 0',\n func: weeklyCleanup,\n})\n```\n\n### Scheduler Middleware\n\n```typescript\nconst schedulerMetrics = pikkuMiddleware(\n async ({ logger }, { scheduledTask }, next) => {\n const start = Date.now()\n logger.info(`Task started: ${scheduledTask.name}`)\n\n try {\n await next()\n logger.info(`Task completed: ${scheduledTask.name}`, {\n duration: Date.now() - start,\n })\n } catch (error) {\n logger.error(`Task failed: ${scheduledTask.name}`, {\n error: error.message,\n duration: Date.now() - start,\n })\n throw error\n }\n }\n)\n\nwireScheduler({\n name: 'dailySummary',\n schedule: '0 9 * * *',\n func: dailySummary,\n middleware: [schedulerMetrics],\n})\n```\n\n## Complete Example\n\n```typescript\n// functions/scheduled.functions.ts\nexport const dailySummary = pikkuVoidFunc({\n title: 'Daily Summary',\n func: async ({ db, emailService, logger }) => {\n const stats = await db.getDailyStats()\n await emailService.sendSummary(stats)\n logger.info('Daily summary sent', { stats })\n },\n})\n\nexport const cleanupExpired = pikkuVoidFunc({\n title: 'Cleanup Expired',\n func: async ({ db, logger }, _input, wire) => {\n const count = await db.countExpiredSessions()\n if (count === 0) {\n wire.scheduledTask.skip('No expired sessions')\n return\n }\n await db.deleteExpiredSessions()\n logger.info(`Cleaned ${count} expired sessions`)\n },\n})\n\nexport const syncInventory = pikkuVoidFunc({\n title: 'Sync Inventory',\n func: async ({ inventoryApi, db, logger }) => {\n const updates = await inventoryApi.getChanges()\n await db.applyInventoryUpdates(updates)\n logger.info(`Synced ${updates.length} inventory changes`)\n },\n})\n\n// wirings/scheduler.wiring.ts\nwireScheduler({\n name: 'dailySummary',\n schedule: '0 9 * * *',\n func: dailySummary,\n})\nwireScheduler({\n name: 'cleanupExpired',\n schedule: '0 */6 * * *',\n func: cleanupExpired,\n})\nwireScheduler({\n name: 'syncInventory',\n schedule: '*/15 * * * *',\n func: syncInventory,\n})\n```\n", "pikku-deploy-azure/SKILL.md": "---\nname: pikku-deploy-azure\ndescription: >-\n Use when deploying a Pikku app to Azure Functions. Covers PikkuAzFunctionsLogger and\n PikkuAzTimerRequest for Azure Functions runtime. TRIGGER when: user asks about Azure Functions,\n Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when: user asks about AWS Lambda\n (use pikku-deploy-lambda) or Cloudflare Workers (use pikku-deploy-cloudflare).\n---\n\n# Pikku Azure Functions Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/azure-functions` provides Azure Functions runtime adapters for Pikku.\n\n## Installation\n\n```bash\nyarn add @pikku/azure-functions @azure/functions\n```\n\n## API Reference\n\n### `PikkuAzFunctionsLogger`\n\nLogger implementation that integrates with Azure Functions' built-in logging context.\n\n### `PikkuAzTimerRequest`\n\nTimer trigger request handler for running Pikku scheduled functions as Azure Timer Triggers.\n\n## Usage Patterns\n\n### HTTP Function\n\n```typescript\nimport { app } from '@azure/functions'\nimport { PikkuAzFunctionsLogger } from '@pikku/azure-functions'\n\napp.http('api', {\n methods: ['GET', 'POST', 'PUT', 'DELETE'],\n route: '{*path}',\n handler: async (request, context) => {\n const logger = new PikkuAzFunctionsLogger(context)\n // Wire Pikku HTTP runner with Azure request/response\n },\n})\n```\n\n### Timer Trigger\n\n```typescript\nimport { app } from '@azure/functions'\nimport { PikkuAzTimerRequest } from '@pikku/azure-functions'\n\napp.timer('scheduler', {\n schedule: '0 */5 * * * *',\n handler: async (timer, context) => {\n const request = new PikkuAzTimerRequest(timer)\n // Process scheduled Pikku functions\n },\n})\n```\n", "pikku-deploy-cloudflare/SKILL.md": "---\nname: pikku-deploy-cloudflare\ndescription: >-\n Use when deploying a Pikku app to Cloudflare Workers. Covers HTTP fetch handler, scheduled\n tasks, and WebSocket via Durable Objects. TRIGGER when: code imports @pikku/cloudflare, user\n mentions Cloudflare Workers deployment, or worker entry uses ExportedHandler/wrangler.toml. DO\n NOT TRIGGER when: just defining functions/wirings without Cloudflare-specific code.\ninstallGroups: [fabric]\n---\n\n# Pikku Cloudflare Workers Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n```bash\nyarn add @pikku/cloudflare\n```\n\n## Worker Entry\n\n```typescript\nimport { runFetch, runScheduled } from '@pikku/cloudflare'\nimport { setupServices } from './setup-services.js'\nimport './.pikku/pikku-bootstrap.gen.js'\n\nexport default {\n async scheduled(controller, env) {\n await setupServices(env)\n await runScheduled(controller)\n },\n\n async fetch(request, env): Promise<Response> {\n await setupServices(env)\n return await runFetch(request as unknown as Request)\n },\n} satisfies ExportedHandler<Record<string, string>>\n```\n\n## Service Setup\n\nCloudflare passes env variables per-request — wrap them with Pikku services:\n\n```typescript\n// setup-services.ts\nimport { LocalVariablesService, LocalSecretService } from '@pikku/core/services'\nimport { createConfig, createSingletonServices } from './services.js'\n\nexport const setupServices = async (\n env: Record<string, string | undefined>\n) => {\n const localVariables = new LocalVariablesService(env)\n const config = await createConfig(localVariables)\n const localSecrets = new LocalSecretService(localVariables)\n return await createSingletonServices(config, {\n variables: localVariables,\n secrets: localSecrets,\n })\n}\n```\n\n## WebSocket (Durable Objects)\n\n```typescript\nimport { CloudflareWebSocketHibernationServer } from '@pikku/cloudflare'\n\nexport class WebSocketHibernationServer extends CloudflareWebSocketHibernationServer {\n protected async getParams() {\n const singletonServices = await setupServices(this.env)\n return { singletonServices }\n }\n}\n```\n\nRegister the Durable Object in `wrangler.toml` and export from the worker entry.\n", "pikku-deploy-express/SKILL.md": "---\nname: pikku-deploy-express\ndescription: >-\n Use when deploying a Pikku app with Express. Covers PikkuExpressServer standalone and\n pikkuExpressMiddleware for existing Express apps. TRIGGER when: code imports @pikku/express or\n @pikku/express-middleware, user mentions Express deployment, or start.ts creates a\n PikkuExpressServer. DO NOT TRIGGER when: just defining functions/wirings without\n Express-specific code.\n---\n\n# Pikku Express Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n## Standalone Server\n\n```bash\nyarn add @pikku/express\n```\n\n```typescript\nimport { PikkuExpressServer } from '@pikku/express'\nimport './.pikku/pikku-bootstrap.gen.js'\nimport { createConfig, createSingletonServices } from './services.js'\n\nconst config = await createConfig()\nconst singletonServices = await createSingletonServices(config)\n\nconst appServer = new PikkuExpressServer(\n { ...config, port: 4002, hostname: 'localhost' },\n singletonServices.logger\n)\nappServer.enableExitOnSigInt()\nawait appServer.init()\nawait appServer.start()\n```\n\n**Constructor:** `new PikkuExpressServer(config, logger)`\n\n**Config extends CoreConfig with:**\n\n- `port: number`\n- `hostname: string`\n- `healthCheckPath?: string`\n- `limits?: Partial<Record<string, string>>`\n- `content?: LocalContentConfig` (for static assets / file uploads)\n\n**Methods:**\n\n- `init(httpOptions?): Promise<void>` — Register middleware and routes\n- `start(): Promise<void>` — Start listening\n- `stop(): Promise<void>` — Graceful shutdown\n- `enableExitOnSigInt(): Promise<void>` — SIGINT handler\n- `enableCors(options): void` — Enable CORS\n- `enableStaticAssets(): void` — Serve static files (requires `content` config)\n\n**Property:** `app: Express` — Direct access to Express instance for custom middleware.\n\n## Middleware (existing Express app)\n\n```bash\nyarn add @pikku/express-middleware\n```\n\n```typescript\nimport express from 'express'\nimport { pikkuExpressMiddleware } from '@pikku/express-middleware'\nimport './.pikku/pikku-bootstrap.gen.js'\n\nconst app = express()\napp.use(\n pikkuExpressMiddleware({\n logger: singletonServices.logger,\n logRoutes: true,\n loadSchemas: true,\n })\n)\n```\n", "pikku-deploy-fastify/SKILL.md": "---\nname: pikku-deploy-fastify\ndescription: >-\n Use when deploying a Pikku app with Fastify. Covers PikkuFastifyServer standalone and\n pikkuFastifyPlugin for existing Fastify apps. TRIGGER when: code imports @pikku/fastify or\n @pikku/fastify-plugin, user mentions Fastify deployment, or start.ts creates a\n PikkuFastifyServer. DO NOT TRIGGER when: just defining functions/wirings without\n Fastify-specific code.\n---\n\n# Pikku Fastify Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n## Standalone Server\n\n```bash\nyarn add @pikku/fastify\n```\n\n```typescript\nimport { PikkuFastifyServer } from '@pikku/fastify'\nimport './.pikku/pikku-bootstrap.gen.js'\nimport { createConfig, createSingletonServices } from './services.js'\n\nconst config = await createConfig()\nconst singletonServices = await createSingletonServices(config)\n\nconst appServer = new PikkuFastifyServer(\n { ...config, hostname: 'localhost', port: 4002 },\n singletonServices.logger\n)\nappServer.enableExitOnSigInt()\nawait appServer.init()\nawait appServer.start()\n```\n\n**Constructor:** `new PikkuFastifyServer(config, logger)`\n\n**Config extends CoreConfig with:** `port`, `hostname`, `healthCheckPath?`\n\n**Methods:** `init(httpOptions?)`, `start()`, `stop()`, `enableExitOnSigInt()`\n\n**Property:** `app: FastifyInstance` — Direct access to Fastify instance.\n\n## Plugin (existing Fastify app)\n\n```bash\nyarn add @pikku/fastify-plugin\n```\n\n```typescript\nimport Fastify from 'fastify'\nimport pikkuFastifyPlugin from '@pikku/fastify-plugin'\nimport './.pikku/pikku-bootstrap.gen.js'\n\nconst app = Fastify()\napp.register(pikkuFastifyPlugin, {\n pikku: {\n logger: singletonServices.logger,\n logRoutes: true,\n loadSchemas: true,\n },\n})\n```\n", "pikku-deploy-lambda/SKILL.md": "---\nname: pikku-deploy-lambda\ndescription: >-\n Use when deploying a Pikku app to AWS Lambda. Covers HTTP handlers, scheduled tasks, SQS queue\n workers, WebSocket via API Gateway, and cold start caching. TRIGGER when: code imports\n @pikku/lambda, user mentions Lambda/serverless/AWS deployment, or handler files export\n Lambda-typed functions. DO NOT TRIGGER when: just defining functions/wirings without\n Lambda-specific code.\n---\n\n# Pikku AWS Lambda Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n```bash\nyarn add @pikku/lambda\n```\n\n## Cold Start Pattern\n\nCache singleton services across Lambda invocations:\n\n```typescript\n// cold-start.ts\nimport './.pikku/pikku-bootstrap.gen.js'\nimport { createConfig, createSingletonServices } from './services.js'\n\nlet singletonServices: SingletonServices | undefined\n\nexport const coldStart = async () => {\n if (!singletonServices) {\n const config = await createConfig()\n singletonServices = await createSingletonServices(config)\n }\n return singletonServices\n}\n```\n\n## HTTP Handler\n\n```typescript\nimport type { APIGatewayProxyEvent } from 'aws-lambda'\nimport { runFetch } from '@pikku/lambda/http'\n\nexport const httpRoute = async (event: APIGatewayProxyEvent) => {\n await coldStart()\n return await runFetch(event)\n}\n```\n\n## Scheduled Tasks\n\n```typescript\nimport type { ScheduledHandler } from 'aws-lambda'\nimport { runScheduledTask } from '@pikku/core/scheduler'\n\nexport const myScheduledTask: ScheduledHandler = async () => {\n await coldStart()\n await runScheduledTask({ name: 'myScheduledTask' })\n}\n```\n\n## SQS Queue Worker\n\n```typescript\nimport type { SQSHandler } from 'aws-lambda'\nimport { runSQSQueueWorker } from '@pikku/lambda/queue'\n\nexport const mySQSWorker: SQSHandler = async (event) => {\n const { logger } = await coldStart()\n return runSQSQueueWorker(logger, event)\n}\n```\n\n## WebSocket (API Gateway v2)\n\n```typescript\nimport {\n connectWebsocket,\n disconnectWebsocket,\n processWebsocketMessage,\n LambdaEventHubService,\n} from '@pikku/lambda/websocket'\n\nexport const connectHandler = async (event) => {\n const params = await getParams(event)\n await connectWebsocket(event, params)\n return { statusCode: 200, body: '' }\n}\n\nexport const disconnectHandler = async (event) => {\n const params = await getParams(event)\n return await disconnectWebsocket(event, params)\n}\n\nexport const defaultHandler = async (event) => {\n const params = await getParams(event)\n return await processWebsocketMessage(event, params)\n}\n```\n\nWebSocket requires a `ChannelStore` (e.g., `PgChannelStore`) and `LambdaEventHubService` for cross-connection messaging.\n", "pikku-deploy-nextjs/SKILL.md": "---\nname: pikku-deploy-nextjs\ndescription: >-\n Use when deploying a Pikku app with Next.js. Covers API route handlers, server-side data\n fetching, and RPC calls from Server Components. TRIGGER when: code imports @pikku/next, user\n mentions Next.js integration, or app/api route files use pikkuAPIRequest. DO NOT TRIGGER when:\n just defining functions/wirings without Next.js-specific code.\n---\n\n# Pikku Next.js Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n```bash\nyarn add @pikku/next\n```\n\n## API Route Handler\n\nThe CLI generates a typed wrapper. Use it in a catch-all route:\n\n```typescript\n// app/api/[...route]/route.ts\nimport { pikkuAPIRequest } from '@/pikku-nextjs.gen.js'\n\nexport const GET = pikkuAPIRequest\nexport const POST = pikkuAPIRequest\nexport const PUT = pikkuAPIRequest\nexport const PATCH = pikkuAPIRequest\nexport const DELETE = pikkuAPIRequest\n```\n\n## Server-Side Data Fetching\n\nUse the generated `pikku()` helper in Server Components or Server Actions:\n\n```typescript\nimport { pikku } from '@/pikku-nextjs.gen.js'\n\nconst { get, post, del, rpc, staticGet, staticPost, staticRPC } = pikku()\n\n// Dynamic (reads headers/cookies — requires request context)\nconst todos = await get('/todos')\nconst created = await post('/todos', { title: 'Buy milk' })\n\n// Static (no request context — suitable for precompile/ISR)\nconst config = await staticGet('/config')\n\n// RPC calls\nconst result = await rpc('calculateTax', { amount: 100, region: 'US' })\n```\n\n**Dynamic vs Static:**\n\n- `get`, `post`, `del`, `rpc` — access headers/cookies, use in dynamic Server Components\n- `staticGet`, `staticPost`, `staticRPC` — no request context, safe for precompile/ISR\n\n## How It Works\n\n`PikkuNextJS` lazy-initializes on first request:\n\n```typescript\nimport { PikkuNextJS } from '@pikku/next'\n\nconst pikku = new PikkuNextJS(createConfig, createSingletonServices)\n```\n\n**Constructor:** `new PikkuNextJS(createConfig?, createSingletonServices)`\n\nThe generated `pikku-nextjs.gen.ts` wraps this with full type safety from your route definitions.\n", "pikku-deploy-uws/SKILL.md": "---\nname: pikku-deploy-uws\ndescription: >-\n Use when deploying a Pikku app with uWebSockets.js. Covers PikkuUWSServer with built-in HTTP and\n WebSocket support, and pikkuWebsocketHandler for standalone ws library. TRIGGER when: code\n imports @pikku/uws or @pikku/ws, user mentions uWebSockets or high-performance server, or\n start.ts creates a PikkuUWSServer. DO NOT TRIGGER when: just defining functions/wirings without\n uWS-specific code.\n---\n\n# Pikku uWebSockets.js Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nHighest-throughput option among Pikku's runtimes. Handles both HTTP and WebSocket automatically.\n\n```bash\nyarn add @pikku/uws\n```\n\n```typescript\nimport { PikkuUWSServer } from '@pikku/uws'\nimport './.pikku/pikku-bootstrap.gen.js'\nimport { createConfig, createSingletonServices } from './services.js'\n\nconst config = await createConfig()\nconst singletonServices = await createSingletonServices(config)\n\nconst appServer = new PikkuUWSServer(\n { ...config, hostname: 'localhost', port: 4002 },\n singletonServices.logger\n)\nappServer.enableExitOnSigInt()\nawait appServer.init()\nawait appServer.start()\n```\n\n**Constructor:** `new PikkuUWSServer(config, logger)`\n\n**Config extends CoreConfig with:** `port`, `hostname`, `healthCheckPath?`\n\n**Methods:** `init(httpOptions?)`, `start()`, `stop()`, `enableExitOnSigInt()`\n\n**Property:** `app: uWS.App` — Direct access to uWebSockets app instance.\n\n## WebSocket Standalone (ws library)\n\nFor WebSocket-only servers using the `ws` library:\n\n```bash\nyarn add @pikku/ws\n```\n\n```typescript\nimport { pikkuWebsocketHandler } from '@pikku/ws'\nimport { stopSingletonServices } from '@pikku/core'\nimport { Server } from 'http'\nimport { WebSocketServer } from 'ws'\nimport './.pikku/pikku-bootstrap.gen.js'\n\nconst server = new Server()\nconst wss = new WebSocketServer({ noServer: true })\n\npikkuWebsocketHandler({\n server,\n wss,\n logger: singletonServices.logger,\n})\n\nserver.listen(4002, 'localhost', () => {\n console.log('Server running at http://localhost:4002/')\n})\n\nprocess.on('SIGINT', async () => {\n await stopSingletonServices()\n wss.close()\n server.close()\n process.exit(0)\n})\n```\n", "pikku-deps/SKILL.md": "---\nname: pikku-deps\ndescription: >-\n Use for the Pikku dependency security audit: the `pikku audit` CLI command, the\n `.pikku/audit.json` artifact, the `SecurityAuditReport` type in @pikku/core, and the console\n Security screen (getSecurityAudit / runSecurityAudit / updateDependency + SecurityAuditView).\n TRIGGER when: user asks about `pikku audit`, dependency vulnerabilities/advisories, outdated\n dependencies, the Security screen/page in the console, updating a vulnerable dependency, or\n reading/rendering audit.json. DO NOT TRIGGER when: user asks about authentication/sessions/JWT\n (use pikku-security), permissions (use pikku-permissions), or secrets/env vars (use\n pikku-config).\ninstallGroups: [core]\n---\n\n# Pikku Dependency Audit\n\n## Agent Operating Procedure\n\n1. The audit is a generated artifact, not live state. `pikku audit` writes the\n normalised report to `.pikku/audit.json` (config `outDir`), so it rides the\n same meta pipeline as every other codegen output — uploaded on deploy,\n readable by the console addon and any tooling. Read it via\n `metaService.readFile('audit.json')`, never by shelling out to the package\n manager from a function.\n2. One source of truth for the shape: `SecurityAuditReport` (and\n `SecurityAuditIssue` / `SecurityAuditUpdate` / `SecurityAuditSummary` +\n `SecuritySeverity` / `SecurityUpdateLevel`) are exported from **@pikku/core**.\n The CLI writes it, the addon reads it, the UI renders it — never redeclare\n the type at a call site.\n3. Validate with `pikku all --tsc` after changes — it type-checks and **fails on\n type errors**, like any real build gate. Separately, `pikku audit` never fails\n a build: advisories are informational, and a missing/failed audit yields an\n empty-but-valid report.\n\n## The `pikku audit` command\n\n- `pikku audit` — reports **security advisories** only.\n- `pikku audit --outdated` — also reports **available dependency updates**.\n- Package-manager detection is by **lockfile** (walks up: `bun.lock`/`bun.lockb`,\n then `yarn.lock`). Only **bun** runs a real audit (`bun audit --json` +\n `bun outdated`, normalised into one `SecurityAuditReport` with per-severity /\n per-update-level counts). Other PMs are detected but **stubbed** with a `note`\n field until their shapes are normalised — issues/updates come back empty.\n- `bun audit` exits non-zero when it *finds* advisories but still writes a valid\n report — treat any non-zero exit as data, not failure.\n\n## Console integration (@pikku/addon-console)\n\nThree RPCs, all reading/writing the same artifact via the meta service. Shared\nspawn/read helpers live in `lib/audit-exec.ts` (`readAuditReport`,\n`runPikkuAudit`, `spawnProcess`, `findBin`) — reuse them, don't re-implement.\nLike every console RPC these require an **authenticated session** (the console\nis admin-only), so the host must have Better Auth wired — see `pikku-better-auth`.\n\n- `getSecurityAudit` — reads `.pikku/audit.json`, returns the report (or `null`).\n- `runSecurityAudit` — runs `pikku audit --outdated` server-side (regenerates the\n artifact) then returns the fresh report. Same shape as the Run Tests action.\n- `updateDependency({ package, version })` — bumps the package in `package.json`\n (preserving the `^`/`~` range prefix), runs `bun install`, re-audits, and\n returns the fresh report. Throws if the package is not a direct dependency.\n NOTE: `bun install` must be scoped to a standalone project — do not run it\n inside a yarn/bun monorepo member (it resolves the whole workspace).\n\n## Console UI (@pikku/console)\n\n- `SecurityPage` — the page: **Run audit** button (`lead`) + responsive\n `ShellHeader` (structured `search` + `selection` for the Issues/Dependencies\n lens; never cram raw controls into the non-collapsing `filters`/`view` escape\n hatch). Empty state until an audit has run.\n- `SecurityAuditView` — exported presentational component. Two lenses\n (Issues grouped by severity; Dependencies table). Each finding row carries its\n actions **right-aligned in the row header** (`Accordion.Control` sibling, so a\n click acts instead of toggling): \"View advisory\" + a per-finding\n **remediation slot**.\n- `renderRemediation({ pkg, version, issue })` — the extension seam. OSS default\n is `UpdateDependencyButton` (the free bump + `bun install`). Downstream\n consoles (Fabric) pass their own sandbox-verified action here — replace the\n action, keep the view.\n- Hooks: `useSecurityAudit` (read), `useRunSecurityAudit` (run),\n `useUpdateDependency` (bump). All are `useMutation`/`useQuery` — surface\n `mutation.error`, never hand-roll loading/error state or swallow the error.\n\n## Report shape (SecurityAuditReport)\n\n```ts\n{\n tool: string // e.g. 'bun'\n note?: string // set when the audit could NOT run (unsupported PM);\n // render ONLY the note — never a reassuring \"no vulnerabilities\"\n summary: { critical, high, moderate, low, info: number }\n issues: SecurityAuditIssue[] // package, severity, title, advisoryId, cwe[], cvssScore?,\n // url?, vulnerableVersions, recommendedVersion?\n updates: SecurityAuditUpdate[] // package, current, latest, level (major|minor|patch|unknown)\n}\n```\n\nWhen `note` is present the audit did not run — show only the note (an \"Audit not\nrun\" state), never the \"no known vulnerabilities / up to date\" copy.\n", "pikku-emails/SKILL.md": "---\nname: pikku-emails\ndescription: >-\n Use when working with Pikku's file-based email templates: authoring HTML/subject/text templates,\n locales, partials and theme, running `pikku emails generate`, and rendering/sending them through\n an EmailService. TRIGGER when: code uses renderEmailTemplate, EmailTemplateName, EmailService,\n SendTemplateEmailInput, LocalEmailService, or imports from .pikku/email/pikku-emails.gen.\n TRIGGER when: the project has an emails/ directory (templates/, locales/, partials/, theme.json)\n or emailTemplatesDir in pikku.config.json. TRIGGER when: user asks to add/edit a transactional\n email (verification, password reset, invitation, receipt), wire email sending, or translate an\n email. DO NOT TRIGGER when: user asks about i18n for the app UI (use pikku-i18n) or auth flows\n in general (use pikku-better-auth).\ninstallGroups: [core]\n---\n\n# Pikku Emails\n\nPikku compiles a directory of plain template files into a typed, dependency-free\nrenderer. `pikku emails generate` reads `emailTemplatesDir` and writes\n`.pikku/email/pikku-emails.gen.ts` (the `renderEmailTemplate` function + per-template\ntypes) and `pikku-emails-meta.gen.json`. Templates are authored as files; the\ngenerated output is never edited by hand.\n\n## Agent Operating Procedure\n\n1. Edit source files under `emailTemplatesDir` only. Never edit `.pikku/email/*`.\n2. After any change run `pikku emails generate` (it is also part of `prebuild`, usually\n `pikku bootstrap; pikku all; pikku emails generate`).\n3. Validate by importing `renderEmailTemplate` and rendering with sample data, or run the\n project's typecheck — the generated `data` type will flag missing/wrong variables.\n4. Fix the source cause; do not patch generated files or update hashes by hand.\n\n## Config\n\n```jsonc\n// pikku.config.json\n{\n \"emailTemplatesDir\": \"emails\", // relative to rootDir; omit to disable emails\n \"outDir\": \".pikku\" // gen lands in <outDir>/email/\n}\n```\n\nIf `emailTemplatesDir` is unset the command is a no-op.\n\n## Directory layout\n\n```text\nemails/\n theme.json # brand tokens: appName, fonts, colors\n locales/\n en.json # translation strings, nested namespaces\n de.json # one file per locale (filename = locale key)\n partials/\n layout.html # outer wrapper; must include {{content}}\n footer.html # reusable fragment, included with {{> footer}}\n templates/\n verify-email.html # body (required)\n verify-email.subject.txt # subject line (required)\n verify-email.text.txt # plain-text alternative (optional)\n```\n\nA template's **name** is its filename without the `.html` / `.subject.txt` / `.text.txt`\nsuffix (`verify-email` above). `html` and `subject` are required; `text` is optional and,\nwhen present, becomes the plain-text MIME part.\n\n## Templating syntax\n\nPlaceholders are `{{ ... }}`. Resolution order inside a template:\n\n- `{{appName}}` — from `data.appName`, falling back to `theme.appName`.\n- `{{theme.colors.accent}}`, `{{theme.fonts.body}}` — values from `theme.json`.\n- `{{t.verifyEmail.heading}}` — string from the active locale file (`locales/<locale>.json`).\n- `{{verifyUrl}}` — any other key is a **runtime variable**, supplied via `data`.\n- `{{> footer}}` — include a partial from `partials/`.\n- `{{content}}` / `{{subject}}` — only meaningful inside `partials/layout.html`\n (the rendered body and subject). `layout.html` wraps every template if present.\n\nLocale strings may themselves contain variables and partial-free placeholders, e.g.\n`\"subject\": \"{{inviterName}} invited you to join {{organizationName}}\"`. These are\nresolved in the same pass, so a subject of `{{t.invitation.subject}}` expands fully.\n\n## Typed variables (per template)\n\nThe generator extracts the runtime variables each template references and emits a typed\n`data` shape. Extraction is **scoped to the template**: it walks the template's\nhtml/subject/text, the partials it includes, and only the locale keys it actually\nreferences (transitively) — variables from unrelated locale entries do not leak in.\n\n```ts\nimport {\n renderEmailTemplate,\n type EmailTemplateName,\n type EmailTemplateVariables,\n} from './.pikku/email/pikku-emails.gen.js'\n\n// EmailTemplateVariables<'organization-invitation'> =\n// { appName?: ...; inviteUrl?: ...; inviterName?: ...; organizationName?: ... }\n```\n\nTo make a variable required-and-typed, reference it directly in the template body (not\nonly in a locale string), so it shows up as that template's variable.\n\n## Rendering\n\n```ts\nconst rendered = renderEmailTemplate({\n name: 'verify-email', // EmailTemplateName (autocompleted)\n locale: 'en', // optional, defaults to 'en'\n data: { verifyUrl: url }, // EmailTemplateVariables<'verify-email'>\n})\n// rendered: { name, locale, subject, html, text?, variables, hash }\n```\n\n`hash` is a stable content hash (useful as an idempotency / dedupe key on outgoing mail).\n\n## Sending through an EmailService\n\n`@pikku/core/services` defines `EmailService.send(input)` where `input` is one of\n`SendTextEmailInput`, `SendHTMLEmailInput`, or `SendTemplateEmailInput`:\n\n```ts\nimport type { EmailService } from '@pikku/core/services'\n\nawait email.send({\n to: user.email,\n template: { name: 'verify-email', locale: user.locale, data: { verifyUrl } },\n})\n```\n\n`LocalEmailService` (dev/test) captures the payload as-is. To actually render templates\nbefore sending, wrap a delegate service: when `input.template` is present, call\n`renderEmailTemplate` and forward `subject` / `html` / `text` to the delegate (e.g. a\nResend/SES/SMTP service). This wrapper is project-owned because `renderEmailTemplate`\nis generated per project; wire it in `services.ts` and inject it into functions.\n\n```ts\nasync send(input: SendEmailInput) {\n if (!('template' in input) || !input.template) return this.delegate.send(input)\n const r = renderEmailTemplate(input.template as RenderEmailInput<EmailTemplateName>)\n return this.delegate.send({\n to: input.to, from: input.from, subject: r.subject, html: r.html,\n ...(r.text ? { text: r.text } : {}),\n })\n}\n```\n\n## Generated artifacts\n\n- `.pikku/email/pikku-emails.gen.ts` — `renderEmailTemplate`, `EmailTemplateName`,\n `EmailLocale`, `EmailTemplateVariables<T>`, inlined templates/locales/partials/theme.\n- `.pikku/email/pikku-emails-meta.gen.json` — per-template `variables`, `hasHtml/Subject/Text`,\n and per-locale content hashes. Both are regenerated; keep them out of hand edits and\n (typically) git-ignored.\n\n## Gotchas\n\n- New template not appearing → you added `.html` but forgot `.subject.txt` (subject is\n required), or didn't rerun `pikku emails generate`.\n- Variable typed `unknown`/missing → it's only in a locale string for a different template;\n reference it in this template to scope it in.\n- Editing a locale string changes that template's content hash — expected; the hash covers\n the strings the template uses.\n- `layout.html` must contain `{{content}}` or the body is dropped.\n", "pikku-fabric/SKILL.md": "---\nname: pikku-fabric\ndescription: 'Build and convert apps for the Pikku Fabric platform. Covers SQLite/libSQL database setup with Kysely, fabric project layout, deploy provider config, `fabric.config.json`, and the pikku-verify workflow. TRIGGER when: user is working on a Fabric-hosted Pikku project, converting an app to Fabric format, or asking about Fabric deployment, database, or project conventions. DO NOT TRIGGER when: user is working on a generic (non-Fabric) Pikku deployment — use pikku-deploy-cloudflare, pikku-deploy-fastify, etc. instead.'\ninstallGroups: [fabric]\n---\n\n# Pikku Fabric\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. **Run structural validation first.** Before any edit, run:\n ```bash\n pikku fabric validate --json\n ```\n This prints every missing file, misconfigured field, and dependency gap with a `fixHint`. Address all `error` findings before proceeding — they block deploy. Resolve `warn` findings before testing — they cause runtime failures. `info` findings are best-practice gaps that are safe to defer.\n2. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n3. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n4. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n5. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n6. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nFabric is a serverless deployment platform for Pikku apps. Every Fabric app runs on Cloudflare Workers with a SQLite database (via libSQL/Turso). This skill covers what's unique to Fabric. For general Pikku concepts, function authoring, HTTP wiring, and more, see `pikku-concepts`, `pikku-http`, `pikku-services`, etc.\n\n## Before you start\n\nAlways run project discovery first:\n\n```bash\nyarn pikku meta context --json\n```\n\nCall the `pikku-meta` tool before grepping or editing a Fabric app.\n\n- Use `section: \"context\"` for the project map: functions, wires, workflows, capabilities, and source files.\n- Use `section: \"clients\"` before frontend/RPC work.\n- Use `section: \"functions\"` to list function ids, then `section: \"function\", id: \"<functionId>\"` for one function.\n- Use `section: \"schemas\"` to list schema names. Only request full JSON Schema bodies with `schemas: [\"SchemaName\"]` for the specific schemas needed.\n\nDo not load every schema body by default; that wastes context and usually makes the model worse.\n\nFor database work:\n\n- Use `pikku-db` for the actual attached Fabric database state: tables, columns, foreign keys, and applied migrations.\n- Use `pikku-meta` `section: \"schemas\"` for code-level JSON Schema contracts, not database introspection.\n- Do not inspect database credentials or connect to the database directly; Fabric Control already exposes the safe introspection surface.\n\n## Database: SQLite via libSQL\n\nFabric apps use SQLite, accessed via Kysely with the libSQL HTTP adapter. NOT PostgreSQL, NOT D1.\n\n### Setup in `services.ts`\n\n```typescript\nimport { Kysely, CamelCasePlugin } from 'kysely'\nimport { LibsqlWebDialect } from '@pikku/kysely-sqlite'\nimport type { DB } from '#pikku/db/schema.gen.js'\n\nconst databaseUrl = await variables.get('DATABASE_URL')\nlet kysely: Kysely<DB>\nif (databaseUrl) {\n kysely = new Kysely<DB>({\n dialect: new LibsqlWebDialect({ url: databaseUrl }),\n plugins: [new CamelCasePlugin()],\n })\n} else if (existingServices?.kysely) {\n kysely = existingServices.kysely as Kysely<DB>\n} else {\n throw new Error('kysely not provided and DATABASE_URL is unset')\n}\n```\n\nFabric injects `DATABASE_URL` as a variable binding when the stage starts. In local dev, `pikku db migrate` uses a local `dev.db` SQLite file.\n\n### Migrations\n\nMigrations are plain `.sql` files at the **project root**, in a directory named\nfor the engine — `db/sqlite/` for SQLite/libSQL stages, `db/postgres/` for\nPostgres ones. Never `db/migrations/`, and never under `packages/functions/`:\nthe deploy pipeline stages `db/<engine>/*.sql` from the root and applies them\nafter upload, so a migration anywhere else is silently never run.\n\n```\ndb/sqlite/\n 0001-init.sql\n 0002-add-users.sql\n```\n\nNumbers must be consecutive and gap-free, and an applied migration is frozen —\ncorrect a mistake with a new forward migration, never by editing or renaming one\nthat has already run (the recorded hash will no longer match).\n\nRun migrations: `pikku db migrate`. It also regenerates `.pikku/db/schema.gen.ts`\n(Kysely types) and `.pikku/db/zod.gen.ts` — there is no separate types step.\n\n**NEVER hand-edit the generated schema** — write a migration and re-run.\n\nA Better Auth app has a second constraint: the plugins you enable (`admin()`,\n`actor()`, …) each declare columns, and `pikku db migrate` refuses to run while\nthe applied schema is missing any of them. `pikku db generate` writes the\nmigration that closes the gap.\n\n### Column conventions\n\n- Use `SERIAL`/`INTEGER PRIMARY KEY AUTOINCREMENT` for IDs\n- Use `TEXT` for strings, `INTEGER` for booleans (0/1) and timestamps (Unix ms)\n- Use `CHECK` constraints sparingly — prefer app-level validation\n- Table and column names: snake_case in SQL, camelCase in TypeScript (via `CamelCasePlugin`)\n\n## Deploy Provider\n\n`pikku.config.json` (in the project root, not `packages/functions/`) **must** declare the Fabric deploy provider:\n\n```json\n{\n \"deploy\": {\n \"providers\": {\n \"cloudflare\": \"@pikkufabric/deploy-cloudflare\"\n }\n }\n}\n```\n\nWithout this, `pikku deploy plan --provider cloudflare` uses the OSS adapter which lacks Fabric's workflow service wiring.\n\nThe Fabric adapter automatically:\n\n- Injects `SQLiteKyselyWorkflowService` when `DATABASE_URL` is bound\n- Sets up the libSQL workflow queue\n- Wires `workflowQueues: true` for the scaffold\n\nNo manual workflow service setup is needed.\n\n## Project Layout\n\n```\npackages/functions/\n src/\n functions/ # Business logic — one pikkuFunc/workflow per file\n wirings/ # Transport bindings\n *.http.ts # wireHTTP / defineHTTPRoutes / wireHTTPRoutes\n *.channel.ts # wireChannel\n *.queue.ts # wireQueueWorker\n *.schedule.ts # wireScheduler\n *.mcp.ts # wireMCPTool\n *.cli.ts # wireCLI\n services.ts # pikkuServices factory (singleton)\n middleware.ts # Shared middleware\n permissions.ts # Shared permissions\n .pikku/\n db/schema.gen.ts # Kysely types, written by `pikku db migrate` — NEVER hand-edit\napps/app/ # Frontend(s)\ndb/sqlite/ # Plain .sql migrations, numbered, gap-free (project root)\npikku.config.json # Pikku + deploy config (project root)\npikkufabric.config.json # Fabric project link + frontends (project root)\n```\n\n## `pikkufabric.config.json`\n\nLinks the repo to a Fabric project and declares its frontends:\n\n```json\n{\n \"projectId\": \"my-project-id\",\n \"production\": {\n \"domain\": \"example.com\"\n },\n \"frontends\": {\n \"app\": {\n \"cwd\": \"apps/app\",\n \"primary\": true,\n \"deploy\": true,\n \"kind\": \"ssr\",\n \"dev\": {\n \"command\": [\"yarn\", \"dev\"],\n \"port\": 7105,\n \"healthPath\": \"/\"\n }\n }\n }\n}\n```\n\n- `projectId`: written by `pikku fabric init` / `link`. Templates ship the\n `__PROJECT_ID__` placeholder — that is *not* a link, and the CLI treats it as\n unlinked.\n- `production.domain`: optional custom domain. Production always maps to `main`;\n without a domain it lives on the platform `*.pikkufabric.app` hostnames.\n- `frontends`: each entry declares a frontend app with its dev command and port\n\n## RPC is the default transport\n\nIn Fabric apps, most features don't need HTTP wirings. Just write the function with `expose: true` — Pikku generates an RPC client and React Query hooks automatically.\n\n```typescript\nexport const listTasks = pikkuSessionlessFunc({\n expose: true,\n readonly: true,\n func: async ({ kysely }, {}) => {\n return { tasks: await kysely.selectFrom('tasks').selectAll().execute() }\n },\n})\n```\n\nAdd `wireHTTP` only when you need a specific REST shape (webhooks, third-party callers).\n\n### Transport rule\n\n- Always use RPC first.\n- If the function should be callable from the app or other generated clients, prefer `expose: true`.\n- Use `expose: true` for public/generated client access unless the user explicitly wants a private function.\n- Do not add HTTP routes unless the user explicitly asks for HTTP/REST, or the project settings explicitly require HTTP transport.\n- Every new or changed function must have a real description.\n- If function metadata would show `missing description`, the work is not finished yet.\n\n## Run it locally\n\nA Fabric app is two processes: the pikku API server (`:3000`) and the frontend\n(vite). The starter template's `bun run dev` starts **both** and takes the whole\nsession down if either dies — a frontend running against a dead API looks like an\napp bug and is the single most common way to waste an hour here.\n\n```bash\nbun run prebuild # pikku all — codegen must be current before the server boots\nbun run dev\n```\n\nThen open the app, sign up as a real user, and click through what you built.\n**HTTP 200 is not evidence.** These are client-rendered pages: the server returns\n200 with an empty shell, so a page whose component throws still looks fine to\ncurl. Either open it in a browser or drive it headlessly and assert on rendered\ntext.\n\nSecrets come from `process.env`, which the CLI populates from a `.env` in the\nworking directory. `BETTER_AUTH_SECRET` is required — without it the first\nsign-up fails with `Requested secret not found`, which names no key and points at\nno file. The starter template generates one on first `bun run dev`.\n\nIf you are running the two processes yourself rather than through the template's\nscript, run `pikku dev` from the **project root** (it resolves `srcDirectories`\nrelative to the config, so a nested cwd yields a doubled watch path and no hot\nreload).\n\n## Deploy\n\n```bash\npikku fabric login # opens a browser; needs a human, wait for it\npikku fabric init https://github.com/<owner>/<repo>\npikku fabric validate # must pass clean\npikku fabric deploy plan --production\npikku fabric deploy apply --production --auto-apply\n```\n\n`apply` confirms before deploying, and with no TTY to ask — CI, an agent shell —\nit refuses rather than hangs. `--auto-apply` supplies that confirmation; drop it\nonly when a human is at a real terminal.\n\n`init` adopts a **GitHub** repo, and adoption goes through the Pikku Fabric\nGitHub App — the app has to be installed on the account or org that owns the\nrepo, and if it is installed with \"selected repositories\" this one must be in\nthe selection. There is no CLI flag that works around a missing installation:\n`init` returns \"Connect the GitHub account '<owner>'\". Send the user to install\nit, or create the project in the console instead (which provisions a Fabric-hosted\ngit repo you push to) and write the returned `projectId` into\n`pikkufabric.config.json` yourself.\n\nDeploy refuses to run unless the target branch equals its upstream — the guard\ncompares `main` against `main@{upstream}`. So the remote you pushed to must be\nthe one the branch tracks; a stale `origin` left over from scaffolding blocks\nthe deploy with \"local HEAD … ≠ remote …\" even though your code is pushed.\n`git branch --set-upstream-to=<remote>/main main` before deploying.\n\n## Versioning\n\nFunctions with `expose: true` are versioned via `versions.pikku.json`. When you change a function's input or output schema, you must bump its version number — otherwise `pikku all` will report a breaking change and callers' generated clients become stale.\n\nThe `pikku-verify` tool catches this automatically.\n\n## After every code change\n\nAlways call the `pikku-verify` tool after modifying functions, wirings, or schemas. It runs:\n\n1. `pikku all` — regenerates all codegen, checks version compliance\n2. `tsc --noEmit` — validates TypeScript types\n\nThe output card shows whether any breaking changes were detected.\n\n## Hard rules\n\nThese apply in every Fabric app:\n\n- **No `process.env`** — use `variables.get('NAME')` and `secrets.getSecret('NAME')`. Declare with `wireVariable` / `wireSecret`.\n- **No `as any`** — fix types properly.\n- **No generic `Error`** — throw `NotFoundError`, `ConflictError`, `BadRequestError`, `UnauthorizedError` from `@pikku/core/errors`.\n- **No auth checks in function bodies** — use `permissions:` field on the function config with a `pikkuPermission` factory.\n- **No hand-editing `.pikku/db/schema.gen.ts`** — write a migration and re-run `pikku db migrate`.\n- **One runtime unit per file** — never define multiple functions/workflows in a single source file.\n- **Workflow steps don't need manual wiring** — `pikkuSessionlessFunc` step functions in `*.steps.ts` files are auto-discovered by codegen.\n\n## Converting an existing app to Fabric format\n\nStart by running the structural validator — it tells you exactly what is missing:\n\n```bash\npikku fabric validate --json\n```\n\nFix every `error` and `warn` in the output before continuing. Then:\n\n1. **Replace the database layer**: swap PostgreSQL/MySQL queries for Kysely + libSQL. Convert schema to SQLite-compatible SQL migrations in `db/sqlite/`.\n2. **Replace route handlers with pikkuFuncs**: extract business logic into `pikkuFunc`/`pikkuSessionlessFunc`, add `wireHTTP` or `expose: true` for transport.\n3. **Replace DI/IoC with pikkuServices**: move service construction to `createSingletonServices` in `services.ts`.\n4. **Replace `process.env` calls** with `wireVariable`/`wireSecret` + `variables.get()`.\n5. **Add `pikku.config.json`** at project root with `srcDirectories`, `outDir`, and `clientFiles`.\n6. **Add `fabric.config.json`** at project root with `projectId`, `production.branch`, and `frontends`.\n7. **Run `pikku all`** — verify codegen succeeds and there are no type errors.\n8. **Run `pikku fabric validate`** once more to confirm no structural issues remain.\n", "pikku-fabric-debug/SKILL.md": "---\nname: pikku-fabric-debug\ndescription: 'Debug a deployed Fabric stage from the CLI — read logs, find recent errors, follow a single request end-to-end by traceId, and check request/error/latency metrics. TRIGGER when: a deployed Fabric app is erroring, timing out, or behaving differently than local; the user asks \"why is prod failing\", \"check the logs\", \"what happened to this request\"; or a deploy succeeded but the app misbehaves. DO NOT TRIGGER when: the failure reproduces locally (debug it locally), the deploy itself failed (use pikku-fabric — that is a build/config problem, not a runtime one), or the project is not deployed to Fabric.'\ninstallGroups: [fabric]\n---\n\n# Debugging a deployed Fabric stage\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Reproduce locally first. If it fails locally too, debug it there — the\n deployed stage adds cost and latency to every iteration.\n2. Start from `errors`, not `logs`. Errors are already filtered and carry the\n traceId that unlocks the rest.\n3. Follow one trace end-to-end before forming a theory. A single failing request\n tells you more than a hundred unrelated log lines.\n4. Fix the source cause and redeploy. Never leave the diagnosis at \"it is flaky\".\n5. Confirm the fix against the same stage — recheck `errors` for the function.\n\nEvery command below requires a logged-in CLI and a linked project. Both fail\nwith the exact remediation if not:\n\n```\nNot logged in. Run `pikku fabric login` first.\nNo fabric project linked. Run `pikku fabric link` first.\n```\n\n## The loop\n\n**1 — What is broken?**\n\n```bash\npikku fabric errors -b main # branch defaults to main\npikku fabric errors -b main --function createOrder\n```\n\nPrints a `WHEN | FUNCTION | TRACE | MESSAGE` table. The message is **truncated\nto 100 characters** — treat it as a label, not the full error. The TRACE column\nis the input to the next step.\n\n**2 — What happened in that one request?**\n\n```bash\npikku fabric trace <traceId> -b main\npikku fabric trace <traceId> -b main --json\n```\n\n`--branch` is **required** here (no default). Each event prints as:\n\n```\n<timestamp> <scriptName> <wireType>:<wireId> <duration>ms — <error|message|outcome>\n```\n\nThis is the whole request across the stage — every unit it touched, in order,\nwith per-event durations. The last event before the failure is where to look.\n\n**3 — Is it one request or the whole stage?**\n\n```bash\npikku fabric metrics -b main # last 24h\npikku fabric metrics -b main --hours 2 --function createOrder\n```\n\nRows are `reqs= err= (rate%) avg= min= max=` per bucket. A single bad request\nwith a healthy error rate is a data problem; a climbing error rate is a\ndeployment or dependency problem. `--json` additionally returns a `wireTypes`\nbreakdown (requests per http/queue/scheduler/…) that the table output omits.\n\n**4 — Wider context around the failure**\n\n```bash\npikku fabric logs -b main\npikku fabric logs -b main --level warn\npikku fabric logs -b main -f # follow\n```\n\n`--branch` is **required** — `logs` throws `Specify --branch <branch-name>.`\nwithout it, even though the flag reads as optional.\n\n**5 — Is the running code the code you think it is?**\n\n```bash\npikku fabric status # active + in-flight deployment, per stage, with gitSha\n```\n\nCheck this *before* deep-diving. A stage still serving an older `gitSha`, or a\ndeploy stuck in flight, explains a whole class of \"my fix did nothing\".\n\n## Known gaps — do not misread these as bugs in your app\n\n- **`pikku fabric logs --since` and `--deployment` are accepted and ignored.**\n They are declared as options but the command never reads them, so\n `--since 15m` silently returns the same default window as no flag at all. Do\n not conclude \"nothing happened in the last 15 minutes\" from it. Narrow by\n `--level`, or by `--function` via `errors`, instead.\n- **`--follow` is a 2-second client-side poll, not a server stream.** It\n dedups against what it already printed, so it behaves like `tail -f`, but new\n entries can appear up to ~2s late and it holds the process open until killed.\n\n## What NOT to do\n\n- **Do not SSH anywhere or query the telemetry backend directly.** These\n commands are the supported surface; anything lower-level is Fabric-internal\n and will not exist for your project.\n- **Do not debug by redeploying with added `console.log`s.** Get the traceId,\n read the trace. A deploy cycle per hypothesis is the slow path.\n- **Do not read the truncated `errors` message as the full error.** Always\n confirm against `trace` before changing code.\n- **Do not treat an empty `errors` table as \"the app is fine\"** — a request that\n returns a wrong 200 logs nothing. Check `metrics` for the outcome mix.\n", "pikku-feature/SKILL.md": "---\nname: pikku-feature\ndescription: 'Drive create-a-feature work for a Pikku project: discover project context, work on a feature branch, implement + verify + commit, and ask the user to review via the diff. TRIGGER when: the user asks to \"create a feature\", \"build a todo app\", \"add X to my Pikku project\", \"wire up a new endpoint\", or anything that implies turning a natural-language request into Pikku functions/wirings/migrations. DO NOT TRIGGER when: the user asks for a one-off code edit in an existing function, or asks about Pikku concepts (use pikku-concepts).'\ninstallGroups: [core]\nallowed-tools: Bash(yarn pikku meta *), Bash(yarn pikku all *), Bash(yarn tsc), Bash(git status *), Bash(git diff *), Bash(git switch *), Bash(git checkout *), Bash(git checkout -b *), Bash(git add *), Bash(git commit *), Bash(git log *), Bash(git branch *)\nargument-hint: '<feature description>'\n---\n\n# Pikku Create-a-Feature\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nEnd-to-end flow: **discover → state intent → branch → implement → verify → commit → hand to reviewer**.\n\nThere is **no plan JSON**. The branch + diff IS the contract. The reviewer\nsees real, compiled, working code. Apply = merge. Reject = `git branch -D`.\n\n## Stage 1 — Discover\n\nRun **once** at the start of every feature request:\n\n```bash\nyarn pikku meta context --json\n```\n\nThis single call returns functions, wires, middleware, permissions, workflows,\n`capabilities` (which wire types are in use), and `layout` (where new files\nshould land).\n\nOnly fall back to targeted commands when you need full input/output JSON\nschemas (`yarn pikku meta functions get <id>`) or workflow steps\n(`yarn pikku meta workflows get <id>`).\n\n**Capability rule:** do not introduce new wires of a type whose\n`capabilities.<type>` is `false` unless the user explicitly asked for it.\n\n## Stage 2 — State intent in plain English (BEFORE writing code)\n\nBefore touching any files, give the user one paragraph stating exactly what\nyou'll do. This is the lightweight \"plan\" — it is chat, not JSON.\n\n> I'll add a `todos` table via a new migration in `sql/`, and two\n> `pikkuSessionlessFunc`s (`createTodo`, `listTodos` with\n> `readonly: true`) in `packages/functions/src/functions/`. Both\n> `expose: true`, so they'll be reachable via the auto-generated RPC\n> client and React Query hooks — no HTTP wiring needed. No new\n> dependencies. OK to proceed?\n\nWait for the user to confirm or redirect. They can ask for changes (\"use the\nexisting tasks table\" / \"make it a queue not http\") in normal chat — no\nschema, no JSON, no ceremony.\n\n**Non-interactive runs (auto mode, CI, batch jobs):** state intent in one\nparagraph and proceed without waiting. Surface course corrections promptly\nin the post-implementation report.\n\n## Stage 3 — Branch off\n\nAfter confirmation, ensure the working tree is clean and create a feature\nbranch off the current default branch (whatever `git branch --show-current`\nreturns at the start — `main`, `master`, `develop`, all fine):\n\n```bash\ngit status\ngit switch -c feature/<short-slug>\n```\n\nIf the working tree is dirty, **stop and ask** — never stash silently or\noverwrite uncommitted work.\n\n## Stage 4 — Implement\n\nWrite the code as a normal human contributor would. Use the project's\nexisting conventions (look at neighbour files in `srcDirectories[0]/functions/`\nand `.../wirings/` for style).\n\n### RPC is the default transport\n\n**Just write the function with `expose: true`** — that's enough to make it\ncallable. Pikku auto-generates an RPC client (and React Query hooks if the\nproject's `clientFiles.reactQueryFile` is set) from every exposed function.\nYou do **not** need an HTTP wiring for callers to reach the function.\n\nDefault flow for a feature:\n\n1. Write the function file with `expose: true` (and `readonly: true` for\n reads).\n2. Run `pikku all` — RPC map, fetch client, and React Query hooks are\n regenerated. Frontends call `useListTodos()` / `mutation.mutate(...)`\n without you wiring anything.\n\nAdd an HTTP wiring **only when** the feature genuinely needs a specific\nREST shape (third-party callers, webhooks, REST-conventional URLs). Most\nin-app features don't.\n\n### Hard rules that always apply\n\n- **`expose: true`** for any function called from a frontend or another\n service. Without it the RPC client won't generate hooks for it.\n- **`readonly: true` for queries.** Mark read functions as `readonly: true`\n on the function config. The runner uses this to enforce read-only sessions\n (a write func called under a readonly session is rejected). The RPC layer\n also uses it to pick `useQuery` (cacheable) vs `useMutation` for client\n hooks. Mutations leave `readonly` unset (or `false`).\n- **`kind` ⇔ `auth` coupling for HTTP wirings (when you have one).** If the\n function is `pikkuFunc` (session-aware), the HTTP wiring needs\n `auth: true`. `pikkuSessionlessFunc` ⇒ `auth: false`. Mismatching is a\n hard error (PKU573).\n- **HTTP method by intent (when you wire HTTP).** Reads → `GET`. Writes →\n `POST`/`PUT`/`PATCH`/`DELETE` per REST conventions.\n- **Workflows.** Prefer `pikkuWorkflowGraph` (DSL) over\n `pikkuWorkflowComplexFunc`. `mode: 'inline'` is sync; `'distributed'` is\n queue-dispatched.\n- **Auth checks belong on the function or wiring**, not in function bodies.\n Use the `permissions` field with a `pikkuPermission` factory.\n- **Throw typed errors** from `@pikku/core/errors` — `NotFoundError`,\n `ConflictError`, `BadRequestError`. Never bare `Error`.\n- **Migrations are inline SQL files** in the project's migrations dir\n (typically `sql/`). Use a numbered prefix matching existing files.\n- **Secrets and env-vars: NEVER `process.env`.** Declare them with\n `wireSecret` (sensitive) or `wireVariable` (non-sensitive) — both with a\n zod schema for type-safe access. Read with\n `services.secrets.getSecret('NAME')` or `services.variables.get('NAME')`.\n See the **pikku-config** skill for the full pattern (including\n OAuth2 credentials). This applies even in `config.ts` and singleton\n service factories.\n\n### Conventions to copy from neighbours\n\nSome patterns vary by project; **read a neighbour file before writing**:\n\n- **Function shape**: zod schemas as exported `const`s (`CreateTodoInput`,\n `CreateTodoOutput`) passed to `input`/`output` on the func config — vs\n generic-typed config. Schema name **must match codegen expectations** (the\n exported const name = the schema name in generated `.gen.json`).\n- **Imports**: usually `'#pikku'` for `pikkuFunc` / `pikkuSessionlessFunc`\n etc. Copy what neighbours do.\n- **Service usage**: e.g. `kysely`, `redis`. Look at how an existing function\n destructures services from its first arg. **Check `application-types.d.ts`**\n to see whether services like `kysely` are typed (`Kysely<DB>`) or untyped\n (`Kysely<any>`) — that drives whether you can lean on generated DB types\n or have to coerce manually.\n- **DB schema namespace**: many projects put tables under a `CREATE SCHEMA`\n (e.g. `app.todos`). Read the first migration in `sql/` to see the\n convention; reuse helper functions/triggers (e.g. `update_last_updated_at`)\n rather than redefining them.\n- **HTTP wiring style** (only relevant if you're adding one). Two common\n shapes — match what the project already uses:\n - Per-route `wireHTTP({ method, route, func, auth })`.\n - Single map: `const routes = defineHTTPRoutes({ auth: false, routes: {\nfooName: { method: 'post', route: '/foo', func: fooFunc } }}); wireHTTPRoutes(routes)`.\n\nFor shared wiring files (e.g. `todos.http.ts` holding both create and list):\ncreate the file with imports if it doesn't exist; **append** wire calls and\nadd missing imports if it does.\n\n## Stage 5 — Verify\n\nBoth must complete cleanly **for your changes** before committing:\n\n```bash\nyarn pikku all\n# Type-check the workspaces you touched:\ncd packages/functions && npx tsc --noEmit\n```\n\nNotes on running `tsc`:\n\n- A root-level `yarn tsc` may be a no-op in monorepos that don't define a\n `tsc` script in each workspace. Don't trust an exit-zero from the root if\n no actual checking happened — verify by running `npx tsc --noEmit` in the\n package(s) you touched.\n\n### What \"fails\" means\n\n**Trust the exit code, not the stderr noise.** `yarn pikku all` may print\nwarnings, `[PKUxxx]` messages, even `level: critical` log lines, while\nstill exiting `0` — those are pre-existing project state, not your\nproblem. Same for `meta context --json`: it streams logs to stderr that\nlook scary on a clean baseline. The exit code is the source of truth.\n\nIf a command exits non-zero, that's a real failure — fix or stop.\n\n### Baseline noise — only your errors matter\n\nMany real-world projects ship with pre-existing warnings or errors\n(legacy types, version drift, gen-layer messages). Those are not your\nproblem; do not \"fix\" them.\n\nTo distinguish your errors from baseline:\n\n1. **Before implementing** (Stage 4), capture the baseline:\n ```bash\n yarn pikku all 2>&1 | tee /tmp/pikku-before.log\n ```\n2. **After implementing**, compare:\n ```bash\n yarn pikku all 2>&1 | tee /tmp/pikku-after.log\n diff /tmp/pikku-before.log /tmp/pikku-after.log\n ```\n\nA clean diff means your changes introduced no new issues — even if the\nunderlying logs both show pre-existing warnings.\n\nIf something genuinely failed because of YOUR change, fix the actual issue.\n**Do not** mask errors with `as any`, `@ts-ignore`, or `--no-verify`. If\nyou're stuck, surface the failure to the user — don't hand them a broken\nbranch.\n\n## Stage 6 — Commit\n\n```bash\ngit add -A\ngit commit -m \"feat: <short title>\"\n```\n\n## Stage 7 — Hand off\n\nTell the user the branch name and how to review. Two options:\n\n- **Local review:** open the pikku console — the changes view diffs the\n current branch against `main` with pikku-aware structure (added functions,\n new wires, migrations).\n- **PR review:** ask before pushing. Once they confirm, `git push -u origin\nfeature/<slug>` and surface the PR-create URL.\n\nDo not push without explicit confirmation. Do not merge.\n\n## Hard constraints\n\nThe skill's `allowed-tools` does **not** permit:\n\n- `yarn add` / `npm install` / dependency changes (ask the user first)\n- `yarn dbmigrate` (never run migrations against the real DB during planning)\n- `pikku deploy apply` (never deploy)\n- secret writes\n- network calls beyond what the implementation requires\n\nIf the feature genuinely needs any of these, **stop and ask** with a clear\nexplanation of why and what would change.\n\n## Output discipline\n\n- Stage 2 (intent statement) is plain English, one paragraph.\n- Between stages, give one-line updates: \"Discovered 30 functions, http+queue\n in use. Drafting intent...\" → \"Branch `feature/todos` created, implementing...\"\n → \"`pikku all` clean, `tsc` clean, committed. Review via console or run\n `git diff main`.\"\n- Don't narrate file-by-file. Only surface what's interesting (new patterns,\n judgment calls, things you suppressed).\n", "pikku-gateway-slack/SKILL.md": "---\nname: pikku-gateway-slack\ndescription: >-\n Use when integrating Slack with a Pikku app. Covers SlackGatewayAdapter, slash commands, OAuth\n flow, message handling, and signature verification. TRIGGER when: code uses SlackGatewayAdapter,\n parseSlashCommand, buildSlackInstallUrl, or user asks about Slack integration, Slack bots, or\n @pikku/gateway-slack. DO NOT TRIGGER when: user asks about general gateway/webhook patterns (use\n pikku-trigger).\n---\n\n# Pikku Gateway Slack\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/gateway-slack` provides a Slack Events API gateway adapter, slash command handling, OAuth installation flow, and message utilities.\n\n## Installation\n\n```bash\nyarn add @pikku/gateway-slack @slack/web-api\n```\n\n## API Reference\n\n### `SlackGatewayAdapter`\n\n```typescript\nimport { SlackGatewayAdapter } from '@pikku/gateway-slack'\n\nconst adapter = new SlackGatewayAdapter(options: SlackGatewayAdapterOptions)\n```\n\nBridges Slack Events API webhooks with Pikku's gateway system for processing Slack events as Pikku functions.\n\n### `SlackGatewayHelper`\n\nHelper for handling Slack messages and metadata within gateway functions.\n\n### Slash Commands\n\n```typescript\nimport { parseSlashCommand, respondToSlashCommand } from '@pikku/gateway-slack'\n\nconst command = parseSlashCommand(request)\nawait respondToSlashCommand(responseUrl, { text: 'Done!' })\n```\n\n### OAuth Flow\n\n```typescript\nimport {\n buildSlackInstallUrl,\n exchangeSlackOAuthCode,\n RECOMMENDED_BOT_SCOPES,\n} from '@pikku/gateway-slack'\n\nconst installUrl = buildSlackInstallUrl({\n clientId: config.slackClientId,\n scopes: RECOMMENDED_BOT_SCOPES,\n redirectUri: config.slackRedirectUri,\n})\n\nconst tokens = await exchangeSlackOAuthCode({\n clientId: config.slackClientId,\n clientSecret: config.slackClientSecret,\n code: oauthCode,\n redirectUri: config.slackRedirectUri,\n})\n```\n\n### Signature Verification\n\n```typescript\nimport { verifySlackSignature } from '@pikku/gateway-slack'\n\nverifySlackSignature(signingSecret, timestamp, body, signature)\n```\n\n## Usage Patterns\n\n### Slack Bot Gateway\n\n```typescript\nimport { SlackGatewayAdapter } from '@pikku/gateway-slack'\n\nconst slackGateway = new SlackGatewayAdapter({\n signingSecret: config.slackSigningSecret,\n botToken: config.slackBotToken,\n})\n\n// Register with your HTTP runner to handle /slack/events endpoint\n```\n\n### Slash Command Handler\n\n```typescript\nconst handleSlashCommand = pikkuSessionlessFunc({\n title: 'Handle Slack Command',\n func: async ({ db }, data) => {\n const command = parseSlashCommand(data)\n // Process command...\n await respondToSlashCommand(command.response_url, {\n text: `Processed: ${command.text}`,\n })\n },\n})\n```\n", "pikku-http/references/http-options.md": "# wireHTTP / defineHTTPRoutes / wireHTTPRoutes — full option reference\n\n## `wireHTTP(config)`\n\nWire a single function to an HTTP endpoint. Import from `@pikku/core/http`.\n\n| Option | Type | Notes |\n| --- | --- | --- |\n| `method` | `'get' \\| 'post' \\| 'put' \\| 'patch' \\| 'delete' \\| 'head'` | HTTP verb |\n| `route` | `string` | e.g. `/books/:bookId` — `:params` become `data` fields |\n| `func` | `PikkuFunc` | The function to call |\n| `auth?` | `boolean` | Override default auth (`true` = require session) |\n| `tags?` | `string[]` | For grouping, middleware targeting |\n| `middleware?` | `PikkuMiddleware[]` | Per-route middleware |\n| `sse?` | `boolean` | Enable Server-Sent Events |\n| `contentType?` | `'xml' \\| 'json'` | Response content type |\n| `timeout?` | `number` | Request timeout in ms |\n| `headers?` | `HTTPHeadersSchema` | Expected headers schema |\n| `docs?` | `HTTPRouteDocsConfig` | OpenAPI docs config |\n\n## `defineHTTPRoutes(config)` + `wireHTTPRoutes(config)`\n\nGroup routes with shared configuration. Groups are composable and nestable. Import from `.pikku/pikku-types.gen.js`.\n\n```typescript\nconst routes = defineHTTPRoutes({\n basePath?: string, // Prepended to all route paths\n tags?: string[], // Applied to all routes in group\n auth?: boolean, // Default auth for all routes (overridable per-route)\n middleware?: PikkuMiddleware[],\n routes: {\n [key: string]: {\n method: string,\n route: string,\n func: PikkuFunc,\n auth?: boolean, // Override group auth\n middleware?: PikkuMiddleware[],\n }\n }\n})\n\nwireHTTPRoutes({\n basePath?: string, // Top-level prefix (e.g. '/api/v1')\n middleware?: PikkuMiddleware[],\n routes: {\n [key: string]: ReturnType<typeof defineHTTPRoutes>,\n }\n})\n```\n\nConfig cascading rules:\n\n- `basePath` — concatenates down the chain\n- `tags` — merge (union)\n- `auth` — child overrides parent\n", "pikku-http/SKILL.md": "---\nname: pikku-http\ndescription: >-\n Use when adding HTTP routes, REST APIs, web endpoints, or SSE streams to a Pikku app. Covers\n wireHTTP, defineHTTPRoutes, route groups, auth, middleware, SSE, and generated\n fetch client. TRIGGER when: code uses wireHTTP/defineHTTPRoutes/wireHTTPRoutes, user asks about\n REST endpoints, API routes, SSE, or the generated fetch client. DO NOT TRIGGER when: user asks\n about WebSocket (use pikku-websocket), queue workers (use pikku-queue), or deployment (use\n pikku-deploy-*).\ninstallGroups: [core]\n---\n\n# Pikku HTTP Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions to HTTP endpoints. Supports single routes, composable route groups, auth, middleware, SSE, and auto-generated type-safe clients. (Authorization lives on the function, not the wiring — see `pikku-permissions`.)\n\n## Before You Start\n\nRun these commands to understand the current project:\n\n```bash\npikku info functions --verbose # See existing functions, their types, tags, middleware\npikku info tags --verbose # Understand project organization and naming conventions\npikku info middleware --verbose # See what middleware is already applied\n```\n\nFollow existing patterns you find (naming, tag usage, file organization). See `pikku-concepts` for the core mental model.\n\n## API Reference\n\n- `wireHTTP(config)` (from `@pikku/core/http`) — wire one function to one endpoint.\n- `defineHTTPRoutes(config)` + `wireHTTPRoutes(config)` (from `.pikku/pikku-types.gen.js`) — group routes with shared config; composable/nestable.\n\nFunction input/output types come from the function's own `input:`/`output:` zod schemas — never declared in the wiring. Route `:params`, query params, and body are merged into the function's `data` arg (see Data Flow).\n\nConfig cascading across groups: `basePath` concatenates down the chain, `tags` merge (union), `auth` child overrides parent.\n\nFor the full option tables (every `wireHTTP` field, the `defineHTTPRoutes`/`wireHTTPRoutes` config shape), read `references/http-options.md`.\n\n### `addHTTPMiddleware(pattern, middlewares)`\n\n```typescript\naddHTTPMiddleware('*', [authBearer()]) // All routes\naddHTTPMiddleware('/api/*', [rateLimit()]) // Pattern match\n```\n\n> HTTP-route-level permissions (`addHTTPPermission`, a `permissions` field on the wiring) were removed in #972. Declare authorization on the function definition (`pikkuFunc({ permissions })`, see `pikku-permissions`), or app-wide via `addGlobalPermission`. Tags/patterns are for *middleware* only now.\n\n## Data Flow\n\nPikku merges route params, query params, and request body into a single `data` object:\n\n```typescript\n// POST /books/42?format=pdf with body { title: \"New Title\" }\nwireHTTP({ method: 'post', route: '/books/:bookId', func: updateBook })\n// → updateBook receives: { bookId: \"42\", format: \"pdf\", title: \"New Title\" }\n```\n\n## Usage Patterns\n\n### Single Route\n\n```typescript\nwireHTTP({\n method: 'get',\n route: '/books/:bookId',\n func: getBook,\n})\n```\n\n### Route Groups (Recommended for CRUD)\n\n```typescript\nconst booksRoutes = defineHTTPRoutes({\n tags: ['books'],\n routes: {\n list: { method: 'get', route: '/books', func: listBooks, auth: false }, // per-route override\n get: { method: 'get', route: '/books/:bookId', func: getBook },\n create: { method: 'post', route: '/books', func: createBook },\n delete: { method: 'delete', route: '/books/:bookId', func: deleteBook },\n },\n})\n\nconst todosRoutes = defineHTTPRoutes({\n auth: false, // group-level default, overridable per-route\n tags: ['todos'],\n routes: {\n list: { method: 'get', route: '/todos', func: listTodos },\n },\n})\n\nwireHTTPRoutes({\n basePath: '/api/v1',\n middleware: [cors()],\n routes: { books: booksRoutes, todos: todosRoutes },\n})\n// Results in: GET /api/v1/books, POST /api/v1/books, GET /api/v1/todos, etc.\n```\n\n### Auth\n\n```typescript\n// Public route (no auth)\nwireHTTP({ method: 'get', route: '/books', func: listBooks, auth: false })\n\n// Authenticated route (default when a global auth middleware is set)\nwireHTTP({ method: 'delete', route: '/books/:bookId', func: deleteBook })\n```\n\nAuthorization is not a wiring concern — declare it on the function via `permissions` (see `pikku-permissions`), or app-wide via `addGlobalPermission`.\n\n### Middleware\n\n```typescript\nimport { cors, authBearer } from '@pikku/core/middleware'\n\n// Global middleware\naddHTTPMiddleware('*', [\n cors({ origin: 'https://app.example.com', credentials: true }),\n authBearer(),\n])\n\n// Scoped middleware\naddHTTPMiddleware('/api/*', [rateLimit({ maxRequests: 100, windowMs: 60_000 })])\n\n// Per-route middleware\nwireHTTP({\n method: 'delete',\n route: '/books/:bookId',\n func: deleteBook,\n middleware: [auditLog],\n})\n```\n\n### SSE (Server-Sent Events)\n\n```typescript\nwireHTTP({\n method: 'get',\n route: '/todos',\n func: getTodos,\n sse: true,\n})\n\nconst getTodos = pikkuFunc({\n title: 'Get Todos',\n func: async ({ db, channel }, {}) => {\n const todos = await db.getTodos()\n\n if (channel) {\n for (const todo of todos) {\n channel.send({ todo })\n await sleep(100)\n }\n return\n }\n\n return { todos }\n },\n})\n```\n\n### Generated Fetch Client\n\nAfter `npx pikku all`, a type-safe client is generated:\n\n```typescript\nimport { pikkuFetch } from '.pikku/pikku-fetch.gen.js'\n\npikkuFetch.setServerUrl('http://localhost:4002')\n\nconst books = await pikkuFetch.get('/api/v1/books', {})\nconst book = await pikkuFetch.get('/api/v1/books/:bookId', { bookId: '42' })\nconst created = await pikkuFetch.post('/api/v1/books', {\n title: 'The Pikku Guide',\n author: 'You',\n})\n\npikkuFetch.setAuthorizationJWT(token)\nconst deleted = await pikkuFetch.delete('/api/v1/books/:bookId', {\n bookId: created.bookId,\n})\n```\n\n## Complete Example\n\nFunctions live in their own files (one per file) and supply behavior + `permissions`; the wiring file imports them and wires routes. Sessionless funcs need no session; `pikkuFunc` does.\n\n```typescript\n// functions/books.functions.ts\nimport { pikkuFunc, pikkuSessionlessFunc } from '#pikku'\n\nexport const listBooks = pikkuSessionlessFunc({\n title: 'List Books',\n func: async ({ db }, { limit }) => ({ books: await db.listBooks(limit) }),\n})\n\nexport const getBook = pikkuFunc({\n title: 'Get Book',\n description: 'Retrieve a book by ID',\n func: async ({ db }, { bookId }) => await db.getBook(bookId),\n permissions: { user: isAuthenticated },\n})\n\n// wirings/books.http.ts — same defineHTTPRoutes/wireHTTPRoutes shape as the Route Groups example above\nimport { addHTTPMiddleware } from '@pikku/core/http'\nimport { cors, authBearer } from '@pikku/core/middleware'\n\naddHTTPMiddleware('*', [cors(), authBearer()])\n```\n", "pikku-i18n/SKILL.md": "---\nname: pikku-i18n\ndescription: 'Wire i18n into a Pikku frontend with Paraglide JS (inlang). English by default, every user-facing string is a typed message function (`m.some__key()`) compiled from `messages/<locale>.json`, and additional languages are served under `/fr` `/de` URL prefixes. TRIGGER when: scaffolding or editing a frontend and writing user-facing text, adding a second language, or asked to \"make this translatable / use tokens / add i18n\". DO NOT TRIGGER for backend functions, error messages thrown from functions, or log output.'\ninstallGroups: [core]\n---\n\n# Pikku i18n (Paraglide JS)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Every user-facing string in a frontend is a message. Never hardcode display text — add a key to `messages/en.json` and render `m.the__key()`. This holds even when the app ships only English; the messages are the seam a second language slots into later.\n2. One `messages/<locale>.json` per language at the app root (NOT under `src/`), declared in `project.inlang/settings.json`. English (`en`) is `baseLocale` and the only locale until someone adds another.\n3. Messages compile to typed ESM functions in `src/paraglide/` (generated, self-gitignored — never edit or commit it). The Vite plugin compiles during `dev`/`build` with HMR on message edits; run the CLI compile only when you need `tsc` before Vite has ever run.\n4. Validate with the app's own `tsc` then its `build`. The deploy pipeline compiles Paraglide and runs each frontend's `tsc` before building it — an i18n mistake blocks the deploy.\n\n## The moving parts (starter-template layout)\n\n- `messages/en.json` — flat keys, `{param}` interpolation, inlang message-format:\n ```json\n {\n \"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n \"auth__login__title\": \"Sign in\",\n \"auth__login__description\": \"Welcome back to {name}.\"\n }\n ```\n Key convention: lower snake_case, `__` (double underscore) between namespace segments, `_` within a segment — `auth__login__title`, `common__email_placeholder`.\n- `project.inlang/settings.json` — `baseLocale`, `locales`, the `@inlang/plugin-message-format` module, `pathPattern: \"./messages/{locale}.json\"`.\n- `vite.config.ts` — `paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide' })` from `@inlang/paraglide-js` (devDependency), FIRST in the plugins array.\n- `src/paraglide/` — compiled output (`messages.js`, `runtime.js`, per-locale `messages/*.js`). Generated; it writes its own `.gitignore`.\n- `src/i18n/config.ts` — locale plumbing, and the ONLY hand-written i18n module: `supportedLocales`/`defaultLocale` (re-exported from `../paraglide/runtime.js`), `detectLocale`, `localeDir` (RTL for ar/he/fa/ur), a reactive locale store (`overwriteGetLocale` bridged to `useSyncExternalStore`), `setActiveLocale`, `useLocale()`. This is not a wrapper over messages — Paraglide's `getLocale()` is a module global with no React reactivity, and this bridges it. Wire `overwriteGetLocale` or `m.*()` will resolve a different locale than the app thinks is active.\n- `tsconfig.json` — `\"allowJs\": true, \"checkJs\": false` so `tsc` can consume Paraglide's JSDoc-typed JS output.\n\n## Using messages in components\n\n```tsx\nimport { m } from '../paraglide/messages.js'\nimport { useLocale } from '@/i18n/config'\n\nfunction LoginPage() {\n useLocale() // subscribe: re-render m.*() when the locale switches\n return (\n <>\n <Title>{m.auth__login__title()}</Title>\n <Text>{m.auth__login__description({ name: m.app__name() })}</Text>\n </>\n )\n}\n```\n\n- Params: `{name}` in the JSON → `m.auth__login__description({ name })`. Params are typed per message.\n- Any component that renders `m.*()` calls `useLocale()` (bare call is enough); it also returns `{ locale, dir, setLocale }` for switchers.\n- Non-component helpers (formatters, status maps) call `m.some__key()` directly — the functions are plain ESM, no hook needed; the render-time subscription lives in the component that displays the result.\n- Locale switching: the root route persists to localStorage, sets `<html lang dir>` (`localeDir`), and calls `setActiveLocale` — in-SPA re-render, no page reload. Mirror `routes/__root.tsx` in the starter template.\n\n## Keys only known at runtime (enum labels, status maps)\n\nA DB value picking a label (`enums__document_status__${status}`) is the one case a\ngenerated message can't express. Paraglide's README (§ \"What about dynamic or\nCMS-driven keys?\") is explicit: use an **explicit mapping from value to message\nfunction**. Key it on the enum type, never `string`:\n\n```ts\nimport { m } from '../paraglide/messages.js'\n\nconst DOCUMENT_STATUS_LABEL: Record<DocumentStatus, () => string> = {\n completed: m.enums__document_status__completed,\n in_progress: m.enums__document_status__in_progress,\n required: m.enums__document_status__required,\n}\n\n// call site — no fallback, because there is no missing case\nDOCUMENT_STATUS_LABEL[status]()\n```\n\n`Record<DocumentStatus, …>` is exhaustive: add a value to the enum without a\nlabel and the build fails. That is the entire point.\n\nDo NOT write `Record<string, () => string>` with a `?? status` fallback, and do\nNOT index the namespace with a computed key (`m[\\`enums__${name}__${value}\\`]`).\nBoth compile, both render the raw identifier to users when a label is missing,\nand both reintroduce exactly the silent-fallback failure Paraglide exists to\neliminate. If you find yourself writing a `resolveDynamicKey(key: string)`\nhelper, stop — that helper IS the bug.\n\n## Type safety — and why deploys block on i18n\n\nA message IS a function: a typo'd or deleted key (`m.auth__login__titel()`) is a missing export — a **TypeScript error**, not a silent runtime fallback string. Params are typed too. The deploy pipeline compiles Paraglide then runs each frontend's `tsc` (`\"tsc\": \"tsc --noEmit\"` script — keep it in every frontend's `package.json`) **before** building; a type error aborts the deploy. `vite build` does not type-check on its own, so this gate is the only thing standing between a broken message and production.\n\nThe gate catches _invalid_ messages but not _inlined_ strings. The `@pikku/mantine` `I18nNode` prop typing catches those: a raw string literal fails to compile on a gated prop, because `I18nString` is a branded type a bare `string` can't satisfy. Between the two, `tsc` is the whole safety net — there is no runtime fallback to inspect, by design.\n\n## Compile step\n\n- **Dev/build:** the Vite plugin compiles automatically; editing `messages/*.json` under a running dev server recompiles + HMRs.\n- **Standalone `tsc` before Vite has run** (fresh clone, CI):\n ```sh\n npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/paraglide\n ```\n This is exactly what the deploy CI does before the per-app `tsc`.\n\n## Adding a second language\n\n1. `messages/fr.json` mirroring `en.json`'s keys (translate the values, keep `{param}` names identical).\n2. Add `\"fr\"` to `locales` in `project.inlang/settings.json`.\n3. Recompile (restart/`vite dev` or the CLI compile). A locale file missing keys falls back to the base locale per message.\n4. Content is reachable via the `/<lang>` URL prefix (`detectLocale` already resolves it); the base locale needs no prefix. Expose the switcher via `useLocale().setLocale`.\n\n## i18n debug mode (find inlined strings)\n\n`tsc` catches invalid messages, and the `@pikku/mantine` gate catches raw strings on gated props — but neither sees a hardcoded string in plain JSX, an `aria-label`, `alt`, `document.title`, or anything passed to a non-Mantine component. Debug mode covers that gap: render every message as block glyphs (`█`), and whatever is still readable never went through a message.\n\n**Build it as a generated locale, never as a runtime wrapper.** Masked text is text, and rendering different text per locale is what Paraglide already does:\n\n1. A script generates `messages/zz.json` from `en.json`, replacing `\\S` with `█` while leaving `{placeholders}` intact (they are message inputs — mangling them changes the compiled signature). Run it before `paraglide-js compile`; gitignore the output.\n2. Add `\"zz\"` to `locales` in `project.inlang/settings.json`.\n3. Switch to it in the locale bridge:\n ```ts\n overwriteGetLocale(() => (isI18nDebug() ? 'zz' : activeLocale))\n ```\n\nKeep `zz` out of the app's own `supportedLocales` — that drives URL prefixes, hreflang and any backend `locale` param, none of which should see it.\n\nGenerate the catalogue in dev only. With `messages/zz.json` absent, Paraglide compiles `zz` to an alias of the base locale (`const zz_x = en_x` — one line per message, no duplicated strings), so a production bundle carries the locale at effectively zero cost.\n\nBoth the generator and the store bridge are being upstreamed (pikkujs/pikku#1036, #1035).\n\nThe wrapper alternative — a module that walks the namespace and pipes each message through a `mask()` — is what this replaces. It defeats tree-shaking (touching every export), adds a check on every call, and forces every component to import `m` from the wrapper instead of Paraglide.\n\n## What NOT to do\n\n- Don't hardcode display strings \"just for now\" — the message is the work.\n- Don't edit or commit anything under `src/paraglide/` — it's regenerated; change `messages/*.json` instead.\n- **Don't wrap `m`.** No re-export module, no branding layer, no resolver. Components import `m` from `../paraglide/messages.js` and call it. `@pikku/react`'s `I18nString` is declared as `string & { readonly __brand: 'LocalizedString' }` — deliberately identical to Paraglide's own `LocalizedString` — so `m.some__key()` satisfies the `@pikku/mantine` `I18nNode` gate natively. A wrapper adds nothing and costs per-message tree-shaking.\n- Don't re-resolve messages by string key or re-implement `{param}` interpolation. A key-string resolver turns a missing key back into silent runtime text, surrendering the type safety that is the entire reason to use Paraglide.\n- Don't reach for i18next/react-i18next or a runtime-fetch translation loader — Paraglide's compiled functions are the whole delivery mechanism.\n- Don't tokenize backend error messages or logs here — those are not frontend display strings.\n", "pikku-info/SKILL.md": "---\nname: pikku-info\ndescription: >-\n Discover what exists in a Pikku project — functions, tags, middleware, permissions, HTTP routes,\n channels, schedulers, queues, and more. Use when you need to understand the project structure,\n find existing functions, or check what middleware and permissions are defined. TRIGGER when:\n user asks \"what functions exist?\", \"show me the project structure\", \"list\n routes/middleware/permissions\", or needs to understand an existing Pikku codebase. DO NOT\n TRIGGER when: user is writing new code (use the specific wiring skill) or asking about Pikku\n concepts (use pikku-concepts).\ninstallGroups: [core]\nallowed-tools: Bash(yarn pikku info *)\nargument-hint: '[functions|tags|middleware|permissions] [--verbose] [--limit N]'\n---\n\n# Pikku Project Discovery\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nUse the `pikku info` CLI commands to inspect this Pikku project. Run the commands below and present the results to the user in a clear summary.\n\n## Available Commands\n\nAlways use `--silent` to suppress the banner and inspector logs.\n\n### Functions\n\nList all registered pikku functions:\n\n```bash\nyarn pikku info functions --silent\n```\n\nFor full details including transport type (http/channel/scheduler/queue/workflow/mcp/cli/trigger), middleware, permissions, and source file:\n\n```bash\nyarn pikku info functions --verbose --silent\n```\n\n### Tags\n\nList all tags with counts of associated functions and middleware:\n\n```bash\nyarn pikku info tags --silent\n```\n\nFor full names instead of counts:\n\n```bash\nyarn pikku info tags --verbose --silent\n```\n\n### Middleware\n\nList all middleware definitions:\n\n```bash\nyarn pikku info middleware --silent\n```\n\nFor full details including source file, required services, and description:\n\n```bash\nyarn pikku info middleware --verbose --silent\n```\n\n### Permissions\n\nList all permission definitions:\n\n```bash\nyarn pikku info permissions --silent\n```\n\nFor full details including source file, required services, and description:\n\n```bash\nyarn pikku info permissions --verbose --silent\n```\n\n## Instructions\n\n1. If the user specifies a subcommand (e.g., `/pikku-info functions`), run only that command.\n2. If no subcommand is specified, run all four commands to give a complete project overview.\n3. Always use `--silent` to suppress the Pikku banner and inspector logs.\n4. Use `--verbose` when the user asks for details, file paths, or \"more info\".\n5. Use `--limit N` to control output size (default is 50 rows).\n6. After running the commands, summarize the findings concisely:\n - Total count of functions, tags, middleware, and permissions\n - Notable patterns (e.g., which transport types are in use, which tags group the most functions)\n - Any functions without tags or transport types (potential issues)\n", "pikku-jose/SKILL.md": "---\nname: pikku-jose\ndescription: >-\n Use when setting up JWT authentication with the jose library in a Pikku app. Covers\n JoseJWTService constructor, secret rotation, token encoding/decoding/verification. TRIGGER when:\n code uses JoseJWTService, user asks about JWT setup, token signing, token verification, or\n @pikku/jose. DO NOT TRIGGER when: user asks about session middleware (use pikku-security) or\n general service setup (use pikku-services).\n---\n\n# Pikku Jose (JWT Service)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/jose` provides JWT signing, verification, and decoding using the [jose](https://github.com/panva/jose) library. Implements the `JWTService` interface from `@pikku/core`.\n\n## Installation\n\n```bash\nyarn add @pikku/jose\n```\n\n## API Reference\n\n### `JoseJWTService`\n\n```typescript\nimport { JoseJWTService } from '@pikku/jose'\n\nconst jwt = new JoseJWTService(\n getSecrets: () => Promise<Array<{ id: string; value: string }>>,\n logger?: Logger\n)\n\nawait jwt.init()\n```\n\n**Constructor Parameters:**\n\n- `getSecrets` — Async function returning an array of `{ id, value }` key pairs. First key is used for signing; all keys are tried for verification (supports rotation).\n- `logger` — Optional logger instance.\n\n**Methods:**\n\n- `init(): Promise<void>` — Fetch and cache secrets. Call at startup.\n- `encode<T>(expiresIn: RelativeTimeInput, payload: T): Promise<string>` — Create a signed JWT.\n- `decode<T>(token: string): Promise<T>` — Decode a JWT payload without verification.\n- `verify(token: string): Promise<void>` — Verify a JWT signature and expiry.\n\n## Usage Patterns\n\n### Basic Setup\n\n```typescript\nimport { JoseJWTService } from '@pikku/jose'\n\nconst jwt = new JoseJWTService(\n async () => [{ id: 'key-1', value: process.env.JWT_SECRET! }],\n logger\n)\nawait jwt.init()\n```\n\n### Secret Rotation\n\nSupply multiple keys. The first is used for signing; all are tried for verification:\n\n```typescript\nconst jwt = new JoseJWTService(async () => [\n { id: 'key-2', value: NEW_SECRET }, // signs with this\n { id: 'key-1', value: OLD_SECRET }, // still verifies tokens signed with this\n])\n```\n\n### With Pikku Services\n\n```typescript\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new ConsoleLogger()\n const jwt = new JoseJWTService(\n async () => [{ id: 'my-key', value: config.jwtSecret }],\n logger\n )\n await jwt.init()\n return { config, logger, jwt }\n})\n```\n\n### Encoding & Verifying Tokens\n\n```typescript\nconst token = await jwt.encode('1h', { userId: 'abc', role: 'admin' })\n\nawait jwt.verify(token) // throws if invalid/expired\n\nconst payload = await jwt.decode<{ userId: string; role: string }>(token)\n```\n", "pikku-knowledge/SKILL.md": "---\nname: pikku-knowledge\ndescription: >-\n Use when writing, reading, reorganising or validating a project's knowledge/ directory — the\n notes that say what the app is, in the language its users use. Covers the Open Knowledge Format\n note (path-as-identity markdown, YAML frontmatter, only `type` required), the sections of the\n app-project profile (slices, entities, decisions, questions, wishlist) and the one question each\n answers, slice status/entities/gherkin rules, the `resource:` URI scheme that ties a note to the\n code it is about, the shapes that are NOT a knowledge base, and the `pikku knowledge\n validate|index` commands. TRIGGER when: user asks to write down a decision, a requirement, an\n entity or an open question; asks what the app does or is; asks about knowledge/, notes, slices,\n or an index.md; or hands over a product brief to record. DO NOT TRIGGER when: user asks what\n functions, routes, tables or permissions exist (that is `pikku meta` / `pikku info`, never a\n note), or asks to write a scenario test (use pikku-scenario).\ninstallGroups: [core]\n---\n\n# Pikku Knowledge\n\nThe knowledge base is `knowledge/` at the repo root: markdown notes about **what the app is**, written for whoever picks the project up next — human or agent.\n\nNot to be confused with `.knowledge/` — the dot-prefixed JSON blueprint that `pikku-software-archaeology` extracts from a legacy repo. Different directory, different format, different purpose.\n\n## Agent Operating Procedure\n\n1. **Read `knowledge/index.md` first**, then the section index for whatever you are about to touch. It is the cheapest way to learn what the app already claims about itself.\n2. Before writing a note, ask whether `pikku meta` already answers it. If it does, do not write the note — see _What never goes in a note_.\n3. Write the note in the section that answers its question. Create the section's `index.md` in the same turn you create the section.\n4. Add a `resource:` only if you can name a real id. A wrong one is worse than none.\n5. Run `pikku knowledge validate`. Fix what it reports.\n6. Run `pikku knowledge index` so each section lists what is actually in it.\n\n## The governing rule\n\n**Record only what pikku cannot tell you.**\n\nPikku already knows every function, route, schema, table, column, queue, cron, channel and permission — `pikku meta` prints them, and the generated meta is the truth. A note that lists tables or routes is a copy that starts drifting the moment somebody edits the code, and it drifts _while looking authoritative_, which is worse than silence.\n\nWhat a note is for is the part no generator can derive: what a thing means, why a rule was chosen, what it rules out, who asked for it, and what is still unanswered.\n\n## The note\n\nA note is a markdown file whose **path is its identity** — moving it renames it. It carries YAML frontmatter and a body:\n\n```markdown\n---\ntype: decision\ntitle: Revocation ends a grant\ndescription: A revoked grant stops working immediately, everywhere.\nresource: func:revokeGrant, table:grant\ntags: [sharing, access]\n---\n\n# Revocation ends a grant\n\nWhen an owner revokes a grant, the person loses access on their next request — no\ngrace period and no scheduled cleanup.\n\nThis rules out a \"revoked but valid until midnight\" state, which we considered\nfor shared days and rejected: two people disagreeing about who can see today is\nworse than one of them losing access mid-session.\n```\n\nFrontmatter fields:\n\n| Field | Meaning |\n| ------------- | ----------------------------------------------------------------------------------------------------------------------- |\n| `type` | **The only required field.** `slice`, `entity`, `decision`, `note`, `overview`. Lowercase — gates compare it literally. |\n| `title` | What to call the note in a listing. Falls back to the first heading, then the filename. |\n| `description` | One line, used as the note's subtitle in a section index. |\n| `resource` | Comma-separated `<kind>:<id>` URIs — the code this note is about. See below. |\n| `tags` | Flow list (`[a, b]`) or a `- item` block; both are read. |\n| `timestamp` | When it was written, if it matters. |\n\n`index.md` and `log.md` are **reserved**: an `index.md` maps a directory, a `log.md` is an append-only record. Neither is ever listed as a note by an index.\n\nPlain markdown links between notes — `[revocation](../decisions/revocation-ends-a-grant.md)` — are what make the base a graph. A link to a note that does not exist yet is legal: it marks something worth writing, not an error.\n\n## The layout\n\n```\nknowledge/\n index.md # type: overview — the map\n slices/\n index.md\n 01-the-daily-entry.md # type: slice\n entities/\n index.md\n entry.md # type: entity\n decisions/\n index.md\n revocation-ends-a-grant.md # type: decision\n security/\n index.md\n one-account-one-person.md\n questions/\n index.md\n who-owns-a-shared-day.md # type: note\n wishlist/\n index.md\n export-to-a-calendar.md # type: note\n```\n\nEach section answers exactly one question, which is what lets a reader find a note without an index of indexes:\n\n| Section | The question it answers |\n| --------------------- | ------------------------------------------------------------------ |\n| `slices/` | What is one buildable piece of this app, and what proves it works? |\n| `entities/` | What is this thing, in the words users use for it? |\n| `decisions/` | What was chosen, and what does that rule out? |\n| `decisions/security/` | Who may do what? |\n| `questions/` | What has been asked and not yet answered? |\n| `wishlist/` | What does somebody want that nobody has asked to be built? |\n\n**Create a section the turn you have a note for it** — never a scaffold of empty directories, and never a section without its own `index.md`. A section index says in one line what belongs in it; that sentence is the reason the file exists, so `pikku knowledge index` writes only the note listing and leaves your prose alone.\n\n## Slices\n\nA slice is the one note type that is a piece of _work_ rather than a fact, so it alone carries state and size:\n\n````markdown\n---\ntype: slice\ntitle: The daily entry\ndescription: An owner writes one entry per day, and sees it on the day.\nstatus: proposed\nentities: entry, day\nresource: func:createEntry\n---\n\n# The daily entry\n\nAn owner writes at most one entry per day. Writing again replaces it.\n\n```gherkin\nGiven 'owner' has no entry for today\nWhen 'owner' writes one\nThen it appears on today's day\nAnd writing again replaces it rather than adding a second\n```\n````\n\n- **`status`** is `proposed` → `dispatched` → `built`. Nothing else. Every gate compares it literally.\n- **`entities`** lists what the slice touches, **at most three**. Past three it is not one buildable piece — split it.\n- **The scenario is a fenced `gherkin` block, in the third person.** `Given 'owner' has no entry` — never `Given I have no entry`. A quoted word _means a persona_, which is what lets a reader (and a test) tell who is acting. First person hides that, so it is rejected.\n\n## `resource:` — tying a note to the code\n\n`resource:` names the code a note is about, as one or more `<kind>:<id>` URIs, comma-separated.\n\n**Every kind resolves.** That is the whole design: a kind that cannot be checked lets notes accumulate references nothing validates, and the graph rots into fiction exactly where it looks most authoritative.\n\n| Kind | An id is | Where it resolves |\n| ----------- | -------------------------------------------------- | --------------------------------------------------------------------------------- |\n| `func:` | a function id | generated function meta |\n| `workflow:` | a workflow name | generated workflow meta |\n| `schema:` | a schema name | generated schemas |\n| `http:` | a route, `method:route`, or the function behind it | generated http wirings |\n| `queue:` | a queue name | generated queue wirings |\n| `cron:` | a scheduled task name | generated scheduler wirings |\n| `channel:` | a channel name | generated channel meta |\n| `table:` | a table name | the generated db schema |\n| `addon:` | `@pikku/addon-x` or bare `x` | the manifests that declare the dependency |\n| `scope:` | a scope name | the `scopes:` a function gates itself with, plus the grants in `scenarios.actors` |\n| `persona:` | a persona name | `scenarios.personas` in `pikku.config.json` |\n\nIds are case-sensitive: `createEntry` is not `createentry`.\n\nThe check **fails closed on drift and open on ignorance**. An id missing from a kind that resolved is an error — the code was renamed or deleted under the note. A kind with no generated meta at all is skipped, so a project without queues is never told its queue references are broken.\n\nThere is no kind for a service, a middleware or a component. Say it in prose instead.\n\n## What never goes in a note\n\nThese are all things that exist somewhere better, so a note is always the copy that drifts:\n\n| Do not write | Because it lives in |\n| ----------------------------------- | ---------------------------------------------------------- |\n| a `personas/` section | `scenarios.personas` in `pikku.config.json` |\n| a `scenarios/` section | the gherkin block inside the slice it belongs to |\n| a `permissions/` section | a decision note under `decisions/security/` |\n| a list of tables, columns or routes | `pikku meta` — the generated schema _is_ the schema |\n| a changelog | `CHANGELOG.md` at the repo root |\n| **secrets or credentials** | a secrets service. Never here — `knowledge/` is committed. |\n\nAnd two shapes that look like a knowledge base but are not:\n\n- **A flat `product.md` / `glossary.md` / `technology.md` at the root of `knowledge/`.** That is one long document: nothing can link into part of it, and no gate can read it. Split it into notes in the sections that answer its questions.\n- **A directory tree with no notes in it.** Sections exist because there is something to put in them.\n\n## The commands\n\n```bash\npikku knowledge validate # check the base against this profile\npikku knowledge index # refresh every index.md\npikku knowledge index --check # report stale indexes without writing (CI gate)\n```\n\n`validate` reports: notes with no `type`, a missing `knowledge/index.md`, a section with no `index.md`, notes flat at the root, sections that duplicate what the project already declares, slices with a bad or missing `status`, slices over three entities, slices with no gherkin block or a first-person one, and every `resource:` that no longer resolves. Errors fail the command; warnings do not.\n\n`index` rewrites only the block between `<!-- pikku:knowledge-index -->` markers, creating a scaffolded `index.md` for a section that has none. It is idempotent — running it twice changes nothing.\n\n## Profiles built on this one\n\nOKF permits frontmatter fields a reader does not know, and the parser ignores them rather than failing. That is the extension point: a tool layered on Pikku can add its own sections and fields on top of everything above without forking the format.\n\nFabric is the one that exists. It adds `decisions/design/` — rules about how the app looks and behaves — and a `design:` field on a slice pointing at the design options it was built from. Both are Fabric's to validate; `pikku knowledge validate` passes them through untouched. Everything else in this skill is the same in both.\n", "pikku-kysely/SKILL.md": "---\nname: pikku-kysely\ndescription: >-\n Use when WRITING KYSELY QUERIES (select/join/aggregate/insert/update/delete) inside a Pikku\n function body, or when setting up SQL database services with Kysely. Covers the query builder\n API (joins, aggregates + groupBy/having, returning, sql template, expression builder, $if,\n transactions, jsonArrayFrom relation helpers) AND @pikku/kysely service setup (channel stores,\n workflow services, secret services, AI storage, deployment services). TRIGGER when: writing any\n non-trivial kysely query (a join, an aggregate/count/sum, groupBy, subquery, transaction, or\n conditional query), the injected `kysely` service is used in a function body, or code uses\n PikkuKysely, KyselyChannelStore, KyselyWorkflowService, KyselySecretService, or the user asks\n about SQL setup with Pikku. DO NOT TRIGGER when: user asks about MongoDB (use pikku-mongodb) or\n Redis (use pikku-redis).\ninstallGroups: [core]\n---\n\n# Pikku Kysely (SQL Database Services)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n## Writing Queries — the Kysely query builder\n\nIn a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly. Pikku wires the **CamelCasePlugin**, so you write **camelCase everywhere in TS** (columns, aliases) and raw **snake_case ONLY inside a `` sql`` `` literal**. Kysely is a query builder, NOT an ORM — there are no relations; shape nested data with the JSON helpers below. Never hand-roll SQL strings; never annotate the return type (in Pikku the output zod schema IS the type).\n\n```typescript\nimport { sql } from 'kysely'\n// Relation helpers are ENGINE-SPECIFIC — import the matching path:\nimport { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/sqlite' // SQLite / libSQL\n// import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres' // Postgres\n```\n\n```typescript\n// SELECT + where/orderBy/limit/offset. Terminals: .execute() | .executeTakeFirst()\n// | .executeTakeFirstOrThrow(() => new NotFoundError()) — pass an error factory.\nconst rows = await kysely.selectFrom('item')\n .select(['id', 'name', 'quantity'])\n .where('warehouseId', '=', warehouseId)\n .orderBy('name').limit(50).execute()\n\n// JOINS + aliased selects (qualify columns once a join exists)\nawait kysely.selectFrom('stock')\n .innerJoin('item', 'item.id', 'stock.itemId')\n .leftJoin('bin as b', 'b.id', 'stock.binId')\n .select(['stock.id', 'item.name as itemName', 'b.code as binCode'])\n .execute()\n\n// AGGREGATES via the fn helper + groupBy/having. eb.fn.count returns string|number —\n// cast if you need a JS number (SQLite: CAST(... AS INTEGER)).\nawait kysely.selectFrom('stock')\n .select((eb) => ['itemId', eb.fn.sum<number>('quantity').as('onHand')])\n .groupBy('itemId')\n .having((eb) => eb.fn.sum('quantity'), '<', 10) // low-stock\n .execute()\n\n// INSERT + RETURNING (one round-trip; works on SQLite & Postgres)\nconst created = await kysely.insertInto('item')\n .values({ name: input.name, warehouseId })\n .returning(['id', 'name']).executeTakeFirstOrThrow()\n\n// UPDATE + RETURNING, DELETE\nawait kysely.updateTable('item').set({ quantity: input.quantity })\n .where('id', '=', input.id).returning(['id', 'quantity']).executeTakeFirstOrThrow()\nawait kysely.deleteFrom('item').where('id', '=', input.id).execute()\n\n// EXPRESSION BUILDER for and/or; $if for conditional building; sql for raw fragments\nawait kysely.selectFrom('item')\n .selectAll()\n .where((eb) => eb.or([eb('quantity', '=', 0), eb('discontinued', '=', true)]))\n .$if(!!input.search, (qb) => qb.where('name', 'like', `%${input.search}%`))\n .select(sql<number>`quantity * unit_cost`.as('value')) // snake_case ok inside sql``\n .execute()\n\n// NESTED DATA (no relations) — jsonObjectFrom (one) / jsonArrayFrom (many)\nawait kysely.selectFrom('warehouse')\n .select((eb) => ['warehouse.id', 'warehouse.name',\n jsonArrayFrom(eb.selectFrom('bin').select(['bin.id', 'bin.code'])\n .whereRef('bin.warehouseId', '=', 'warehouse.id')).as('bins')])\n .execute()\n\n// TRANSACTION — multi-write atomicity. Use trx (not kysely) inside.\nawait kysely.transaction().execute(async (trx) => {\n await trx.updateTable('stock').set({ quantity: 0 }).where('itemId', '=', id).execute()\n await trx.insertInto('stockMove').values({ itemId: id, delta: -qty }).execute()\n})\n```\n\nPikku provides SQL database services through four packages:\n\n- `@pikku/kysely` — Base service implementations (database-agnostic)\n- `@pikku/kysely-postgres` — PostgreSQL-specific implementations + `PikkuKysely` connection wrapper\n- `@pikku/kysely-mysql` — MySQL-specific implementations\n- `@pikku/kysely-sqlite` — SQLite-specific implementations + `createSQLiteKysely` factory\n\nAll implement standard Pikku interfaces from `@pikku/core`.\n\n## Installation\n\n```bash\n# Pick your database\nyarn add @pikku/kysely @pikku/kysely-postgres # PostgreSQL\nyarn add @pikku/kysely @pikku/kysely-mysql # MySQL\nyarn add @pikku/kysely @pikku/kysely-sqlite # SQLite\n```\n\n## API Reference\n\n### PostgreSQL Connection — `PikkuKysely`\n\n```typescript\nimport { PikkuKysely } from '@pikku/kysely-postgres'\n\nconst db = new PikkuKysely<DB>(\n logger: Logger,\n connectionOrConfig: postgres.Sql | postgres.Options | string,\n defaultSchemaName?: string\n)\n\nawait db.init()\ndb.kysely // Kysely<DB> instance for queries\nawait db.close()\n```\n\n### SQLite Factory — `createSQLiteKysely`\n\n```typescript\nimport { createSQLiteKysely } from '@pikku/kysely-sqlite'\n\nconst kysely = createSQLiteKysely(database: SqliteDatabase | (() => Promise<SqliteDatabase>))\n```\n\n### Available Services\n\nEach database variant exports these services with a prefix (`Pg`, `MySQL`, `SQLite`, or base `Kysely`):\n\n| Service | Interface | Purpose |\n| --------------------- | ------------------------------------- | ---------------------------------------------- |\n| `*ChannelStore` | `ChannelStore` | WebSocket channel state persistence |\n| `*EventHubStore` | `EventHubStore` | Event hub state persistence |\n| `*WorkflowService` | `PikkuWorkflowService` | Workflow definition storage |\n| `*WorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |\n| `*DeploymentService` | `DeploymentService` | Deployment state management |\n| `*AIStorageService` | `AIStorageService, AIRunStateService` | AI conversation/run storage |\n| `*AgentRunService` | `AgentRunService` | Agent execution tracking |\n| `*SecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |\n\nAll services take a `Kysely<KyselyPikkuDB>` instance in their constructor and have an `init()` method that creates tables if needed.\n\n### Secret Service\n\n```typescript\nimport { PgKyselySecretService } from '@pikku/kysely-postgres'\n\nconst secrets = new PgKyselySecretService(db.kysely, {\n kekSecret: 'your-key-encryption-key',\n salt: 'your-salt',\n})\nawait secrets.init()\n\nawait secrets.setSecret('api-key', { key: 'sk-...' })\nconst value = await secrets.getSecret<{ key: string }>('api-key')\nawait secrets.rotateKEK() // Re-encrypt all secrets with new KEK\n```\n\n## Usage Patterns\n\n### PostgreSQL Setup\n\n```typescript\nimport {\n PikkuKysely,\n PgKyselyChannelStore,\n PgKyselyWorkflowService,\n} from '@pikku/kysely-postgres'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n const db = new PikkuKysely(logger, config.databaseUrl)\n await db.init()\n\n const channelStore = new PgKyselyChannelStore(db.kysely)\n await channelStore.init()\n\n const workflowService = new PgKyselyWorkflowService(db.kysely)\n await workflowService.init()\n\n return { config, logger, database: db, channelStore, workflowService }\n})\n```\n\n### SQLite Setup\n\n```typescript\nimport {\n createSQLiteKysely,\n SQLiteKyselyChannelStore,\n} from '@pikku/kysely-sqlite'\nimport Database from 'better-sqlite3'\n\nconst kysely = createSQLiteKysely(new Database('app.db'))\nconst channelStore = new SQLiteKyselyChannelStore(kysely)\nawait channelStore.init()\n```\n\n### MySQL Setup\n\n```typescript\nimport { MySQLKyselyWorkflowService } from '@pikku/kysely-mysql'\n\nconst workflowService = new MySQLKyselyWorkflowService(kyselyInstance)\nawait workflowService.init()\n```\n", "pikku-machine-auth/SKILL.md": "---\nname: pikku-machine-auth\ndescription: >-\n Use when authenticating a CLI/agent/service against a Pikku server, adding machine-to-machine\n (M2M) auth, issuing scoped API keys for sandboxes/agents/workers, or wiring better-auth sessions\n into Pikku middleware. Covers `pikku login` (device-authorization), the better-auth API Key\n plugin, machine identities, and `betterAuthSession` with the api-key branch. TRIGGER when: user\n asks about CLI login, `pikku login`, machine agents, service-to-service auth, API keys, client\n credentials, sandbox/worker tokens, or resolving a better-auth session in a Pikku function. DO\n NOT TRIGGER when: user asks about end-user HTTP session/cookie auth only (use pikku-http + the\n app betterAuth config) or about WebSocket channel mechanics (use pikku-websocket).\n---\n\n# Pikku Machine Auth\n\nUnified authentication for humans **and** machines against a Pikku + better-auth\nserver. Two paths, two headers, one resolver:\n\n| Caller | Credential | Header | Obtained by |\n|---|---|---|---|\n| **Human** (CLI, dev) | better-auth session token | `Authorization: Bearer <token>` | `pikku login` (device flow) → `~/.pikku/session.json` |\n| **Machine** (agent, sandbox, worker) | scoped API key | `x-api-key: <key>` | `createApiKey` (server-side, at provision/spawn) |\n\nBoth resolve to a Pikku `UserSession` through one middleware:\n`betterAuthSession({ mapSession, apiKey: { mapKey } })`.\n\n> The literal OAuth `client_credentials` grant is **not** implemented in\n> better-auth's oidc-provider. The API Key plugin gives the same capability (a\n> baked secret a service presents for scoped access), not the wire protocol.\n\n## Agent Operating Procedure\n\n1. Discover before editing — inspect the app's `betterAuth({ plugins: [...] })`\n config and existing middleware wiring before adding anything.\n2. Server changes go in the auth factory + a middleware wiring file; never put\n auth checks in a function body (use `permissions`).\n3. The API Key plugin contributes an `apikey` table — add the matching SQL\n migration and regenerate DB types before relying on it.\n4. Validate with the narrowest command, then `pikku all`.\n\n## Human path — `pikku login`\n\n```bash\npikku login --url https://app.example.com # device-authorization flow\npikku whoami # show current session + expiry\npikku logout # remove stored session\n```\n\n`pikku login` runs the RFC 8628 device flow: it requests a code, opens the\nbrowser to the verification URL, polls until you approve, then stores the\nsession token (keyed by base URL) at `~/.pikku/session.json` with its expiry.\n\n**Server requirement** — enable the `deviceAuthorization` and `bearer` plugins:\n\n```typescript\nimport { deviceAuthorization, bearer } from 'better-auth/plugins'\n\nbetterAuth({\n // ...\n plugins: [\n deviceAuthorization({ expiresIn: '5min', interval: '5s', schema: {} }),\n bearer(), // lets `Authorization: Bearer <session-token>` resolve a session\n ],\n})\n```\n\nThe browser approval is two steps the user's browser does automatically:\n`GET /auth/device?user_code=XXXX` (claims the code while signed in) then\n`POST /auth/device/approve`. The CLI only requests the code and polls\n`POST /auth/device/token`.\n\n## Machine path — API keys\n\nInstall the plugin (separate official package) and enable it:\n\n```bash\nyarn add @better-auth/api-key # peer: better-auth ^1.6.19\n```\n\n```typescript\nimport { apiKey } from '@better-auth/api-key'\n\nbetterAuth({\n plugins: [\n apiKey({\n enableMetadata: true, // REQUIRED to store scope on the key\n enableSessionForAPIKeys: true, // lets a key resolve via getSession too\n }),\n ],\n})\n```\n\n### Identity model\n\nA **machine is an API key, not a throwaway user.** Keys are owned by a small set\nof stable **service-user** identities you provision once (e.g. `orchestrator`,\n`machine-agent`, `builder`, `sandbox-runtime`). Per-machine scope rides on the\nkey's `metadata`/`permissions`. A key requires a real owning user row — minting\none for a non-existent `userId` is created but will not resolve.\n\n### Mint a scoped key (server-side, at spawn/provision)\n\n```typescript\n// `auth` is the better-auth instance (injected service)\nconst { key } = await auth.api.createApiKey({\n body: {\n userId: sandboxRuntimeUserId, // a stable service user\n name: `sandbox:${sandboxId}`,\n expiresIn: 60 * 60, // seconds\n metadata: { sandboxId }, // keep only STABLE ids here\n permissions: { sandbox: ['read', 'write'] },\n },\n})\n// inject `key` into the machine's env; it sends it as `x-api-key`.\n```\n\nRotate by minting a new key and expiring/deleting the old (`deleteApiKey`);\nmultiple active keys per identity allow zero-downtime rotation.\n\n### Resolve scope — `verifyApiKey`, not `getSession`\n\n`getSession(x-api-key)` returns only a bare mock session **without** the\nmetadata. Scope must come from `verifyApiKey`, which returns\n`{ valid, key: { userId, metadata, permissions } }`. The\n`betterAuthSession` api-key branch does this for you:\n\n```typescript\nimport { betterAuthSession } from '@pikku/better-auth'\nimport { addHTTPMiddleware } from '@pikku/core/http'\n\naddHTTPMiddleware([\n betterAuthSession({\n // human path: getSession result -> app session\n mapSession: ({ user }) => ({ userId: user.id }),\n // machine path: verified key -> app session. `services` lets you resolve\n // CURRENT scope (e.g. look up the owning row) instead of trusting only the\n // baked metadata.\n apiKey: {\n header: 'x-api-key', // default\n mapKey: async (key, services) => {\n const sandboxId = key.metadata?.sandboxId\n if (!sandboxId) return null // reject\n const row = await services.kysely\n .selectFrom('sandboxInstance')\n .innerJoin('sandbox', 'sandbox.id', 'sandboxInstance.sandboxId')\n .select(['sandbox.orgId', 'sandbox.projectId'])\n .where('sandboxInstance.sandboxId', '=', sandboxId)\n .where('sandboxInstance.stoppedAt', 'is', null)\n .executeTakeFirst()\n if (!row) return null\n return { userId: sandboxId, orgId: row.orgId, role: 'sandbox' }\n },\n },\n }),\n])\n```\n\nWhen the api-key header is present it is authoritative — the middleware never\nfalls through to `getSession` (a bare mock session would shadow the scoped one).\nWhen it is absent, the human `getSession` path runs as normal.\n\n### WebSocket channels authenticate on the upgrade handshake\n\nGenerated channel CLI clients attach the credential as a connection header\n(`x-api-key` for `PIKKU_API_KEY`, else `Authorization: Bearer` from\n`~/.pikku/session.json`). The `@pikku/ws` server copies the upgrade-request\nheaders into the channel's `http.request` and runs the inherited HTTP `*`\nmiddleware during `runUpgradeMiddleware`, so `betterAuthSession` resolves the\nsession before the channel opens. For this to work the app must register\n`betterAuthSession` via `addHTTPMiddleware([...])` (the `*` group) — not only on\nspecific routes — so it is inherited into the channel upgrade. Browser clients\ncannot set WebSocket headers, so header-auth only covers the Node CLI path; a\nbrowser channel needs a query-param/subprotocol vector instead.\n\n## Gotchas\n\n- `apiKey()` rejects `metadata` unless `enableMetadata: true`.\n- `deviceAuthorization()` requires a `schema` option (pass `schema: {}`).\n- Keep the two paths on **different headers** — `x-api-key` (machine) vs\n `Authorization: Bearer` (human). One header for both reintroduces ambiguity.\n- The `apikey` table is plugin-contributed — add the SQL migration + regen types.\n- `~/.pikku/session.json` is written `0600` and stores the token + expiry; the\n CLI uses the expiry to detect when a re-login is needed.\n", "pikku-mcp/SKILL.md": "---\nname: pikku-mcp\ndescription: >-\n Use when exposing Pikku functions as MCP tools, resources, or prompts for AI assistants. Covers\n mcp: true flag, pikkuMCPResourceFunc, pikkuMCPPromptFunc, and MCP wire object. TRIGGER when:\n code uses mcp: true or pikkuMCPResourceFunc/pikkuMCPPromptFunc, user asks about MCP, Model\n Context Protocol, AI tool integration, or exposing functions to Claude/ChatGPT. DO NOT TRIGGER\n when: user asks about AI agents (use pikku-ai-agent) or general function definitions (use\n pikku-concepts).\ninstallGroups: [core]\n---\n\n# Pikku MCP Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nExpose Pikku functions as Model Context Protocol (MCP) tools, resources, and prompts for AI assistants like Claude, ChatGPT, and others.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions that could become MCP tools\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### MCP Tools (simplest approach)\n\nAdd `mcp: true` to any existing `pikkuFunc` to expose it as an MCP tool:\n\n```typescript\nconst myFunc = pikkuFunc({\n description: string, // Used as MCP tool description\n input: ZodSchema, // Becomes MCP tool input schema\n output: ZodSchema, // Return type\n mcp: true, // ← Expose as MCP tool\n func: async (services, data) => { ... },\n})\n```\n\n### MCP Resources (`pikkuMCPResourceFunc`)\n\n```typescript\nimport { pikkuMCPResourceFunc } from '#pikku'\n\nconst resource = pikkuMCPResourceFunc({\n uri: string, // URI template, e.g. 'todos/{id}'\n title: string, // Human-readable title\n description?: string,\n func: async (services, data, { mcp }) => {\n // Must return array of { uri, text } or { uri, blob, mimeType }\n return [{ uri: mcp.uri!, text: JSON.stringify(result) }]\n },\n})\n```\n\n### MCP Prompts (`pikkuMCPPromptFunc`)\n\n```typescript\nimport { pikkuMCPPromptFunc } from '#pikku'\n\nconst prompt = pikkuMCPPromptFunc({\n name: string,\n description: string,\n func: async (services, data) => {\n // Must return array of MCP messages\n return [\n {\n role: 'user',\n content: { type: 'text', text: '...' },\n },\n ]\n },\n})\n```\n\n### MCP Wire Object\n\nInside MCP-enabled functions, `wire.mcp` provides:\n\n```typescript\nmcp.uri // Current resource URI (for resources)\nmcp.sendResourceUpdated(uri) // Notify clients a resource changed\nmcp.enableTools({ toolName: true }) // Dynamically enable/disable tools\n```\n\n## Usage Patterns\n\n### Expose Existing Functions as MCP Tools\n\nThe simplest path — add `mcp: true` to any function:\n\n```typescript\nexport const createTodo = pikkuFunc({\n description: 'Create a new todo item',\n input: CreateTodoInput,\n output: CreateTodoOutput,\n mcp: true,\n func: async ({ db }, { text, priority }) => {\n return await db.createTodo({ text, priority })\n },\n})\n```\n\n### MCP Resources with URI Templates\n\n```typescript\nexport const getTodo = pikkuMCPResourceFunc({\n uri: 'todos/{id}',\n title: 'Todo Details',\n description: 'Get a todo by ID',\n func: async ({ db }, { id }, { mcp }) => {\n const todo = await db.getTodo(id)\n return [{ uri: mcp.uri!, text: JSON.stringify(todo) }]\n },\n})\n```\n\n### MCP Prompts\n\n```typescript\nexport const codeReview = pikkuMCPPromptFunc({\n name: 'codeReview',\n description: 'Generate a code review prompt',\n func: async ({}, { filePath, context }) => {\n return [\n {\n role: 'user',\n content: {\n type: 'text',\n text: `Review ${filePath}. Context: ${context}`,\n },\n },\n ]\n },\n})\n```\n\n### Dynamic Tool Control\n\n```typescript\nexport const manageTodos = pikkuFunc({\n description: 'Manage todo items',\n input: ManageTodosInput,\n output: ManageTodosOutput,\n mcp: true,\n func: async ({ db }, { action, id }, { mcp }) => {\n if (action === 'delete') {\n await db.deleteTodo(id)\n mcp.sendResourceUpdated(`todos/${id}`)\n await mcp.enableTools({ archiveTodos: true })\n return { deleted: true }\n }\n },\n})\n```\n\n### MCP Server Setup\n\n```typescript\n// start.ts\nimport { PikkuMCPServer } from '@pikku/modelcontextprotocol'\n\nconst server = new PikkuMCPServer(config, singletonServices, createWireServices)\nawait server.init()\nawait server.start()\n```\n\n## Complete Example\n\n```typescript\n// functions/todos.functions.ts\nexport const listTodos = pikkuSessionlessFunc({\n description: 'List all todo items',\n input: ListTodosInput,\n output: ListTodosOutput,\n mcp: true,\n func: async ({ db }, { status }) => {\n return { todos: await db.listTodos(status) }\n },\n})\n\nexport const createTodo = pikkuFunc({\n description: 'Create a new todo item',\n input: CreateTodoInput,\n output: CreateTodoOutput,\n mcp: true,\n func: async ({ db }, { text, priority }) => {\n return await db.createTodo({ text, priority })\n },\n})\n\nexport const completeTodo = pikkuFunc({\n description: 'Mark a todo as complete',\n input: CompleteTodoInput,\n output: CompleteTodoOutput,\n mcp: true,\n func: async ({ db }, { todoId }) => {\n return await db.completeTodo(todoId)\n },\n})\n\n// functions/todos.mcp.ts\nexport const getTodoResource = pikkuMCPResourceFunc({\n uri: 'todos/{id}',\n title: 'Todo Details',\n description: 'Get details of a specific todo',\n func: async ({ db }, { id }, { mcp }) => {\n const todo = await db.getTodo(id)\n return [{ uri: mcp.uri!, text: JSON.stringify(todo) }]\n },\n})\n\nexport const planDayPrompt = pikkuMCPPromptFunc({\n name: 'planDay',\n description: 'Create a daily plan based on pending todos',\n func: async ({ db }, {}) => {\n const { todos } = await db.listTodos('pending')\n return [\n {\n role: 'user',\n content: {\n type: 'text',\n text: `Plan my day. Here are my pending todos:\\n${todos.map((t) => `- ${t.text} (${t.priority})`).join('\\n')}`,\n },\n },\n ]\n },\n})\n```\n", "pikku-middleware/references/middleware-patterns.md": "# Middleware Patterns (extended)\n\nDetailed, less-common middleware recipes. The common-path bearer-auth pattern lives inline in SKILL.md; this file holds the client-side caller, session-setting, and audit recipes.\n\n## Service-to-Service: the client (caller) side\n\nUse the generated `RPCInvoke` type from `.pikku/rpc/pikku-rpc-wirings-map.gen.d.ts` — never hand-write the input/output types:\n\n```typescript\nimport type { RPCInvoke } from '../../backends/my-service/.pikku/rpc/pikku-rpc-wirings-map.gen.d.js'\n\nexport function getServiceRPC(baseUrl: string, token: string): RPCInvoke {\n return async (name: string, data?: unknown) => {\n const res = await fetch(`${baseUrl}/rpc/${String(name)}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ data: data ?? {} }),\n })\n if (!res.ok) {\n const text = await res.text().catch(() => '')\n throw new Error(`rpc ${String(name)} failed: ${res.status} ${text}`)\n }\n return res.json()\n } as RPCInvoke\n}\n```\n\n## Session-Setting Middleware\n\n```typescript\nconst apiKeyAuth = pikkuMiddleware(async ({ kysely }, { http, setSession, session }, next) => {\n if (session) return next() // already authenticated\n\n const header = http?.request?.header?.('x-api-key')\n if (!header) return next()\n\n const row = await kysely.selectFrom('apiKey').select('userId').where('key', '=', header).executeTakeFirst()\n if (row) setSession?.({ userId: row.userId })\n\n return next()\n})\n\naddTagMiddleware('api-key-auth', [apiKeyAuth])\n```\n\nFunctions tagged `'api-key-auth'` with `auth: true` reject requests without a valid key; those with `auth: false` can inspect the session but won't reject.\n\n## Request Logging / Audit\n\n```typescript\nconst auditLog = pikkuMiddleware(async ({ logger, db }, wire, next) => {\n const start = Date.now()\n await next()\n await db.createAuditLog({ duration: Date.now() - start })\n})\n\naddHTTPMiddleware('/admin/*', [auditLog])\n```\n", "pikku-middleware/SKILL.md": "---\nname: pikku-middleware\ndescription: >-\n Use when adding any middleware to a Pikku app — global HTTP middleware, tag-scoped middleware\n (including service-to-service bearer auth), per-route middleware, session-setting middleware, or\n understanding middleware execution order and priority. TRIGGER when: user wants middleware on\n some or all routes, machine-to-machine auth, tag-scoped cross-cutting concerns, global\n interceptors, or middleware priority/order questions. DO NOT TRIGGER when: user asks about\n permissions/authorization checks (use pikku-permissions), auth strategies like\n authBearer/authCookie (use pikku-security), or deployment.\ninstallGroups: [core]\n---\n\n# Pikku Middleware\n\n## Agent Operating Procedure\n\n1. Discover before editing. Run `pikku info middleware --verbose` and `pikku info tags --json` to understand the existing middleware and tag landscape.\n2. Identify the source files that own the behavior — wirings files, not generated output.\n3. Register middleware at module load time — in a `wirings/*.ts` file, never inside a function body.\n4. Validate: run `pikku all --tsc` after adding or changing middleware — it regenerates and then confirms type safety in one pass.\n\n## The `pikkuMiddleware` Factory\n\n```typescript\nimport { pikkuMiddleware } from '#pikku'\n\n// Simple: just a function\nconst myMiddleware = pikkuMiddleware(async (services, wire, next) => {\n // runs before the function\n await next()\n // runs after the function (optional)\n})\n\n// With metadata (name + priority)\nconst telemetryMiddleware = pikkuMiddleware({\n name: 'my-telemetry',\n priority: 'highest',\n func: async (services, wire, next) => {\n const start = performance.now()\n try {\n await next()\n } finally {\n services.logger.info({ duration: Math.round(performance.now() - start) })\n }\n },\n})\n```\n\nThe `wire` object gives you:\n- `wire.http` — inbound HTTP context (headers, URL, cookies)\n- `wire.setSession(session)` — set the session for this request\n- `wire.getSession()` — read the current session\n- `wire.session` — the session set so far (may be undefined)\n\nThrow a typed error to abort: `UnauthorizedError`, `ForbiddenError`, etc. from `@pikku/core/errors`.\n\n## Scoping: Five Levels\n\nFrom broadest to narrowest:\n\n```typescript\n// 1. Wire-agnostic global: all wire types (HTTP, Queue, Channel, Trigger, Workflow, ...)\naddGlobalMiddleware([telemetryOuter()])\n\n// 2. HTTP global: all HTTP routes\naddHTTPMiddleware('*', [cors(), authBearer()])\n\n// 3. Prefix-based: URL pattern\naddHTTPMiddleware('/admin/*', [auditLog])\n\n// 4. Tag-based: any wiring with matching tag\naddTagMiddleware('machine-agent', [bearerAuth]) // tag on function or wire\n\n// 5. Inline: per-wiring\nwireHTTP({\n route: '/books/:id',\n func: getBook,\n middleware: [cacheControl],\n})\n```\n\n## Global Middleware (`addGlobalMiddleware`)\n\nRuns before everything else, across every wire type: HTTP, Queue, Channel, Trigger, Scheduler, Workflow, Agent, CLI, MCP. Use it for cross-cutting concerns (e.g. telemetry) that must wrap every invocation regardless of transport.\n\n```typescript\nimport { addGlobalMiddleware } from '@pikku/core'\nimport { telemetryOuter, telemetryInner } from '@pikku/core/middleware'\n\naddGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })]) // wraps the full call\naddGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })]) // closest to the function body\n```\n\n`telemetryOuter` ships with `priority: 'highest'`, `telemetryInner` with `priority: 'lowest'` — so priority sorting places outer first regardless of array/call order.\n\n## HTTP & Prefix Middleware (`addHTTPMiddleware`)\n\n```typescript\nimport { addHTTPMiddleware } from '@pikku/core/http'\nimport { cors, authBearer } from '@pikku/core/middleware'\n\n// All routes\naddHTTPMiddleware('*', [cors({ origin: 'https://app.example.com', credentials: true })])\n\n// Scoped to /api/* prefix\naddHTTPMiddleware('/api/*', [rateLimit({ maxRequests: 100, windowMs: 60_000 })])\n```\n\n## Tag Middleware (`addTagMiddleware`)\n\nTag middleware fires for any wiring (function or wire object) that carries a matching tag. This is the canonical approach for service-to-service bearer auth, rate limiting a group, or any cross-cutting concern scoped to a subset of routes.\n\n### Setting Tags\n\n```typescript\n// On the function definition\nexport const myFunc = pikkuSessionlessFunc({\n auth: false,\n tags: ['machine-agent'],\n func: async (services, input) => { ... },\n})\n\n// On the wire object\nwireHTTP({\n route: '/internal/action',\n method: 'post',\n auth: false,\n tags: ['internal'],\n func: myFunc,\n})\n```\n\nTags from the function definition and the wire object are merged — middleware from both tag sets runs.\n\n### Registering Tag Middleware\n\n```typescript\nimport { addTagMiddleware } from '.pikku/pikku-types.gen.js'\n\naddTagMiddleware('machine-agent', [machineAgentBearerAuth])\n```\n\nCall at module load time — typically in the same `wirings/*.ts` file as the `wireHTTP` calls that use the tag.\n\n## Middleware Execution Order\n\n**Scope resolution order (broadest → narrowest):**\n\n```text\nglobal → httpGroup/* → httpGroup/prefix → wiringTags → wiringMiddleware → funcTags → funcMiddleware → function body\n```\n\n**Within each scope, sorted by priority:**\n\n```text\nhighest → high → medium (default) → low → lowest\n```\n\nSet priority using the config-object form of `pikkuMiddleware`:\n\n```typescript\nconst earlyMiddleware = pikkuMiddleware({\n name: 'early',\n priority: 'highest', // 'highest' | 'high' | 'medium' | 'low' | 'lowest'\n func: async (services, wire, next) => { ... },\n})\n```\n\nPriority is the primary sort key; within the same level, registration order is preserved. Use priority when a middleware must run before/after others regardless of registration order (e.g. telemetry wrapping everything, session extraction before auth checks).\n\n## Service-to-Service Bearer Auth (canonical pattern)\n\nA server that exposes RPCs only to a trusted caller (e.g. an API calling a machine-agent). Auth lives in a tag middleware — NOT in the function body. Authorization/permission checks belong in the `permissions` field (see `pikku-permissions`), never inside `func`.\n\n**On the server (the service being called):** tag the function, register a `pikkuMiddleware` that reads the `Authorization` header on that tag.\n\n```typescript\n// lib/host-token.ts\nlet _token: string | null = null\nexport const setToken = (t: string) => { _token = t }\nexport const getToken = () => _token\n```\n\n```typescript\n// wirings/http.wiring.ts\nimport { timingSafeEqual } from 'node:crypto'\nimport { addTagMiddleware, pikkuMiddleware } from '../../.pikku/pikku-types.gen.js'\nimport { UnauthorizedError } from '@pikku/core/errors'\nimport { getToken } from '../lib/host-token.js'\n\nconst bearerAuth = pikkuMiddleware(async (_services, { http }, next) => {\n const authHeader = http?.request?.header?.('authorization') || http?.request?.header?.('Authorization')\n const token = getToken()\n const expected = token ? `Bearer ${token}` : null\n if (\n !expected ||\n !authHeader ||\n authHeader.length !== expected.length ||\n !timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected))\n ) {\n throw new UnauthorizedError()\n }\n return next()\n})\n\naddTagMiddleware('machine-agent', [bearerAuth])\n```\n\n```typescript\n// functions/my.function.ts\nexport const myFunc = pikkuSessionlessFunc({\n expose: true,\n auth: false,\n tags: ['machine-agent'],\n func: async (services, input) => { ... },\n})\n```\n\n**On the client (the caller):** use the generated `RPCInvoke` type — never hand-write a `fetch` wrapper's types. See `references/middleware-patterns.md`.\n\n## More patterns\n\n`references/middleware-patterns.md` covers the client-side `RPCInvoke` caller, session-setting middleware (set a session from an API key), and request logging / audit middleware.\n\n## After Changes\n\n```bash\npikku all # regenerate metadata so new tags are picked up\npikku all --tsc # regenerate, then type-check (fails on type errors)\n```\n", "pikku-mongodb/SKILL.md": "---\nname: pikku-mongodb\ndescription: >-\n Use when setting up MongoDB database services in a Pikku app. Covers PikkuMongoDB connection,\n channel stores, workflow services, secret services, AI storage, agent runs, and deployment\n services. TRIGGER when: code uses PikkuMongoDB, MongoDBChannelStore, MongoDBWorkflowService,\n MongoDBSecretService, or user asks about MongoDB setup with Pikku. DO NOT TRIGGER when: user\n asks about SQL databases (use pikku-kysely) or Redis (use pikku-redis).\n---\n\n# Pikku MongoDB\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/mongodb` provides MongoDB-backed implementations of Pikku's core service interfaces.\n\n## Installation\n\n```bash\nyarn add @pikku/mongodb\n```\n\n## API Reference\n\n### `PikkuMongoDB` (Connection Wrapper)\n\n```typescript\nimport { PikkuMongoDB } from '@pikku/mongodb'\n\nconst mongo = new PikkuMongoDB(\n logger: Logger,\n clientOrUri: MongoClient | string,\n dbName: string,\n options?: MongoClientOptions\n)\n\nawait mongo.init()\nmongo.db // Db instance for queries\nawait mongo.close()\n```\n\n### Available Services\n\n| Service | Interface | Purpose |\n| --------------------------- | ------------------------------------- | ---------------------------------------------- |\n| `MongoDBChannelStore` | `ChannelStore` | WebSocket channel state persistence |\n| `MongoDBEventHubStore` | `EventHubStore` | Event hub state persistence |\n| `MongoDBWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |\n| `MongoDBWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |\n| `MongoDBDeploymentService` | `DeploymentService` | Deployment state management |\n| `MongoDBAIStorageService` | `AIStorageService, AIRunStateService` | AI conversation/run storage |\n| `MongoDBAgentRunService` | `AgentRunService` | Agent execution tracking |\n| `MongoDBSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |\n\nAll services take a `Db` instance in their constructor and have an `init()` method that creates collections/indexes.\n\n### Secret Service\n\n```typescript\nimport { MongoDBSecretService } from '@pikku/mongodb'\n\nconst secrets = new MongoDBSecretService(mongo.db, {\n kekSecret: 'your-key-encryption-key',\n salt: 'your-salt',\n})\nawait secrets.init()\n\nawait secrets.setSecret('api-key', { key: 'sk-...' })\nconst value = await secrets.getSecret<{ key: string }>('api-key')\nawait secrets.rotateKEK()\n```\n\n## Usage Patterns\n\n### Full Setup\n\n```typescript\nimport {\n PikkuMongoDB,\n MongoDBChannelStore,\n MongoDBWorkflowService,\n} from '@pikku/mongodb'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n const mongo = new PikkuMongoDB(logger, config.mongoUri, 'myapp')\n await mongo.init()\n\n const channelStore = new MongoDBChannelStore(mongo.db)\n await channelStore.init()\n\n const workflowService = new MongoDBWorkflowService(mongo.db)\n await workflowService.init()\n\n return { config, logger, database: mongo, channelStore, workflowService }\n})\n```\n", "pikku-n8n-import/references/addon-mapping.md": "# Integration stub → Pikku addon\n\nTranslate n8n integration nodes (`gmailTool`, `slackTool`, `googleSheetsTool`, plain\n`gmail` / `slack` action nodes, etc.) that the importer left as throwing stubs into\nreal `ref('<addonRpc>')` references pointing at functions in **installed**\n`@pikku/addon-*` packages.\n\nThis is **per-stub mechanical**. Do not invent business logic, chain calls, or\n\"improve\" the workflow. Read one manifest entry, find the matching addon function,\nrewrite the stub.\n\n## Inputs\n\n1. **Manifest** — `<workflow>.integrations.json` next to the `.graph.ts`. Each entry:\n ```jsonc\n {\n \"rpcName\": \"agentGmailtool__sendAMessageInGmail\",\n \"n8nType\": \"n8n-nodes-base.gmailTool\",\n \"n8nName\": \"Send a message in Gmail\",\n \"parameters\": { \"sendTo\": \"...\", \"message\": \"...\", \"subject\": \"...\" },\n \"credentials\": { \"gmailOAuth2\": { \"id\": \"...\", \"name\": \"Personal Gmail\" } },\n \"isAgentTool\": true,\n \"agentName\": \"Inbox Assistant\"\n }\n ```\n2. **Installed addons** — `@pikku/addon-*` in the project's `package.json`\n `dependencies`. Map **only** against installed packages. If the addon for an n8n\n type is not installed, surface it (see SKILL step 4) — never silently skip and\n never pick a vaguely-named function from another addon.\n\n## Per entry, in order\n\n### Step 1 — identify the target addon\n\nMap `n8nType` to a package by reading its source. Common shapes (**guesses, not\nauthoritative** — always verify against installed source):\n\n| n8n type prefix | typical addon candidate |\n|---|---|\n| `n8n-nodes-base.gmail` / `gmailTool` | `@pikku/addon-email-gmail` |\n| `n8n-nodes-base.slack` / `slackTool` | `@pikku/addon-chat-slack` |\n| `n8n-nodes-base.googleSheets` / `…Tool` | `@pikku/addon-sheets-google` |\n| `n8n-nodes-base.notion` / `notionTool` | `@pikku/addon-docs-notion` |\n| `n8n-nodes-base.telegram` / `telegramTool` | `@pikku/addon-chat-telegram` |\n\nIf no installed addon plausibly covers the n8n type, stop and report it — do not\npick a wrong addon.\n\n### Step 2 — pick the function (resource + operation → fn name)\n\nn8n nodes use a `(resource, operation)` pair. Rubric:\n\n- `resource` defaults to the integration's primary noun if absent (gmail →\n `message`, slack → `message`, sheets → `spreadsheet`). Read the addon's folder\n structure (`messages/`, `drafts/`, `channels/`) to see what nouns exist.\n- `operation` is usually a verb (`get`, `getAll`, `send`, `delete`, `addLabels`).\n- The pikku function name is almost always `<resource><Verb>` in camelCase, matching\n the file's `export const` (`messageList` for `messages/list.function.ts`).\n\nVerify: `grep -h \"^export const\" <addonPkg>/src/functions/**/*.ts` and match by name.\nConventions in `@pikku/addon-email-gmail` (a **sanity reference**, not a fallback):\n\n- `getAll → <resource>List`, `get → <resource>Get`, `send → <resource>Send`,\n `delete → <resource>Delete`, `reply → <resource>Reply`\n- `addLabels → <resource>AddLabel` (singular!), `removeLabels → <resource>RemoveLabel`\n- `markAsRead / markAsUnread → <resource>MarkRead / <resource>MarkUnread`\n- `create → <resource>Create`\n\nIf the function has a `node:` block, prefer matching its `category`/`displayName`\nover guessing.\n\n### Step 3 — rewrite the stub\n\nTwo outcomes, by `isAgentTool`:\n\n**A) `isAgentTool: true`** — the stub is an agent tool referenced via `ref()`:\n1. Delete the stub file.\n2. In the agent file, replace `ref('agentGmailtool__sendAMessageInGmail')` in\n `tools: [...]` with `ref('messageSend')` (the resolved addon function).\n3. Ensure the addon is where pikku scans functions (usually automatic via\n `node_modules/@pikku/addon-*`).\n\nIf you can't delete safely, leave a one-line re-export instead of a stub:\n```ts\nimport { messageSend } from '@pikku/addon-email-gmail'\nexport const agentGmailtool__sendAMessageInGmail = messageSend\n```\nDefault is delete + retarget; wrappers add maintenance burden.\n\n**B) `isAgentTool: false`** — the stub is a graph node:\n1. Open `<workflow>.graph.ts`.\n2. In `nodes: { … }` find the entry whose value is the stub rpc name.\n3. Replace it with the addon function name (`'messageSend'`).\n4. If `config: { <id>: { input } }` produces an `{ items }` envelope, rewrite it to\n the addon function's real input schema (read its `input: z.object({...})`).\n5. Delete the stub file.\n\n### Step 4 — port hard-coded parameters\n\n- **Hardcoded values** (`\"limit\": 20`, `\"labelIds\": [\"INBOX\"]`) were user choices.\n Preserve them in the graph node's `input` (case B). **Agent tools cannot carry\n hardcoded params** (the LLM fills args at call time) — surface the trade-off; if\n the user needs a value pinned, they keep a thin wrapper.\n- **`$fromAI('Name', '', 'string')`** placeholders are LLM-filled — the addon's Zod\n schema becomes the tool schema. Just drop the placeholder string; no other action.\n\n### Step 5 — credentials\n\n`credentials: { gmailOAuth2: { id, name } }` is the n8n credential ref. Pikku addons\nexpect a wired service (`services.gmail`). Do **not** auto-wire — leave a TODO:\n\n> `// TODO: wire services.gmail using credential \"Personal Gmail\" (n8n id: gmail_cred_1) — see @pikku/addon-email-gmail/README.md`\n\n## Never\n\n- Invent functions that don't exist — grep first.\n- Pick a wrong addon because the right one isn't installed — report `npm i @pikku/addon-<x>`.\n- Bake per-mapping tables into `@pikku/n8n-import` — it is addon-agnostic; mapping lives here.\n- Modify the manifest — it's an audit artifact.\n- Chain calls — each entry maps to exactly one addon function.\n- Silently drop a hardcoded param — surface it.\n", "pikku-n8n-import/references/code-translation.md": "# n8n Code node → Pikku function\n\nReplace a Code-node stub's `throw new Error(...)` body with a faithful TypeScript\nreimplementation. The original JS is preserved verbatim in the JSDoc above the\nfunction. Keep the signature and JSDoc intact; only widen the Zod input/output if\nthe code's data shape demands it.\n\n**Narrow, mechanical translation** — behavioral parity, not better code. Do not\nrefactor, add error handling, or invent fields.\n\n## Process\n\n1. **Read the file.** Identify the input schema, output schema, the verbatim JS in\n the JSDoc, and the `pikkuSessionlessFunc` shape.\n2. **Determine the n8n mode** from the original JSON if available (importer leaves it\n in `fixtures/`, or ask). Two modes:\n - `runOnceForAllItems` (default) — code runs once with `items: Array<{ json, binary, pairedItem }>`, returns an array of envelopes.\n - `runOnceForEachItem` — runs once per item, `$json` / `$input.item.json` in scope, returns a single envelope.\n - If unknown, infer: bare `items.X` → all-items; bare `$json.X` / `$input.item.X` → each-item.\n3. **Apply the rubric.**\n4. **Edit only the function body.** Leave imports, schemas, JSDoc, name, description, refs untouched unless step 5 forces it.\n5. **If the schemas are wrong** (code reads `$json.userId: string` but input is `items: z.array(z.unknown())`), tighten with the smallest change. Prefer `z.unknown()` over `z.any()`. Never widen output to `z.any()`.\n6. **Add one comment** at the top of the body noting the mode: `// translated from n8n Code node, mode: runOnceForAllItems`. This is the *only* comment you may add.\n7. **Typecheck** (`yarn tsc` from the package root); fix errors with the smallest change.\n\n## Rubric\n\n### Envelope unwrapping — all-items mode\n\n| n8n | Pikku |\n|---|---|\n| `items` | `(data.items ?? []) as any[]` (or typed if known) |\n| `items[i].json.X` | `items[i].X` |\n| `items[i].json` | `items[i]` |\n| `items[i].binary` | **NOT supported** — leave a TODO and explain |\n| `items.length` | `items.length` |\n| `items.map(i => i.json.X)` | `items.map((i: any) => i.X)` |\n\n### Envelope unwrapping — each-item mode\n\n| n8n | Pikku |\n|---|---|\n| `$json.X` / `$input.item.json.X` | `data.X` (input is the item itself) |\n| `$input.item.json` | `data` |\n| `$input.all()` | not available per-item — change to all-items mode |\n\n### Return statement\n\n| n8n | Pikku |\n|---|---|\n| `return [{ json: X }]` | `return { items: [X] }` |\n| `return items.map(i => ({ json: ... }))` | `return { items: items.map(...) }` |\n| `return [{ json: X }, { json: Y }]` | `return { items: [X, Y] }` |\n| `return { json: X }` (each-item) | `return X` |\n| `return [...]` (already plain) | wrap in `{ items: [...] }` only if the output schema expects it |\n\n### Built-ins — do NOT auto-translate\n\nIf the code references any of these, **stop**, leave the body a stub, and annotate\n`// TODO:` + explain:\n\n- `this.helpers.*` (binary buffers, HTTP requests, prepareBinaryData)\n- `$node['Some Node'].json` (cross-node refs — resolve via Pikku `ref()` upstream, not in the body)\n- `$workflow`, `$execution`, `$item()`, `$items('Other Node')`\n- `getBinaryDataBuffer` / `getStaticData` — no equivalent, TODO\n- `require()` / dynamic `import()` — flag and stop\n\nTranslate these only when reachable: `$now`/`$today` → `new Date()`; `$env.X` →\n`services.variables.get('X')` (never `process.env` — Pikku house rule; `services` is\nthe first param).\n\n### Async / types\n\n- Original uses `await` → the body is already `async`; keep every `await`.\n- `this.helpers.httpRequest(...)` → do NOT inline; the user should use a separate\n `httpRequest` rpc node. Leave a `// TODO:` and explain.\n- Cast `items` as `any[]` only if the schema is `z.array(z.unknown())`; use the\n inferred type if tightened. Never `as any` on the return — fix the schema instead.\n\n## Example\n\nBefore (stub):\n```ts\n/**\n * STUB — generated from n8n Code node \"Custom Code\".\n * const total = items.reduce((acc, i) => acc + i.json.amount, 0);\n * return [{ json: { total } }];\n */\nexport const codeStubCustomCode = pikkuSessionlessFunc({\n input: CodeStubCustomCodeInput,\n output: CodeStubCustomCodeOutput,\n func: async (_services, _data) => {\n throw new Error('Stub: ported from n8n Code node \"Custom Code\" — implement me')\n },\n})\n```\n\nAfter:\n```ts\nexport const codeStubCustomCode = pikkuSessionlessFunc({\n description: 'Ported from n8n Code node \"Custom Code\"',\n input: CodeStubCustomCodeInput,\n output: CodeStubCustomCodeOutput,\n func: async (_services, data) => {\n // translated from n8n Code node, mode: runOnceForAllItems\n const items = (data.items ?? []) as any[]\n const total = items.reduce((acc, i) => acc + i.amount, 0)\n return { items: [{ total }] }\n },\n})\n```\n\n## Report\n\nTerse: the mode you inferred (one sentence), the literal rubric translations\napplied, anything flagged TODO and why, any schema tightening (before → after).\n\nDo not add tests, refactor, edit other files, \"improve\" the logic, or add\ntry/catch unless the original did. If the code is empty, comment-only, or so\ndependent on n8n internals that no honest translation is possible, leave the stub\nand tell the user which n8n features block it.\n", "pikku-n8n-import/references/loops-and-control.md": "# Loops & control stubs\n\nThe importer maps the mechanical control flow (IF/Filter/Switch it can normalize →\n`graph:branch`) but leaves the **semantic** cases as `control` stubs — chiefly\n**Loop Over Items / splitInBatches** and Switch in expression mode. These need\njudgment, which is why they are not compiled. Read the loop body and the workflow\naround it; decide, or ask.\n\n## Loop Over Items / splitInBatches\n\nn8n's loop node has two outputs: **loop** (output 1, fires per batch) and **done**\n(output 0, fires once when iteration finishes). The loop body flows from the loop\noutput back into the node — a cycle. Pikku graphs are a DAG, so **the loop becomes a\n`graph:map`** (`@pikku/addon-graph`) and the back-edge disappears:\n\n```ts\ntheLoop: \"graph:map\", // (graph:fanout) — one child invocation per item\n// config:\ntheLoop: {\n input: (ref) => ({\n items: ref(\"<predecessor>\"), // what fed the loop\n child: \"<childRpc-or-subGraph>\", // the loop body\n childInput: { /* $item-rebound body input */ },\n stepPrefix: \"theLoop\",\n }),\n next: \"<done-branch target>\", // output 0\n}\n```\n\nInside `childInput`, references rebind to the current element: the body's `$json` /\npredecessor and any `$('<loop node>')` become `$item`.\n\n### Decide the shape first\n\n| Loop body does… | Emit |\n|---|---|\n| transform each item independently (enrich, format, call one thing) | `graph:map` — child = the body |\n| accumulate across items (running total, build one object/array) | a **reduce**: a single generated function over the whole array, not a map — `graph:map` collects per-item results and *loses* the accumulator |\n| pure side-effect per item, nothing downstream consumes results | `graph:map` with **no `next`** (done branch empty) — the safest, unambiguous case |\n\n### Child arity\n\n- **Single-node body** → `child: \"<that node's rpc>\"`, its input as `childInput`.\n- **Multi-node body** → the child must be a per-item **sub-graph**. Lift the body\n into its own `pikkuWorkflowGraph` (see `pikku-workflow`) and set `child` to that\n workflow's registered name. If the body references nodes **outside** the loop\n (not just the item), that value has to be threaded in as `childInput` — if you\n can't do it cleanly, stop and ask rather than emit something subtly wrong.\n\n### Done-branch semantics (ask if it matters)\n\nn8n's done output is version-dependent: it may carry the *original* items or the\n*accumulated* results. `graph:map`'s `next` receives the array of child results.\nIf a downstream node reads that array's shape and the distinction matters, add:\n\n```ts\n// TODO(n8n): done branch receives collected loop results (not original items) — confirm this matches intent\n```\n\nand call it out in your summary. When the done branch is empty, there's nothing to\ndecide.\n\n### batchSize > 1\n\n`graph:map` is one-item-at-a-time. A real numeric `batchSize` (chunk into groups,\nrun the body per chunk) has no direct primitive — leave the stub, and tell the user\nthis loop batches N-at-a-time and needs a manual pass (or a `graph:chunk` +\n`graph:map` composition if the body is chunk-shaped).\n\n## Switch / control stubs the importer couldn't normalize\n\nA Switch in **expression mode** (routing by an arbitrary JS expression rather than\ncomparable conditions) stays a `control` stub. Options, in order of preference:\n\n1. If the expression is really a set of value comparisons, rewrite the node as a\n `graph:branch` by hand (see `pikku-workflow` for the `branch` shape) and wire the\n emitted `next` keys to the branch targets.\n2. If it's genuinely computed routing, translate the stub into a small function that\n returns the branch key, then feed it a `graph:branch`.\n3. If neither is faithful, leave the stub and explain what the Switch does.\n\n## Never\n\n- Emit a `graph:map` for an accumulator loop — you'll silently drop the running state.\n- Guess the done-branch semantics when a downstream node depends on the shape — mark\n it and surface it.\n- Invent a batching primitive — say what's unsupported instead.\n", "pikku-n8n-import/SKILL.md": "---\nname: pikku-n8n-import\ndescription: 'Use to import an n8n workflow JSON export into a runnable Pikku workflow. Triggers when the user says \"import this n8n workflow\", \"convert this n8n export to pikku\", points at an n8n `.json` export or a directory of them, or picks up after `pikku import n8n` left throwing stub functions (`STUB — generated from n8n …`, `— implement me`) or a `<workflow>.integrations.json` manifest. Owns the whole flow: run the importer, triage what it could not map, fill each stub, report any missing `@pikku/addon-*` integrations, and verify the result compiles and runs with no surviving stubs. DO NOT TRIGGER for hand-written addon wiring unrelated to an n8n import (use pikku-addon), or for authoring workflows from scratch (use pikku-workflow).'\nmetadata:\n version: 1.0.0\n---\n\n# n8n → Pikku Import\n\nTake an n8n export all the way to a compiling, stub-free Pikku workflow. The\n`@pikku/n8n-import` package (invoked by `pikku import n8n`) is **frozen**: it does\nthe provable, mechanical conversion and leaves everything it cannot prove as a\ntyped stub that throws at runtime. This skill runs that package, then fills the\nremainder with judgment, reports what needs a human decision, and verifies.\n\nNever re-do what the importer already did, and never hand-edit generated files to\npaper over a stub — fix the source cause (the stub function, the graph node, or a\nmissing dependency).\n\n## Agent Operating Procedure\n\n1. Discover before editing. Prefer `pikku-meta`/`pikku meta ... --json` when\n available; inspect only the focused output you need.\n2. Identify the source file that owns the behavior. Do not start from generated\n output, `.pikku`, `node_modules`, or vendored packages.\n3. Make the smallest source change that satisfies the task. Keep generated files\n generated.\n4. Validate with the narrowest relevant command first, then `pikku all` /\n `pikku-verify` when functions, wirings, or schemas changed.\n5. If validation fails, fix the source cause and rerun. Never edit generated\n files to hide an error.\n\n## Workflow\n\n### 1 — Run the importer (do as much as possible, cheaply)\n\n```bash\npikku import n8n <export.json> [outDir]\n```\n\nIt writes `<slug>.graph.ts` (+ `.agent.ts` for AI workflows), `<slug>.addons.gen.ts`,\na `<slug>.integrations.json` manifest, and one stub function per node it could not\nmap. It **exits 1** on an un-importable input (a cross-workflow sub-workflow\nreference, a dynamic workflow target, a mid-flow `respondToWebhook`) with a\n`[reason] message` — relay that to the user; do not fake a partial scaffold.\n\nFor a directory of exports, run it per file.\n\n### 2 — Triage what it left\n\nEvery unmapped node is a stub that throws `… — implement me`. Classify each by its\nJSDoc marker and route to the matching reference:\n\n| Stub marker / signal | Handle via |\n|---|---|\n| `STUB — generated from n8n node \"…\" (type \"n8n-nodes-base.<svc>…\")` | `references/addon-mapping.md` |\n| `STUB — generated from n8n Code node \"…\"` | `references/code-translation.md` |\n| A `control` stub — Loop Over Items / **splitInBatches**, Switch expr-mode | `references/loops-and-control.md` |\n| `STUB — … vector-store … #902` | rare now (RAG ships as `<store>:query`/`:ingest`); a residual one = an unmapped store → report it, don't guess |\n| Importer `diagnostics` (already exited 1) | explain the reason; the workflow is un-importable as-is |\n\nRead a reference file only when you actually hit that stub class.\n\n### 3 — Fill each stub\n\nWork the manifest + stub files per the routed reference. The mechanical classes\n(addon, code) are near-deterministic; the loop/control class needs judgment\n(map vs reduce, done-branch semantics) — reference `loops-and-control.md` tells you\nwhen to decide vs ask.\n\n### 4 — Report missing integrations (first-class output)\n\nAn addon stub can only be wired to an **installed** `@pikku/addon-*`. When the\npackage for an n8n service is not in `dependencies`, do not guess a lookalike —\ncollect it. Give the user one upfront list:\n\n```\nMissing integrations — install these or the nodes stay stubs:\n • slackTool \"Post to channel\" → npm i @pikku/addon-chat-slack\n • hubspot \"Create contact\" → no @pikku/addon-hubspot exists yet\n```\n\n### 5 — Verify it works\n\n1. `pikku all` (regenerate) → `yarn tsc` from the package root; fix the source\n cause of any error and rerun.\n2. Grep the emitted functions for any surviving `— implement me`\n / `throw new Error('Stub:`. **Any survivor means the import is not done** —\n list them by node name.\n3. Green tsc **and** zero surviving stubs = success.\n\n## References\n\n| Open when you need to… | Read |\n|---|---|\n| map an integration stub (gmailTool, slackTool, googleSheets, plain action nodes) to an installed addon `ref(...)` | `references/addon-mapping.md` |\n| translate an n8n Code node body into a Pikku function body | `references/code-translation.md` |\n| lower a Loop Over Items / splitInBatches loop, or a Switch that stayed a stub | `references/loops-and-control.md` |\n\n## Final summary\n\nReport, terse:\n\n- Files written and workflow shape (`pure-graph` / `agent`).\n- Stubs filled, by class.\n- **Missing integrations** (the step-4 list) — the thing the user must act on.\n- Anything left as a `// TODO:` and why (credentials to wire, a loop deferred, an\n unmapped store).\n- Verification: `tsc` status + surviving-stub count (must be 0).\n", "pikku-n8n-import/SPEC.md": "# n8n → Pikku Import Specification\n\n## Intent\n\nTake an n8n workflow JSON export all the way to a compiling, stub-free, runnable\nPikku workflow. The `@pikku/n8n-import` package is treated as **frozen**: it does\nthe provable mechanical conversion and leaves everything it cannot prove as a typed,\nthrowing stub. This skill owns the end-to-end flow around it — run it, fill the\nremainder with judgment, report gaps that need a human decision, and verify.\n\n## Scope\n\nIn scope:\n- Running `pikku import n8n` and triaging its output.\n- Filling integration stubs (→ addon refs), Code stubs (→ function bodies), and\n loop/control stubs (→ `graph:map`/reduce/branch).\n- Reporting missing `@pikku/addon-*` integrations as a first-class output.\n- Verifying via `pikku all` + `tsc` + a zero-surviving-stub check.\n\nOut of scope:\n- Extending `@pikku/n8n-import` itself (it is frozen; do not add per-service tables\n or new compiler rules to it).\n- Authoring workflows from scratch (`pikku-workflow`) or hand-written addon wiring\n unrelated to an import (`pikku-addon`).\n- Inventing batching primitives or guessing ambiguous loop semantics — surface them.\n\n## Users And Trigger Context\n\n- Primary users: developers importing their own n8n workflows into a Pikku app,\n usually inside an agentic session.\n- Common requests: \"import this n8n workflow\", \"convert this n8n export to pikku\",\n finishing `— implement me` stubs, wiring a `*.integrations.json` manifest.\n- Should not trigger for: from-scratch workflow authoring, or addon wiring with no\n n8n import involved.\n\n## Runtime Contract\n\n- Required first action: run `pikku import n8n <export.json> [outDir]` (per file for\n a directory); relay any exit-1 diagnostic instead of scaffolding a partial.\n- Required outputs: filled stubs, a missing-integrations list, verification status.\n- Non-negotiable: never hand-edit generated files to hide a stub; map only to\n installed addons; zero surviving `— implement me` stubs at success.\n- Bundled files loaded at runtime: `references/addon-mapping.md`,\n `references/code-translation.md`, `references/loops-and-control.md` — each only\n when its stub class appears.\n\n## Source And Evidence Model\n\nAuthoritative sources:\n- `@pikku/n8n-import` codegen (stub markers, manifest shape, `import-n8n` command).\n- `@pikku/addon-graph` function contracts (`graph:map`/`fanout`, `branch`).\n- Installed `@pikku/addon-*` source (function names verified by grep, never guessed).\n\nUseful improvement sources: real imported workflows, harness coverage deltas,\naddon catalogue changes.\n\nData that must not be stored: credential secrets, customer data, private ids beyond\nwhat a manifest already records for reproduction.\n\n## Reference Architecture\n\n- `SKILL.md`: the run → triage → fill → report → verify workflow + router.\n- `references/`: per-stub-class depth (addon mapping, code translation, loops/control).\n\n## Validation\n\n- Lightweight: `yarn tsc` from the package root after each fill.\n- Deeper: `pikku all` regeneration; grep emitted functions for surviving stub throws.\n- Acceptance gates: green tsc **and** zero surviving `— implement me` stubs.\n\n## Known Limitations\n\n- `splitInBatches` with `batchSize > 1` and reduce-style accumulators have no direct\n primitive — surfaced, not auto-converted.\n- Missing addons block their nodes; the skill reports, it does not install.\n- Cross-workflow sub-workflow references fail import at the package level.\n\n## Maintenance Notes\n\n- Update `SKILL.md` when the import command, stub taxonomy, or verify gates change.\n- Update a reference when an addon convention, the `graph:map` contract, or a rubric\n changes.\n- This skill supersedes the former `pikku-n8n-addon-map` and `pikku-n8n-code-translate`\n skills (folded into `references/addon-mapping.md` and `references/code-translation.md`).\n", "pikku-paraglide/SKILL.md": "---\nname: pikku-paraglide\ndescription: 'Generate typed, static enum-label maps for a Paraglide i18n frontend with `@pikku/paraglide`, and reconcile them against the database enum columns so a label can never silently drift from a DB value. Enum-valued labels live under a reserved `enum__<group>__<member>` message namespace; the generator emits `i18n-enum.gen.ts` typed `satisfies EnumLabel<DbEnum>`. TRIGGER when: labelling an enum/status/kind/role value in a Paraglide app, replacing a dynamic `mKey(...)`/`m[...]` lookup with a static map, wiring `@pikku/paraglide` into Vite, or reconciling i18n against `CHECK (col IN (...))` / Postgres enum columns. DO NOT TRIGGER for plain free-text UI copy (that is a normal `m.some_key()` message), backend errors, or logs.'\ninstallGroups: [core]\n---\n\n# Pikku Paraglide enum labels\n\n## Agent Operating Procedure\n\nUse this as an execution checklist, not reference material.\n\n1. **Is the value an enum (a closed set — a status/kind/role/tag from a DB column or a fixed union)?** Then its label is a static map entry, never a dynamic lookup. Add `enum__<group>__<member>` keys to the catalog (`messages/en.json`) and read the value through the generated map: `group[value]()`.\n2. **Is the value free text or a one-off literal** (a heading, a button, a never-indexed label)? Then it is a normal Paraglide message — call `m.<key>()` directly. Do **not** invent an enum group for something that is always a literal.\n3. **Wire the generator** (`@pikku/paraglide/vite` or the CLI) so `i18n-enum.gen.ts` is regenerated from the catalog, and point it at the DB enums module (`enums.gen.ts`) so the maps are typed against the database.\n4. **Validate with the app's own `tsc`.** Reconciliation is enforced purely by types: a missing key or a dropped DB member is a compile error, and the deploy gate runs `tsc` before building. A clean `vite build` alone does not type-check.\n\n## The rules that don't change\n\n- **Never resolve an enum key dynamically.** No `mKey('status.' + value)`, no `m['enum__status__' + value]()`, no `mExists`/`mList` helpers. Dynamic keys can't be type-checked or tree-shaken. Everything is a static `m.<literal>()` reference, generated into the map.\n- **The `enum__<group>__<member>` namespace.** `__` separates the prefix / group / member segments; a single `_` joins words *within* a segment (`enum__booking_status__form_received`). The prefix (`enum`) and separator (`__`) are configurable but leave them at the defaults.\n- **Members must be valid JS identifiers.** Spell out leading digits — `two_guests`, not `2_guests`. The generator quotes an invalid member as a fallback but warns you to rename it.\n- **`asI18n(...)` is only for opaque server data** (names, slugs, ids returned from the API). Never `asI18n()` a hardcoded English string or an enum value — an enum value goes through its label map.\n\n## The generated module\n\n`i18n-enum.gen.ts` is **AUTO-GENERATED — do not edit.** It exports, per enum group:\n\n```ts\nimport { m } from './messages.js'\nimport type { I18nString } from '@pikku/react'\nimport type { BookingStatus } from '#pikku/db/enums.gen' // when reconciled\n\nexport type I18nMessage = () => I18nString\nexport type EnumLabel<E extends string> = Record<E, I18nMessage>\n\nexport const bookingStatus = {\n enquiry: m.enum__booking_status__enquiry,\n reserved: m.enum__booking_status__reserved,\n confirmed: m.enum__booking_status__confirmed,\n} satisfies EnumLabel<BookingStatus>\nexport type BookingStatusKey = keyof typeof bookingStatus\n```\n\n- Each value is an `I18nMessage` — a `() => I18nString` accessor. **Call it at render time** so the label tracks the active locale.\n- App code: `import { bookingStatus } from './i18n/i18n-enum.gen'` then `bookingStatus[value]()`.\n- For an open server value, gate it: `value in bookingStatus ? bookingStatus[value as BookingStatusKey]() : asI18n(value)`.\n\n### Module-scope hazard — store the accessor, don't call it\n\nA label used in a config built at module load (nav items, column defs) must hold the **accessor**, not the result — calling `m.foo()` at module scope freezes the label to the locale that was active at import:\n\n```ts\n// nav.config.ts\nconst items = [{ label: m.common__nav__items__dashboard /* ← reference */ }]\n// at render: <span>{item.label()}</span> // ← call here\n```\n\n## Reconciliation against the database\n\nThe DB column is the real source of truth for what an enum can be. The pikku CLI's db codegen emits a bare unions module — `.pikku/db/enums.gen.ts` — covering **both** Postgres native enums and SQLite `CHECK (col IN ('a','b',…))` constraints:\n\n```ts\nexport type BookingStatus = 'enquiry' | 'reserved' | 'confirmed' | 'ended' | 'cancelled'\n```\n\nPoint `@pikku/paraglide` at that file (`enumsFile`) and each catalog group whose member set **exactly matches** a DB enum is typed `satisfies EnumLabel<DbEnum>`. The label map then **is** the reconciliation — no separate assertion:\n\n- catalog drops a DB member, or `en.json` is missing the key → `m.enum__…` doesn't exist / `Record<DbEnum,…>` isn't exhaustive → **`tsc` error naming the gap**.\n- a DB enum with **no** catalog group → `unmatchedDbEnums: 'emit'` (default) generates a label map referencing `enum__<table>_<column>__<member>` keys, so `tsc` tells you exactly which keys to add; `'warn'` only reports it.\n- a group with a member the DB lacks (a *derived* UI state, e.g. a `waitlisted` view of a `pending` row) → a drift warning. Make that a **standalone `m.<key>()` message**, not an enum member — the enum group must mirror the DB column exactly.\n\nLabelling an enum that's never rendered costs nothing: Paraglide compiles only the messages actually referenced, so unused labels are tree-shaken away. So label every DB enum; don't add an opt-out.\n\n**To make a column an enum**, give it a closed domain in the migration so codegen can see it:\n- SQLite: `status TEXT NOT NULL CHECK (status IN ('enquiry','reserved','confirmed'))`\n- Postgres: a native `CREATE TYPE … AS ENUM (…)` column.\n\n## Wiring\n\n### Vite (dev + build)\n\nPlace `paraglideEnums` **after** `paraglideVitePlugin` (the generated file imports the compiled `m`). It regenerates on catalog/enums edits and only writes on change, so it never loops HMR.\n\n```ts\nimport { paraglideVitePlugin } from '@inlang/paraglide-js'\nimport { paraglideEnums } from '@pikku/paraglide/vite'\n\nexport default defineConfig({\n plugins: [\n paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide' }),\n paraglideEnums({\n catalog: './messages/en.json',\n outFile: './src/i18n/i18n-enum.gen.ts',\n enumsFile: './packages/functions/.pikku/db/enums.gen.ts', // reconcile against the DB\n // enumsImport: '#pikku/db/enums.gen', // explicit specifier; defaults to a relative path\n }),\n ],\n})\n```\n\n### CLI (CI / non-Vite), run right after `paraglide-js compile`\n\n```sh\n# paraglide-enums <catalog.json> <out.gen.ts> [messagesImport] [enums.gen.ts]\nparaglide-enums ./messages/en.json ./src/i18n/i18n-enum.gen.ts ./messages.js ./packages/functions/.pikku/db/enums.gen.ts\n```\n\n`i18n-enum.gen.ts` is generated — gitignore it once the plugin/CLI runs in the build.\n\n## What NOT to do\n\n- Don't write `mKey`/`mList`/`mExists` or any `m[expr]()` dynamic lookup — every enum label is a static generated reference.\n- Don't introduce a literal-key indirection helper (`k('approve_enquiry')`); a literal is `m.approve_enquiry()` directly.\n- Don't call `m.foo()` at module scope for config built at import time — store `m.foo` and call it at render.\n- Don't put an extra UI-only member into an enum group to match a derived state — make it a standalone message and keep the group an exact mirror of the DB column.\n- Don't hand-edit `i18n-enum.gen.ts` or `enums.gen.ts` — fix the catalog / the migration and regenerate.\n", "pikku-permissions/SKILL.md": "---\nname: pikku-permissions\ndescription: >-\n Use when adding authorization checks to Pikku functions — pikkuPermission, pikkuAuth,\n per-function permissions, global permissions, or understanding OR/AND permission logic.\n TRIGGER when: user wants to restrict who can call a function, check resource ownership, add\n role-based access, or understand where permission checks belong. DO NOT TRIGGER when: user asks\n about middleware or request interception (use pikku-middleware), authentication strategies (use\n pikku-security), or session management.\ninstallGroups: [core]\n---\n\n# Pikku Permissions\n\n## The Rule\n\n**ALWAYS put authorization checks in the `permissions` field of `pikkuFunc` or `pikkuSessionlessFunc` — NEVER inside the `func` body.**\n\nThis includes: org access checks, repo access checks, role checks, resource ownership, and any other authorization logic. The `permissions` field runs before `func`, is visible to the inspector, and is the only place Pikku enforces authorization.\n\n```typescript\n// CORRECT\nexport const deleteBook = pikkuFunc({\n func: async ({ db }, { bookId }) => {\n await db.deleteBook(bookId)\n },\n permissions: {\n owner: isBookOwner, // ← authorization here\n },\n})\n\n// WRONG — permission check inside func body\nexport const deleteBook = pikkuFunc({\n func: async ({ db }, { bookId }, { session }) => {\n if (!session) throw new UnauthorizedError() // ← never do this\n await db.deleteBook(bookId)\n },\n})\n```\n\n## Agent Operating Procedure\n\n1. Discover before editing. Run `pikku info permissions --verbose` and `pikku info functions --verbose` to understand what permissions are already defined and applied.\n2. Define permission checkers in a `src/permissions.ts` or domain-specific `src/lib/*-permissions.ts` file.\n3. Apply them via the `permissions` field on the function. For an app-wide baseline that every function must additionally satisfy, use `addGlobalPermission`.\n4. Validate: run `pikku all --tsc` to confirm permission checker signatures are correct.\n\n## Permission Factories\n\n### `pikkuAuth(fn)` — Session-Only Checks\n\nUse for checks that read the session but need no request data — and that assert\nsomething **beyond** merely having a session (a flag, a tier, a claim).\n\n```typescript\nimport { pikkuAuth } from '#pikku'\n\n// Good: a real gate on the session's contents, not just its existence.\nexport const isVerified = pikkuAuth(\n async (_services, session) => !!session?.emailVerified\n)\n```\n\n**Do NOT write an \"is signed in\" permission.** A checker that just returns\n`!!session` is not authorization — it re-checks authentication, which the\nfunction already enforces. A function that needs a signed-in user sets\n`auth: true` (the default for `pikkuFunc`); it does not also carry a\n`permissions: { signedIn }`.\n\n```typescript\n// WRONG — redundant with auth: true; adds a permission that gates nothing.\nexport const isSignedIn = pikkuAuth(async (_s, session) => !!session)\npikkuFunc({ auth: true, permissions: { signedIn: isSignedIn }, /* ... */ })\n\n// RIGHT — auth: true already requires the session; permissions are for capability.\npikkuFunc({ auth: true, /* ... */ })\n```\n\nA permission answers \"*may this user do this?*\" (role, ownership, tier) — never\n\"*is there a session?*\".\n\n### `pikkuPermission(fn)` — Data-Aware Checks\n\nUse when authorization depends on the actual request data (e.g., resource ownership).\n\n```typescript\nimport { pikkuPermission } from '#pikku'\n\nexport const isBookOwner = pikkuPermission(\n async ({ db }, { bookId }, { session }) => {\n const book = await db.getBook(bookId)\n return book?.authorId === session?.userId\n }\n)\n\nexport const hasBookAccess = pikkuPermission(\n async ({ db }, { bookId }, { session }) => {\n return await db.hasAccess(session?.userId, bookId)\n }\n)\n```\n\n## OR / AND Logic\n\n```typescript\npermissions: {\n verified: isVerified, // OR: verified users can access\n owner: isBookOwner, // OR: owners can access\n reviewer: [isAuthenticated, hasBookAccess], // AND: both must pass\n}\n// Logic: verified OR owner OR (isAuthenticated AND hasBookAccess)\n```\n\nGroups are OR'd. Entries within a group array are AND'd.\n\n## Where to Apply Permissions\n\n### Per-Function (preferred)\n\n```typescript\nexport const deleteBook = pikkuFunc({\n func: async ({ db }, { bookId }) => {\n await db.deleteBook(bookId)\n },\n permissions: {\n verified: isVerified,\n owner: isBookOwner,\n },\n})\n```\n\n### Global (`addGlobalPermission`) — App-Wide AND Gate\n\nA global permission is an app-wide baseline that **every** function must additionally pass. It is an independent AND gate: it can only ever *narrow* access — it never grants access a function's own `permissions` would deny.\n\n```typescript\nimport { addGlobalPermission } from '.pikku/pikku-types.gen.js'\n\naddGlobalPermission([signedInUser]) // every function now also requires a session\n```\n\nMultiple `addGlobalPermission` calls accumulate and are AND'd together.\n\n> Wire-, tag-, and HTTP-route-level permissions (`addHTTPPermission`, `addTagPermission`, and a `permissions` field on HTTP/channel/MCP wirings) were **removed in #972**. Permissions now live only on the function definition, plus the optional global gate. Tags are organizational only — use tag/HTTP *middleware* (`addTagMiddleware`, `addHTTPMiddleware`) for cross-cutting request handling, not authorization.\n\n## The Two Gates\n\nAuthorization is two independent gates, both of which must pass:\n\n1. **Global permissions** (`addGlobalPermission`) — AND'd together. A broad baseline that can only narrow access.\n2. **The function's own `permissions`** — OR'd groups (OR-of-ANDs), as above.\n\nThe gates are independent: a broad global (e.g. `signedInUser`) can **never** satisfy an admin-only function's own requirement. Each function still enforces its own `permissions` in full.\n\n## Complete Example\n\n```typescript\n// src/permissions.ts\nimport { pikkuAuth, pikkuPermission } from '#pikku'\n\nexport const isAuthenticated = pikkuAuth(\n async (_services, session) => !!session\n)\n\nexport const isVerified = pikkuAuth(\n async (_services, session) => !!session?.emailVerified\n)\n\nexport const isOrgMember = pikkuPermission(\n async ({ db }, { orgId }, { session }) => {\n return await db.isMember(session?.userId, orgId)\n }\n)\n\n// src/functions/org.function.ts\nexport const deleteOrg = pikkuFunc({\n func: async ({ db }, { orgId }) => {\n await db.deleteOrg(orgId)\n },\n permissions: {\n verified: isVerified,\n owner: [isAuthenticated, isOrgMember],\n },\n})\n```\n\n## After Changes\n\n```bash\npikku all # regenerate if wirings changed\npikku all --tsc # regenerate, then verify permission checker types (fails on type errors)\n```\n", "pikku-pino/SKILL.md": "---\nname: pikku-pino\ndescription: >-\n Use when setting up structured logging with Pino in a Pikku app. Covers PinoLogger setup and log\n levels. TRIGGER when: code uses PinoLogger, user asks about structured logging, Pino, or\n @pikku/pino. DO NOT TRIGGER when: user asks about ConsoleLogger (use pikku-services) or general\n service setup.\n---\n\n# Pikku Pino (Structured Logging)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/pino` provides structured JSON logging via [Pino](https://getpino.io/). Implements the `Logger` interface from `@pikku/core`.\n\n## Installation\n\n```bash\nyarn add @pikku/pino\n```\n\n## API Reference\n\n### `PinoLogger`\n\n```typescript\nimport { PinoLogger } from '@pikku/pino'\n\nconst logger = new PinoLogger()\n```\n\nNo constructor parameters. Creates a Pino logger instance.\n\n**Properties:**\n\n- `pino: pino.Logger` — Access the underlying Pino instance for advanced config.\n\n**Methods:**\n\n- `setLevel(level: LogLevel): void` — Set minimum log level.\n- `info(messageOrObj: string | Record<string, any> | Error): void`\n- `warn(messageOrObj: string | Record<string, any> | Error): void`\n- `error(messageOrObj: string | Record<string, any> | Error): void`\n- `debug(messageOrObj: string | Record<string, any>): void`\n\n## Usage Patterns\n\n### Basic Setup\n\n```typescript\nimport { PinoLogger } from '@pikku/pino'\n\nconst logger = new PinoLogger()\nlogger.setLevel('debug')\n```\n\n### With Pikku Services\n\n```typescript\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n return { config, logger }\n})\n```\n\n### Accessing Underlying Pino\n\n```typescript\nconst logger = new PinoLogger()\nlogger.pino.child({ module: 'auth' }).info('Token verified')\n```\n", "pikku-product-second-opinion/example/sample-report.md": "# Your app, in plain English — and where it could get better\n*A second opinion on the competitor-tracking system*\n\n> Worked example for the pikku-product-second-opinion skill. Shows the voice and the\n> layered structure on one real area (competitor tracking), drawn from a\n> pikku-software-archaeology blueprint + parity report. A full report would repeat\n> Part 2 for each major area, and cover every significant technology bet — not just\n> the one shown here.\n\n**How to read this:** no technical background needed. I'll explain what you have,\nwhat's solid, and what I'd change — and for each change, what it costs and what\nit buys you.\n\n## Part 1 — The short version\n\n**What you have.** A competitive-intelligence app: it watches your competitors'\nwebsites, spots meaningful changes (pricing, hiring, product updates), summarizes\nthem, and feeds your briefings and dashboards so your team knows first.\n\n**The headline.**\n- The hard part — reading messy websites and telling a real change from noise — is built well.\n- Until recently the app wasn't re-checking sites on its own at all *(now fixed)*.\n- When it does spot a change, the follow-up work only happens if someone clicks a button — so your dashboards can quietly go stale while looking current.\n\n**If it were me, this is the order I'd tackle things:**\n\n| Fix | Why it matters to you | Effort | Payoff |\n|---|---|---|---|\n| Turn on automatic checking | Sites weren't refreshing themselves | *Done* | High |\n| Make the follow-up automatic | Stops your intelligence going stale unnoticed | Medium | High |\n| Make failures visible | Problems surface instead of hiding | Small | Medium |\n\n---\n\n## Part 2 — Area by area\n\n### Competitor tracking\n\n**What this does.** Watches your competitors' sites for you and turns meaningful\nchanges into summaries your team can act on.\n\n**How it works today.** Like a clipping service: on a timer, the app re-reads\neach competitor's site, compares it to last time, decides whether anything\n*meaningful* changed (it ignores trivial edits), and writes up a summary when\nsomething real happens.\n\n**What's working.** The expensive, valuable part is solid — the app is genuinely\ngood at reading messy sites, separating real changes from noise, and summarizing\nthem. Keep it.\n\n**What's holding you back.**\n- **The automatic checking wasn't switched on.** The machinery existed but nothing\n pulled the trigger, so sites weren't refreshing on their own. What it means for\n you: your \"live\" intelligence wasn't live. Severity: Urgent. Effort: Small.\n *(Already fixed.)*\n- **The follow-up is manual.** When a change is found, updating your briefings and\n comparisons doesn't happen on its own — someone has to click \"regenerate.\" What\n it means for you: if nobody clicks, the dashboard shows old information while\n looking up to date, and you can't trust it. Severity: Serious. Effort: Medium.\n\n**How I'd do it differently — and why it's worth it.** Make the whole chain\nfinish as one task: when a change is found, the briefings and comparisons update\nautomatically as part of the same job, so \"done\" means \"your intelligence is\nactually current.\" And a failed step should show as a visible error, not vanish.\nThis is a **rewire, not a rebuild** — the valuable machinery stays; I'm connecting\npieces that already sit next to each other. Outcome: **more reliable, fewer\nsurprises** — which is the entire promise of the product.\n\n---\n\n### The technology bets\n\n**Pikku — the framework I'm suggesting you rebuild onto.** *Buys you:* one way to\nwrite a capability and drive it from anywhere — web, timers, background jobs,\nassistants — so the tracking rule is written once instead of three times, which is\nexactly the sprawl above. *Costs you:* it hasn't shipped a stable 1.0 (it's 0.12.x;\n0.13 is the first release promising backwards compatibility), so until then\nupgrades can break you — pin the version and budget for upgrade work. Its community\nand hiring pool are far smaller than the mainstream default's. That's normal for a\nyoung framework and survivable, but it's a real cost and it's yours to weigh.\n*Usually:* worth it when the problem is genuinely sprawl, as it is here — and worth\nwaiting if nobody has capacity to own upgrades.\n\n---\n\n## Part 3 — The fine print\n\n**How confident am I.** High on both problems — I can see them directly. The fix\nis a well-understood pattern rather than an experiment, though I'd want one live\ntest against your real data before calling it done.\n", "pikku-product-second-opinion/README.md": "# pikku-product-second-opinion\n\nTurns a `pikku-software-archaeology` blueprint into a **plain-language report for a\nnon-technical owner** — a founder/PM stuck with an app they didn't build.\nExplains how it works and how it could be better, in business terms.\n\n```\nExisting repo → pikku-software-archaeology → .knowledge/ blueprint → pikku-product-second-opinion → founder report\n (facts) (extract) (machine-readable) (translate + advise) (markdown + web page)\n```\n\n## The split from pikku-software-archaeology\n\n- **pikku-software-archaeology** extracts *facts* into `.knowledge/` for a machine (Pikku) to rebuild from. Engineer/generator audience.\n- **pikku-product-second-opinion** reads that blueprint and writes an *opinionated report* for a human to decide from. Non-technical audience.\n\nOne extracts; one advises. This skill consumes the other's output — it doesn't re-read the code.\n\n## What the report is calibrated to (locked by the author)\n\n- **Layered depth** — a one-page executive summary, then a section per major area for anyone who wants detail.\n- **Direct but fair tone** — names problems plainly, always with why-it-matters and credit for what's good.\n- **Both formats** — a markdown copy in the repo plus a clean, shareable web page (rendered via the `artifact-design` skill).\n\n## The rules that make it work\n\n1. **Translate, don't dump.** Every technical concept becomes a business outcome or a plain description. The jargon→plain table is in `SKILL.md`.\n2. **Every problem carries impact + severity + effort.** A problem with no \"what it means for you\" doesn't ship.\n3. **Always credit what works.** All-criticism reports get dismissed.\n4. **Argue improvements in business outcomes** (more reliable / faster / cheaper / safer / easier to hand off), and say whether each is a cheap **rewire** or an expensive **rebuild** — never recommend a rewrite just because the code is messy.\n5. **Mark confidence.** Certain and \"I'd need to check\" are different sentences.\n6. **Cover the frontend and the other ways the app is used** when the blueprint has them — walk the screens as a journey, call out consistency, and flag the custom-logic pieces (charts/tables/editors) as the real work vs the cheap standard pieces. Name the ways the product can be driven (people/web, developers/API+SDK, AI agents/MCP, power users/CLI) — often a genuine strength.\n7. **Give honest technology tradeoffs — both sides.** Every stack bet (framework, auth, hosting, key libraries) gets what-it-buys AND what-it-costs in business terms, tied to the founder's stage/goals. Don't cheerlead, don't trash, and **don't soften the disadvantages**. The app's own bets are derived from the blueprint (`architecture.json`/`integrations.json`/`frontend.json` + the repo's manifest) — never from a list in the skill, because a verdict you could write before reading the blueprint isn't a second opinion. Separately, and only when a rebuild is actually being recommended, the target stack (Pikku, Better Auth, TanStack Start) gets the *same* both-sides treatment with cons first-class — pinning someone's dependency for being pre-1.0 while staying quiet about the replacement being pre-1.0 too is a pitch, not an opinion.\n\n## Files\n\n```\npikku-product-second-opinion/\n├── SKILL.md # method + voice rules + red flags\n├── README.md # this file\n├── references/report-template.md # the layered structure to fill in\n└── example/sample-report.md # worked example (competitor-tracking area, founder voice)\n```\n", "pikku-product-second-opinion/references/report-template.md": "# Report template\n\nFill this in. Keep sentences short. Lead every point with what it means for the\nreader. Delete any section that would be empty rather than padding it.\n\n---\n\n# {App name}, in plain English — and where it could get better\n*A second opinion on {scope: the whole app / the competitor-tracking system / …}*\n\n**How to read this:** no technical background needed. Part 1 is the summary — if\nyou read nothing else, read that. Parts 2–3 go area by area for anyone who wants\ndetail.\n\n## Part 1 — The short version\n\n**What you have.** {2–3 sentences: what the product does, who uses it.}\n\n**The headline.** {3–5 bullets, one plain line each — the biggest risks and\nopportunities. No jargon.}\n\n**If it were me, this is the order I'd tackle things:**\n\n| Fix | Why it matters to you | Effort | Payoff |\n|---|---|---|---|\n| {…} | {business impact} | Small/Medium/Large | High/Medium/Low |\n\n---\n\n## Part 2 — Area by area\n\n### {Area name, in business terms — e.g. \"Competitor tracking\"}\n\n**What this does.** {the capability, as the business experiences it}\n\n**How it works today.** {a plain walkthrough — a small story beats a diagram}\n\n**What's working.** {genuine credit — the parts that are solid and worth keeping}\n\n**What's holding you back.**\n- **{Problem in plain terms}.** What it means for you: {business impact}.\n Severity: {Minor / Worth fixing / Serious / Urgent}. Effort to fix: {Small /\n Medium / Large}.\n\n**How I'd do it differently — and why it's worth it.** {the better design in\noutcomes: more reliable / faster to change / cheaper / safer / easier to hand\noff. Say whether it's a cheap rewire or an expensive rebuild.}\n\n{repeat per area}\n\n### The technology bets\n\n{One entry per significant choice — framework, sign-in, hosting, database, key\nlibraries — AND anything a rebuild would move them ONTO. Each gets both sides.}\n\n**{Technology}.**\n- *Buys you:* {in business terms}\n- *Costs you:* {in business terms — bills, hiring, shipping speed, upgrade work,\n the risk of betting on something young. Don't soften it. If it hasn't shipped a\n stable 1.0, say so and say what that means: pin the version, budget upgrades.}\n- *Usually:* {recommendation tied to their stage — normally \"keep it, watch this\"}\n\n{The same bar applies to anything you're recommending they move to. A stack you\npropose with no cons listed is a pitch, not a second opinion.}\n\n---\n\n## Part 3 — The fine print (optional)\n\n**How confident am I.** {where you're certain vs guessing; what you'd verify\nagainst real data before committing}\n\n**A few words explained.** {glossary — only terms that couldn't be avoided}\n", "pikku-product-second-opinion/SKILL.md": "---\nname: pikku-product-second-opinion\ndescription: 'Use when a non-technical owner (founder, PM, operator) wants a plain-language report on an app they hold but did not build — explaining how it works and how it could be better. Reads the .knowledge/ blueprint from pikku-software-archaeology and produces a layered, jargon-free report that credits what works, names what does not (with business impact + effort), and argues an opinionated better design. TRIGGER: \"explain how my app works\", \"what would you do differently\", \"review my app for a non-technical audience\", \"I inherited/am stuck with an agency-built app\", \"is this built well?\". DO NOT TRIGGER for: extracting the machine-readable blueprint itself (use pikku-software-archaeology), or an engineer-facing technical code review.'\ninstallGroups: [fabric]\n---\n\n# Product Second Opinion\n\n## Overview\n\nTurn an extracted product blueprint into a **report a non-technical owner can act on**. Two jobs, in one voice: (1) explain, in plain language, how the app they're stuck with actually works; (2) give an honest, opinionated second opinion — what's solid, what's holding them back, and how you'd build it better, argued in business outcomes, not architecture.\n\nThe reader is a founder/PM/operator, not an engineer. If they finish a section and don't know what it means for their business or what to do about it, the report failed — no matter how correct it is.\n\n**REQUIRED INPUT:** the `.knowledge/` blueprint produced by **pikku-software-archaeology**. If none exists, run that skill first — this one consumes its output (`product.json`, `domains.json`, `workflows.json`, `gaps.json`, `invariants.json`, `migration.json`, and any `parity-*.md`), it does not re-derive facts from the code. When the optional consumer-surface files are present (`interfaces.json`, `frontend.json`, `frontend-routes.json`, `frontend-components.json`), cover them too — see \"The frontend and the other ways your app is used\" and \"Technology choices\" below.\n\n## The cardinal rule: translate, don't dump\n\nEvery technical concept becomes a business outcome or a plain-language description. Never make the reader learn your vocabulary. If a term is unavoidable, define it in one clause the first time — but prefer describing the *effect* and skipping the term entirely.\n\n| Don't write | Write instead (describe the effect) |\n|---|---|\n| queue / worker / job | \"a background task that runs on its own\" |\n| workflow | \"a multi-step task that resumes where it left off if interrupted\" |\n| API / endpoint / route | \"something the app (or another tool) can ask it to do\" |\n| webhook | \"an automatic message the app sends to another tool when something happens\" |\n| event | \"a signal that something happened, that other parts can react to\" |\n| cron / scheduler | \"a timer that runs something on a schedule\" |\n| schema / migration | \"the shape of your stored data\" / \"a change to how data is stored\" |\n| auth / session / token | \"how the app knows who you are and what you're allowed to do\" |\n| refactor / rewire | \"reorganizing the inside without changing what it does\" |\n| cache | \"a saved copy kept around for speed\" |\n| race condition | \"two things happening at once and stepping on each other\" |\n| component | \"a reusable piece of the screen (a button, a chart, a table)\" |\n| route / page | \"a screen in the app\" |\n| design system / component library | \"the shared kit of screen pieces that keeps everything looking consistent\" |\n| SSR / SPA / rendering | \"how pages get built and shown\" (only mention if it affects speed or SEO) |\n| MCP server | \"a way for AI assistants to use your app's data and actions directly\" |\n| SDK | \"a ready-made toolkit so other developers can build on your app\" |\n| CLI | \"a way to drive the app by typing commands (for power users / automation)\" |\n| theme token / design variable | \"a single setting (like your brand color) reused everywhere, so you change it once\" |\n| modal / drawer | \"a pop-up box\" / \"a slide-out panel\" |\n\nWhen in doubt, say what the *user or the business* experiences, not what the machine does.\n\n## Report structure (layered — skim or dive)\n\nWrite these three parts in order. A reader can stop after Part 1.\n\n**Part 1 — Executive summary (one page).**\n- *What you have*: 2–3 sentences — what the product does and who uses it.\n- *The headline*: the 3–5 biggest risks/opportunities, one plain line each.\n- *Recommended order*: a table (Fix | Why it matters | Effort | Payoff). This is the part they act on.\n\n**Part 2 — One section per major area** (drive the areas from `domains.json`; skip domains with nothing worth saying). Each section follows this shape (see `example/sample-report.md`):\n- *What this does* — the capability in business terms.\n- *How it works today* — a plain walkthrough, ideally as a small story (\"on a timer, the app re-reads each site, compares…\").\n- *What's working* — genuine credit. Never skip this; a report that's all criticism gets dismissed.\n- *What's holding you back* — each problem MUST carry: **what it means for you** (business impact), **severity** (Minor / Worth fixing / Serious / Urgent), and **effort** (Small / Medium / Large).\n- *How I'd do it differently — and why it's worth it* — the opinionated part. Argue the improvement in one of these business outcomes: **more reliable / fewer surprises**, **faster to add features**, **cheaper to run**, **safer / less risk**, **easier to maintain or hand off**. Be explicit whether it's a cheap rewire or an expensive rebuild.\n\n**Part 3 — Appendix.**\n- *How confident am I* — REQUIRED. Where you're certain vs guessing; what you'd verify against real data first. The blueprint carries confidence tiers — anything you're relaying from a `low`/`medium` entry, or from a reconstructed (`explicit: false`) event, says so here.\n- *Glossary* (optional) — only for any term that slipped through.\n\n## Rewire vs rebuild (say which)\n\nThe blueprint's `migration.json` tells you which is which — `mappings[]` is what survives (each with its `recommendation`), `dropped[]` is what goes. The reader needs to know because the cost is 10× different.\n- **Rewire** — the valuable machinery exists; you're connecting pieces or turning something on. Cheap, low-risk. (Most \"it should be automatic but isn't\" findings are this.)\n- **Rebuild** — the capability doesn't exist or is fundamentally wrong. Expensive, risky. Reserve the word for when it's true; founders hear \"rewrite\" and panic or overspend.\n\nNever recommend a full rewrite because the code is messy. Messy-but-working is a rewire-over-time story, not a bonfire.\n\n## The frontend and the other ways your app is used\n\nWhen the blueprint has the consumer-surface files, add these to the report — they're often where a founder's questions actually live (\"why does the app feel inconsistent?\", \"can partners build on this?\").\n\n**The screens (`frontend-*.json`) — one area section, founder-framed.**\n- *What a user can do* — walk the main screens as a journey, not a component list.\n- *Consistency* — is it built from one shared kit of screen pieces, or a patchwork? A consistent kit means changes are cheap and the app feels coherent; a patchwork means every change is bespoke and the look drifts. Say which, plainly.\n- *The expensive pieces* — this is the key frontend insight. Most of the screen is standard pieces that are cheap to rebuild or restyle. A **small number carry real custom logic** — a bespoke chart, a complicated data table, a drawing/drag interaction, a rich editor. Those are the parts that take real effort to move or change, and the ones most likely to break. Name them, say what they do, and flag them as the real work — so nobody assumes \"it's just screens, it'll be quick.\"\n- *Design consistency (the \"it looks a bit off\" problems)* — from the blueprint's design findings, call out broken patterns in plain terms and, crucially, why each matters and roughly what it costs to fix. Common ones and how to frame them:\n - **The same action behaves differently in different places** (a slide-out panel here, a pop-up box there for the same task). *Why it matters:* the app feels inconsistent and users have to re-learn each screen. *Fix:* pick one pattern and apply it everywhere — cheap.\n - **Colors/spacing are hardcoded instead of set in one place.** *Why it matters:* changing your brand color, or fixing contrast, means hunting through every screen instead of editing one setting — slow and error-prone. *Fix:* move them to shared \"design tokens\" — a small, high-leverage cleanup.\n - **The same element looks different from page to page** (buttons, headings, cards). *Why it matters:* reads as unpolished and erodes trust, especially in a paid product. *Fix:* one shared version of each, reused — cheap and makes every future change faster.\n These are almost always **cheap rewires with an outsized polish/trust payoff**, not rebuilds. Give each an effort (usually Small–Medium) and say the payoff is perceived quality + faster future changes. Do NOT design-nitpick without a reason — every design point needs a \"why it matters to you.\" And credit consistency where the app already has it.\n- Frame rebuild/restyle work as **rewire vs rebuild**: restyling standard pieces to a consistent kit is cheap; re-creating a custom-logic piece is real engineering.\n\n**How your app can be driven (`interfaces.json`) — usually a short, positive section.**\nExplain, in one line each, the ways the product can be used: people through the web, developers through an API or toolkit, AI assistants through a direct connection (MCP), power users through the command line. This is often a genuine strength worth naming — an app that agents and partners can build on is more valuable than one only humans can click. But be honest about `status`: a connection that exists but only does two things is a *start*, not a feature — say so.\n\n## Technology choices — the honest tradeoffs (don't be cheap on the cons)\n\nThe app made specific technology bets. The founder deserves to know what each bet *bought* and what it *costs* — in business terms, tied to their situation (early vs scaling, chasing enterprise deals or not, big team or two people). Present every significant choice as a genuine tradeoff with **both sides**. Never cheerlead a technology, and never trash one — but do not soften the disadvantages to sound positive. A report that only lists upsides is not honest and is not useful.\n\nRules:\n- For each notable choice (framework, auth, hosting, database, key libraries): **what it buys** and **what it costs**, both in plain business terms, then a recommendation tied to *their* stage and goals — usually \"keep it, here's what to watch\" rather than \"switch.\"\n- Tie cons to consequences the founder feels: vendor bills, security/breach liability, hiring difficulty, how fast they can ship, enterprise-sales blockers, the risk of betting on something young.\n- Distinguish \"younger / smaller community\" (a real, manageable risk) from \"wrong choice\" (rare). Most stack choices are defensible; the job is informed eyes-open, not alarm.\n- **Verify before you disparage.** \"Don't be cheap on the cons\" means ACCURATE cons, not invented ones. Do NOT label a technology immature, niche, or feature-poor from vibes, its name, or its age — check its actual adoption, maturity, and feature set first. And separate an **inherent tradeoff of an approach** (e.g. self-hosting anything means you run and secure it) from a **deficiency of a specific tool** (often false — the tool may be mature and full-featured). Overstating cons is as dishonest as hiding them.\n- **Hold your own recommendation to the same bar.** If \"how I'd do it differently\" lands on a specific stack — Pikku included — it gets the same both-sides treatment as everything else, cons first-class. Pinning someone's dependency for being pre-1.0 while not mentioning that the replacement is pre-1.0 too isn't a second opinion, it's a pitch.\n\n- **Derive the app's choices from the blueprint, never from a list in this file.** Read the stack off `architecture.json`, `integrations.json`, `frontend.json`, and the manifest the repo actually has (`package.json`, `Gemfile`, `go.mod`, …). Cover the bets that are *load-bearing for this product*: typically the framework, the auth/identity approach, the datastore, the hosting/deploy model, the payment and other critical vendor integrations, and anything the blueprint marks `replacementDifficulty: hard`. A choice earns a paragraph if switching it would be expensive, or if living with it constrains the business — not because it appears in some canonical list. If you could write the verdict before reading the blueprint, you are not giving a second opinion.\n\n### The choices *this* app made\n\nWhatever the blueprint shows. A Rails app's bets are Rails, Devise, Pundit, MySQL, Sidekiq, its ERP and payment vendors; a Go app's are different again. Fill in buys/costs/usually for each, from evidence. If a legacy choice is working fine, credit it and move on — \"boring and working\" is a feature, and the pressure to find something to say about a stack is exactly what produces dishonest reports.\n\nWorth naming when it applies: adopting one coherent system in place of hand-rolled, drifted machinery (several ways of deciding who's an admin, a bespoke token table, a back-door test login, home-grown crypto) is a real reliability-and-security upgrade, not a lateral swap — say so when the blueprint's `gaps.json` and `policies.json` show that sprawl.\n\n### The stack a rebuild would land on\n\nInclude this section **only if you are actually recommending a rebuild** onto it — and then give every part of it the same both-sides treatment you gave the app's own bets, per the \"hold your own recommendation to the same bar\" rule above. These are not choices the app made; they are choices you are proposing, which is exactly why their costs are the reader's to weigh. The Pikku target stack is Pikku + Better Auth + TanStack Start + Mantine; the framings below are reference material for the parts you actually recommend, not a script to recite.\n\n**Better Auth (self-hosted sign-in) — instead of a paid service like Auth0/Clerk.**\n- *Buys you:* a mature, battle-tested, **framework-agnostic** library with a deep first-class plugin catalog — two-factor auth, multi-tenancy/organizations, multi-session, rate limiting, Stripe subscription billing, an admin panel, API keys for partners/automation, single-sign-on — plus a plugin system to add more without forking. You keep your users in your own database (single source of truth, no per-user bill that grows with success), with full control of the auth flows, and you can run it embedded in the app or as a standalone self-hosted auth server. So self-hosting here means neither giving up features nor rolling your own security.\n- *Cleans up messy auth (often the biggest win):* adopting it consolidates the kind of hand-rolled, drifted auth that accumulates in an older codebase — several different ways of deciding who's an admin, a bespoke token table, a back-door test login, home-grown encryption — into **one coherent system**. If the blueprint shows a before (legacy, bespoke) and after (on Better Auth), point at it directly: the sprawl collapses into a single well-structured setup. A concrete reliability-and-security upgrade, not just a swap.\n- *Costs you (the honest tradeoff — operational, not security-implementation):* the auth flows and security practices are handled by the library, so this is NOT \"build secure auth from scratch.\" What self-hosting means is you **operate** it — hosting, upgrades, uptime, and incident response sit with your team, where a paid SaaS runs that for you and bundles hosted extras (bot/anomaly detection, leaked-password monitoring, vendor compliance certifications) you'd otherwise operate and document yourself. You trade a per-user bill and vendor ops for control, data ownership, and predictable cost.\n- *Usually:* a strong default for an independent product — mature, full-featured, framework-agnostic, and frequently a genuine cleanup of inherited auth. The real question is who owns operating it, not whether the tool is good enough.\n\n**TanStack Start (the web framework) — instead of the incumbent (Next.js).**\n- *Buys you:* modern, tidy developer experience; strong type-safety that catches whole classes of bugs before users see them; fast iteration; fine-grained control; deploys well to modern/edge hosting. The libraries underneath it — the TanStack ecosystem (Query, Router, Table) — are mature, battle-tested and everywhere in React.\n- *Costs you (the honest tradeoff — maturity of the framework itself):* separate the ecosystem from the framework. Query/Router/Table are mature; **TanStack Start, the framework that wraps them, has not shipped a stable 1.0** — its own maintainers describe it as a release candidate that is feature-complete with a stable API, and tell production users to lock to an exact version and follow the last-mile changes into 1.0. In practice that means pinning your version and budgeting for occasional upgrade work as it settles, rather than upgrading casually. On top of that it's newer than the incumbent (Next.js), which has the largest ecosystem — fewer ready-made templates and third-party examples, and a smaller (though growing) pool of developers who've used *this specific* framework, which can make hiring slightly slower.\n- *Usually:* a credible, modern choice on a mature foundation, but a **pre-1.0 one** — so it carries pinning and upgrade risk that the incumbent does not. Reasonable if the team wants the type-safety and is willing to track the framework to 1.0; harder to justify if nobody has capacity to own upgrades.\n\n**Pikku (the framework a rebuild would land on) — instead of staying where you are.**\n- *Buys you:* one way to write a capability and drive it from anywhere — web, background jobs, timers, realtime, AI assistants, the command line — so a feature is written once instead of five times. Type-safe clients and the API spec fall out of the code rather than being hand-maintained until they drift. The sprawl an organically-grown app accumulates collapses into one shape a small team can hold in its head.\n- *Costs you (the honest tradeoff — it is younger than anything it would replace):* Pikku has **not shipped a stable 1.0** — it's 0.12.x, and 0.13 is the first release that will promise backwards compatibility. Until then upgrades can break you. In practice: pin your version, budget for upgrade work, and know that the community, the ready-made examples, and the pool of developers who have used it are all far smaller than the incumbent's — smaller than TanStack Start's, let alone Next.js's. Being pre-1.0 is normal for a young framework, and survivable, but it is a real cost and it is the reader's to weigh, not yours to skip.\n- *Usually:* worth it when the real problem is sprawl — many surfaces, hand-maintained glue, the same rule implemented three slightly different ways — and the team wants one shape instead of five. Harder to justify for an app that works and needs a few rewires: those are usually cheaper in place. If nobody has capacity to own upgrades, that's a real reason to wait.\n\nCheck these statuses before you write them up rather than repeating them from here — a framework's release stage moves, and the point is the current fact, not this example.\n\n## Delivery\n\nProduce **both**:\n1. A **markdown** report in the repo (e.g. `docs/reports/<app>-second-opinion.md`) — versioned, diffable.\n2. A **shareable web page**: load the **artifact-design** skill, then render the same report as one clean, print-friendly, theme-aware page they can send to a cofounder or the agency. Same content, nicer to read.\n\n## Red flags — you're writing the wrong report\n\n| Symptom | Fix |\n|---|---|\n| A technical term with no translation | Rephrase as the effect on the user/business, or cut the term. |\n| A problem with no \"what it means for you\" | Incomplete — add the business impact or delete it. |\n| All problems, no credit | You'll lose the reader's trust. Name what's genuinely good. |\n| A recommendation with no effort + payoff | Not decision-useful. Add both. |\n| \"Rewrite the app\" | Almost always wrong. Separate rewire (cheap) from rebuild (dear); lean on what `migration.json.mappings` says survives. |\n| A guess stated as fact | Mark confidence. \"I'm certain\" and \"I'd need to check\" are different sentences. |\n| Only listed the upsides of a technology choice | Not honest. Every bet has a cost — name it in business terms, don't soften it to sound positive. |\n| Recommended a stack (including ours) without its cons | You applied a maturity bar to their technology and exempted your own. Both sides, or cut the recommendation. |\n| Trashed a technology as \"the wrong choice\" | Equally lazy. Most choices are defensible; frame as tradeoff + \"what to watch,\" not a verdict. |\n| \"The frontend is just screens, it'll be quick\" | Wrong. The custom-logic pieces (charts, complex tables, editors) are real work — flag them separately from the cheap standard pieces. |\n| A design point with no \"why it matters\" | Taste, not advice. Tie every design finding to user perception (polish/trust) or maintenance cost (change-once vs hunt-everywhere), plus effort. |\n| Reads like a code review | Wrong audience. Would a founder know what to *do* after this paragraph? |\n\n## Relationship to pikku-software-archaeology\n\n`pikku-software-archaeology` = facts → `.knowledge/` blueprint, for a machine to rebuild from. **This skill** = blueprint → opinionated report, for a human to decide from. One extracts; one advises. Run archaeology first (or point this skill at an existing `.knowledge/`), then translate its `gaps.json` + `invariants.json` + `migration.json` into the business-language report above.\n", "pikku-queue/SKILL.md": "---\nname: pikku-queue\ndescription: >-\n Use when adding background job processing, async task queues, or distributed workers to a Pikku\n app. Covers wireQueueWorker, job enqueuing, progress tracking, retries, BullMQ and PgBoss\n adapters. TRIGGER when: code uses wireQueueWorker, user asks about background jobs, task queues,\n async processing, BullMQ, PgBoss, or job retries. DO NOT TRIGGER when: user asks about scheduled\n cron tasks (use pikku-cron) or event-driven triggers (use pikku-trigger).\ninstallGroups: [core]\n---\n\n# Pikku Queue Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions as background queue workers. Supports job control (progress, retry, discard), configurable concurrency, and type-safe job publishing.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their types\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireQueueWorker(config)`\n\n```typescript\nimport { wireQueueWorker } from '@pikku/core/queue'\n\nwireQueueWorker({\n name: string, // Queue name (unique identifier)\n func: PikkuFunc, // Worker function\n config?: {\n batchSize?: number, // Process N jobs at once\n removeOnComplete?: number | boolean, // Clean up completed jobs\n },\n})\n```\n\n### Wire Object (`wire.queue`)\n\nInside queue worker functions:\n\n```typescript\nwire.queue.updateProgress(percent: number) // Report progress (0-100)\nwire.queue.discard(reason: string) // Silently discard job\nwire.queue.fail(reason: string) // Mark job as failed\n```\n\n### Job Publishing\n\n```typescript\nconst jobId = await queue.add(queueName, data, options?)\n```\n\nOptions:\n\n```typescript\n{\n priority?: number, // Higher = processed first\n delay?: number, // Delay in ms before processing\n attempts?: number, // Max retry attempts\n backoff?: {\n type: 'exponential' | 'fixed',\n delay: number, // Base delay in ms\n },\n}\n```\n\n## Usage Patterns\n\n### Basic Queue Worker\n\n```typescript\nconst processReminder = pikkuSessionlessFunc({\n title: 'Process Reminder',\n func: async ({ db, emailService }, { todoId, userId }) => {\n const todo = await db.getTodo(todoId)\n await emailService.sendReminder(userId, todo)\n return { sent: true }\n },\n})\n\nwireQueueWorker({\n name: 'todo-reminders',\n func: processReminder,\n})\n```\n\n### Job Control (Progress, Discard, Fail)\n\n```typescript\nconst processReminder = pikkuSessionlessFunc({\n title: 'Process Reminder',\n func: async ({ db }, { todoId }, wire) => {\n await wire.queue.updateProgress(25)\n\n const todo = await db.getTodo(todoId)\n if (!todo) {\n await wire.queue.discard('Todo not found')\n return\n }\n\n if (todo.completed) {\n await wire.queue.fail('Todo already completed')\n return\n }\n\n await wire.queue.updateProgress(100)\n return { sent: true }\n },\n})\n```\n\n### Retries & Configuration\n\n```typescript\nwireQueueWorker({\n name: 'todo-reminders',\n func: processReminder,\n config: {\n batchSize: 5,\n removeOnComplete: 100,\n },\n})\n\n// Enqueue with retry options\nconst jobId = await queue.add(\n 'todo-reminders',\n {\n todoId: 'abc-123',\n userId: 'user-456',\n },\n {\n priority: 10,\n delay: 5000,\n attempts: 3,\n backoff: { type: 'exponential', delay: 1000 },\n }\n)\n```\n\n### Type-Safe Queue Publishing\n\nAfter `npx pikku all`:\n\n```typescript\nimport { PikkuQueue } from '.pikku/pikku-queue.gen.js'\n\nconst queue = new PikkuQueue(queueService)\n\nconst jobId = await queue.add('todo-reminders', {\n todoId: 'abc-123',\n userId: 'user-456',\n})\n\nconst job = await queue.getJob('todo-reminders', jobId)\nconst status = await job.status() // 'waiting' | 'active' | 'completed' | 'failed'\nconst result = await job.waitForCompletion(30_000)\n```\n\n### Queue Adapters\n\n**BullMQ** (Redis-based):\n\n```typescript\nimport { BullMQQueueService } from '@pikku/queue-bullmq'\n\nconst queueService = new BullMQQueueService({\n connection: { host: 'localhost', port: 6379 },\n})\n```\n\n**PgBoss** (PostgreSQL-based):\n\n```typescript\nimport { PgBossQueueService } from '@pikku/queue-pg-boss'\n\nconst queueService = new PgBossQueueService({\n connectionString: 'postgres://...',\n})\n```\n\n## Complete Example\n\n```typescript\n// functions/email.functions.ts\nexport const sendWelcomeEmail = pikkuSessionlessFunc({\n title: 'Send Welcome Email',\n func: async ({ emailService, db }, { userId }, wire) => {\n await wire.queue.updateProgress(10)\n\n const user = await db.getUser(userId)\n if (!user) {\n await wire.queue.discard('User not found')\n return\n }\n\n await wire.queue.updateProgress(50)\n await emailService.send({\n to: user.email,\n subject: 'Welcome!',\n template: 'welcome',\n data: { name: user.name },\n })\n\n await wire.queue.updateProgress(100)\n return { sent: true, email: user.email }\n },\n})\n\n// wirings/queue.wiring.ts\nwireQueueWorker({\n name: 'welcome-emails',\n func: sendWelcomeEmail,\n config: { removeOnComplete: 100 },\n})\n\n// Enqueue from another function\nexport const registerUser = pikkuSessionlessFunc({\n title: 'Register User',\n func: async ({ db, queue }, { email, name }) => {\n const user = await db.createUser({ email, name })\n await queue.add('welcome-emails', { userId: user.id })\n return { user }\n },\n})\n```\n", "pikku-react/SKILL.md": "---\nname: pikku-react\ndescription: 'Set up @pikku/react in a React app: PikkuProvider context, createPikku factory, and the usePikkuRPC / usePikkuFetch hooks for direct (non-React-Query) calls. TRIGGER when: the user is bootstrapping a React frontend that talks to a Pikku backend, asks how to wire `PikkuProvider`, or needs to make one-off RPC calls outside of useQuery/useMutation. DO NOT TRIGGER when: the user is asking about useQuery/useMutation hooks (use pikku-react-query) or about workflows (use pikku-workflows-client).'\ninstallGroups: [core]\n---\n\n# Pikku React\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/react` is the smallest possible binding: a Context provider plus\ntwo hooks. It does **not** depend on React Query — that's a separate\nopt-in via the generated `api.gen.ts`. Use this skill when setting up the\nprovider or making direct RPC calls.\n\n## What ships\n\n```tsx\nimport {\n PikkuProvider,\n createPikku,\n usePikkuFetch,\n usePikkuRPC,\n usePikkuRealtime,\n} from '@pikku/react'\n```\n\nFive exports. `usePikkuRealtime` is only valid when you wired a\n`PikkuRealtime` class via `createPikku` — see step 3 below.\n\n## Resolving the server URL\n\nEvery client (`createPikku`, realtime, the auth client) resolves its base\nthrough one shared helper in `src/lib/env.ts`. Write this once:\n\n```ts\n// Endpoints come from env, never hardcoded.\nexport function apiUrl(): string {\n // SSR: the client hooks only run in the browser, so a placeholder is fine.\n if (import.meta.env.SSR) {\n return import.meta.env.VITE_API_URL ?? '/__api'\n }\n return import.meta.env.VITE_API_URL ?? `${window.location.origin}/api`\n}\n```\n\n**Never fall back to `http://localhost:3000`.** `import.meta.env.VITE_API_URL`\nis substituted by Vite at *build* time, so any deploy that supplies the URL as\na *runtime* env var or platform binding leaves it `undefined` in the shipped\nbundle — the fallback is then the only branch that ever runs in the browser. A\nlocalhost fallback means every request from a deployed app goes to the user's\nown machine. `origin + '/api'` is same-origin, needs no build-time knowledge of\nthe domain, and is correct wherever the app is served from.\n\nFor local dev, set `VITE_API_URL`, or proxy `/api` → your backend in\n`vite.config.ts` under `server.proxy`. One `/api` entry also covers\n`/api/auth/*`; only add more entries for root-level routes outside `/api`.\n\n## Setup at the app root\n\n```tsx\nimport { createPikku, PikkuProvider } from '@pikku/react'\nimport { PikkuFetch } from './pikku/pikku-fetch.gen'\nimport { PikkuRPC } from './pikku/pikku-rpc.gen'\nimport { apiUrl } from './lib/env'\n\nconst pikku = createPikku(PikkuFetch, PikkuRPC, {\n serverUrl: apiUrl(),\n})\n\ncreateRoot(document.getElementById('root')!).render(\n <PikkuProvider pikku={pikku}>\n <App />\n </PikkuProvider>\n)\n```\n\nIf the project also exposes realtime events (see **pikku-realtime**), pass\nthe `PikkuRealtime` class as the third argument and the instance gets a\n`realtime` field too:\n\n```tsx\nimport { PikkuRealtime } from './pikku/realtime.gen'\n\nconst pikku = createPikku(PikkuFetch, PikkuRPC, PikkuRealtime, {\n serverUrl: apiUrl(),\n})\n// pikku.fetch / pikku.rpc / pikku.realtime — all share the same fetch\n// (server URL + auth configured once).\n```\n\nThe generated classes come from your `pikku.config.json`:\n\n| config field | generated file |\n| ---------------------------- | ----------------------------------------------------- |\n| `clientFiles.fetchFile` | typed HTTP client (`PikkuFetch` class) |\n| `clientFiles.rpcWiringsFile` | RPC client (`PikkuRPC` class) calling all exposed fns |\n| `clientFiles.realtimeFile` | `PikkuRealtime` (websocket events + SSE + channels) |\n\nIf a file isn't being generated, that field is missing from the config —\nadd it and re-run `pikku all`.\n\n`createPikku(...)` accepts the same `CorePikkuFetchOptions` as `PikkuFetch`\nplus `serverUrl`. Auth headers, request interceptors, etc. are configured\non the fetch instance — RPC and realtime inherit them automatically.\n\n## Calling an RPC directly (no React Query)\n\nInside a component:\n\n```tsx\nimport { usePikkuRPC } from '@pikku/react'\n\nfunction Logout() {\n const rpc = usePikkuRPC()\n return <button onClick={() => rpc.invoke('logoutUser', {})}>Sign out</button>\n}\n```\n\n`rpc.invoke(name, data)` is typed against `FlattenedRPCMap` — `name` must\nbe an exposed function id, `data` matches the input schema, return value\nmatches the output schema.\n\nYou also have `rpc.<funcName>(data)` if the generated RPC client builds\ndirect methods (project-dependent).\n\n## Calling fetch directly\n\n```tsx\nconst fetch = usePikkuFetch()\nconst data = await fetch.get('/some-rest-route', { searchParams: {...} })\n```\n\nUse this only when the function is wired via HTTP (REST shape) and you\nneed a path-style call. For RPC calls, `usePikkuRPC()` is cleaner.\n\n## Realtime subscriptions\n\nIf you wired a `PikkuRealtime` class into `createPikku`, use\n`usePikkuRealtime()` to grab the shared instance:\n\n```tsx\nimport { usePikkuRealtime } from '@pikku/react'\nimport type { PikkuRealtime } from './pikku/realtime.gen'\n\nfunction TodoList() {\n const realtime = usePikkuRealtime<PikkuRealtime>()\n useEffect(() => {\n return realtime.subscribe('todo-created', ({ todo }) => {\n /* ... */\n })\n }, [realtime])\n // ...\n}\n```\n\nThe hook throws if no `PikkuRealtime` was wired — that's how you know to\nadd it to `createPikku(...)`. Full event-hub setup, publishing, and SSE\nhelpers live in **pikku-realtime**.\n\n## When to reach for what\n\n| Need | Use |\n| ----------------------------------- | --------------------------------------------- |\n| Render data, dedupe + cache | **usePikkuQuery** (react-query) |\n| Trigger a write, wait for result | **usePikkuMutation** (react-query) |\n| Paginate | **usePikkuInfiniteQuery** (react-query) |\n| One-off call from an event handler | `usePikkuRPC()` direct |\n| Hit a REST endpoint (not RPC) | `usePikkuFetch()` |\n| Run a workflow | **pikku-workflows-client** |\n| Subscribe to events / SSE / channel | `usePikkuRealtime()` (see **pikku-realtime**) |\n\nThe first three live in your generated `api.gen.ts` (see the\n**pikku-react-query** skill). This skill covers the bottom four rows.\n\n## Authentication\n\nAuth is handled at the `PikkuFetch` layer — pass options to `createPikku`\nor set headers on the fetch instance after creation. Common pattern:\n\n```tsx\nconst pikku = createPikku(PikkuFetch, PikkuRPC, {\n serverUrl: apiUrl(),\n fetchOptions: {\n onRequest: (req) => {\n const token = localStorage.getItem('token')\n if (token) req.headers.set('Authorization', `Bearer ${token}`)\n },\n },\n})\n```\n\nExact option names depend on the `@pikku/fetch` version — read\n`PikkuFetch`'s constructor type if unsure.\n\n## What NOT to do\n\n- Don't instantiate `PikkuFetch`/`PikkuRPC` inside a component — `createPikku`\n goes once at the app root, the instance flows through Context.\n- Don't call `usePikkuRPC()` outside a `<PikkuProvider>` — it throws.\n- Don't write a custom RPC client. The generated one already covers every\n exposed function with full types.\n- Don't hardcode user-facing strings. Every display string goes through an\n i18n token — see **pikku-i18n** for the setup (it's English-only by default).\n", "pikku-react-query/SKILL.md": "---\nname: pikku-react-query\ndescription: 'Use the Pikku auto-generated React Query hooks (`usePikkuQuery`, `usePikkuMutation`, `usePikkuInfiniteQuery`) to call backend RPC functions from a React frontend with full type safety. TRIGGER when: writing React components that need to call a Pikku function, fetch data, mutate data, or paginate; user mentions React Query, useQuery, useMutation, or building a frontend that talks to a Pikku backend. DO NOT TRIGGER when: working on the backend (use pikku-rpc / pikku-feature) or wiring a non-React frontend.'\ninstallGroups: [core]\n---\n\n# Pikku React Query Hooks\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nPikku generates a typed React Query layer from your backend `expose: true`\nfunctions. You don''t write `useQuery`/`useMutation` against `fetch`\nyourself — you call hooks named after RPCs and get full type inference for\ninput + output.\n\n## Discover what's available on the client\n\nBefore writing a hook, get the full client surface in one call:\n\n```bash\nyarn pikku meta clients --json\n```\n\nReturns RPCs, workflows, and channels with descriptions and type names:\n\n```json\n{\n \"rpcs\": [\n { \"name\": \"createTodo\", \"description\": \"Create a todo\",\n \"readonly\": false, \"input\": \"CreateTodoInput\", \"output\": \"CreateTodoOutput\" },\n { \"name\": \"listTodos\", \"description\": \"List all todos\",\n \"readonly\": true, \"input\": null, \"output\": \"ListTodosOutput\" }\n ],\n \"workflows\": [...],\n \"channels\": [...]\n}\n```\n\nThe `name` is the RPC identifier; pass it to the hooks below. Input/output\nshapes are inferred automatically — the hook is typed against\n`FlattenedRPCMap[name]['input' | 'output']`. Use `description` to pick the\nright RPC; use `readonly` to choose `usePikkuQuery` vs `usePikkuMutation`.\n\n## Setup (once per app)\n\nIn your app entry (e.g. `main.tsx`):\n\n```tsx\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { PikkuProvider, createPikku } from '@pikku/react'\nimport { PikkuFetch } from './pikku/pikku-fetch.gen'\nimport { PikkuRPC } from './pikku/pikku-rpc.gen'\n\nimport { apiUrl } from './lib/env'\n\nconst queryClient = new QueryClient()\nconst pikku = createPikku(PikkuFetch, PikkuRPC, {\n serverUrl: apiUrl(),\n})\n\n<QueryClientProvider client={queryClient}>\n <PikkuProvider pikku={pikku}>\n <App />\n </PikkuProvider>\n</QueryClientProvider>\n```\n\nThe two generated files come from `pikku.config.json`'s\n`clientFiles.fetchFile` and `clientFiles.rpcWiringsFile`. Hooks live in\nthe file at `clientFiles.reactQueryFile` (typically `api.gen.ts`).\n\n`apiUrl()` is the shared server-URL helper — see **pikku-react**. Never\ninline `?? 'http://localhost:3000'`: a deploy that supplies the URL as a\nruntime binding leaves `import.meta.env.VITE_API_URL` undefined in the\nbundle, so the fallback is the branch that actually runs.\n\n## TanStack Start (SSR)\n\nUnder Start the provider mounts in `routes/__root.tsx` rather than\n`main.tsx`, and the same module is evaluated on the server. Three things\ndiffer:\n\n1. **`apiUrl()` must have an SSR branch.** `window` is undefined during\n render; return the build-time var or a placeholder (the client hooks\n only fire in the browser).\n2. **Build auth clients lazily.** Better Auth validates its baseURL with\n `new URL(...)` at construction, so a module-scope `createAuthClient`\n crashes SSR on the placeholder. Memoize it behind a getter:\n\n ```ts\n let _authClient: ReturnType<typeof createAuthClient> | undefined\n export const authClient = () =>\n (_authClient ??= createAuthClient({ baseURL: `${apiUrl()}/auth` }))\n ```\n\n3. **The auth baseURL needs the `/auth` suffix.** Better Auth only\n appends its default `/api/auth` when the baseURL carries no path.\n `apiUrl()` already ends in `/api`, so a bare `apiUrl()` leaves the\n client calling `/api/get-session` and 404ing.\n\nServer functions that need typed RPC access use the generated shim:\n\n```bash\npikku tanstack-start # emits the makeApi server-function shim\n```\n\n## The hooks\n\nAll hooks are imported from your generated `api.gen.ts`:\n\n```tsx\nimport {\n usePikkuQuery,\n usePikkuMutation,\n usePikkuInfiniteQuery,\n} from './pikku/api.gen'\n```\n\n### `usePikkuQuery(name, data, options?)`\n\nFor RPCs that **read** data. Cacheable. The hook is typed against the RPC's\ninput + output.\n\n```tsx\nexport function TodoList() {\n const { data, isLoading, error } = usePikkuQuery('listTodos', {})\n\n if (isLoading) return <p>Loading…</p>\n if (error) return <p>{error.message}</p>\n return (\n <ul>\n {data?.todos.map((t) => (\n <li key={t.id}>{t.title}</li>\n ))}\n </ul>\n )\n}\n```\n\nThe query key is `[name, data]` automatically — no manual key wrangling.\nPass standard `useQuery` options through (`staleTime`, `enabled`, etc.).\n\n### `usePikkuMutation(name, options?)`\n\nFor RPCs that **write**. Returns a React Query mutation object.\n\n```tsx\nexport function CreateTodoForm() {\n const queryClient = useQueryClient()\n const mutation = usePikkuMutation('createTodo', {\n onSuccess: () => queryClient.invalidateQueries({ queryKey: ['listTodos'] }),\n })\n\n const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n const title = (\n e.currentTarget.elements.namedItem('title') as HTMLInputElement\n ).value\n mutation.mutate({ title })\n }\n\n return (\n <form onSubmit={onSubmit}>\n <input name=\"title\" />\n <button type=\"submit\" disabled={mutation.isPending}>\n {mutation.isPending ? 'Adding…' : 'Add'}\n </button>\n </form>\n )\n}\n```\n\nThe input passed to `mutation.mutate(...)` is type-checked against the RPC's\ninput schema. After success, **invalidate** any list/get queries that should\nrefetch.\n\n### `usePikkuInfiniteQuery(name, data, options?)`\n\nOnly available for RPCs whose output has a `nextCursor?: string | null`\nfield — typically a list endpoint with pagination. The hook auto-feeds\n`nextCursor` into the next page's request.\n\n```tsx\nconst { data, fetchNextPage, hasNextPage, isFetchingNextPage } =\n usePikkuInfiniteQuery('listTodos', { limit: 20 })\n\nconst todos = data?.pages.flatMap((p) => p.rows) ?? []\n```\n\nIf the hook isn't generated for an RPC, the RPC's output doesn't include\n`nextCursor` — paginate it on the backend or use `usePikkuQuery` with\nmanual cursor state.\n\n## Workflow hooks\n\nWhen the project has workflows (`capabilities.workflow: true`), three\nextra hooks are generated. See the **pikku-workflows-client** skill.\n\n## Calling RPCs without React Query\n\nFor one-off calls (event handlers outside of state, side effects), use\n`usePikkuRPC()` from `@pikku/react`:\n\n```tsx\nconst rpc = usePikkuRPC()\nconst handleClick = async () => {\n const result = await rpc.invoke('createTodo', { title: 'inline' })\n}\n```\n\nBut prefer the React Query hooks for anything that touches render state —\ncaching, retries, dedup, and dev-tools come for free.\n\n## Common patterns\n\n- **Optimistic updates**: pass `onMutate` to `usePikkuMutation` to update\n the cache before the server responds. Standard React Query pattern;\n Pikku doesn't add anything special.\n- **Conditional fetching**: pass `enabled: !!someValue` to skip a query\n until you have the input.\n- **Refetch on focus**: enabled by default in React Query; disable with\n `refetchOnWindowFocus: false` in options.\n\n## What NOT to do\n\n- Don't import the RPC client directly and call it inside `useEffect` —\n use the hooks. They handle dedup, caching, and unmount safely.\n- Don't hand-write `useQuery({ queryKey: ['listTodos'], queryFn: ... })`\n — `usePikkuQuery('listTodos', {})` does it correctly with one line.\n- Don't construct hook names dynamically. Hook names = RPC names known at\n generation time.\n- Don't bypass the type system with `as any` — if a hook's types don't\n match what you expect, the backend's input/output schemas are wrong;\n fix those first.\n", "pikku-realtime/references/other-routes.md": "# Subscribing to other SSE / WebSocket routes\n\nThe same `PikkuRealtime` client also handles generic SSE + channel routes (not just\n`/events` topics). Use the path; the base URL is inherited from `PikkuFetch`.\n\n```ts\n// Any `sse: true` HTTP route\nconst sub = realtime.subscribeToSSE<{ progress: number }>(\n `/workflow-run/${runId}/stream`,\n (event) => setProgress(event.progress)\n)\n// later: sub.close()\n\n// Any wireChannel — open a raw socket, wrap in PikkuWebSocket for typed I/O\nconst ws = realtime.connectToChannel('/ws/kanban')\nconst typed = new PikkuWebSocket<'kanban-live'>(ws)\ntyped.getRoute('command').subscribe('message', (data) => {\n /* ... */\n})\n```\n\nDiscover what's available with `pikku meta clients --json` — `channels` and any HTTP\n`sse: true` routes are listed there.\n", "pikku-realtime/SKILL.md": "---\nname: pikku-realtime\ndescription: 'Use Pikku''s realtime feature — typed pub/sub events over WebSocket (multi-topic) or SSE (single-topic, auto-cleanup). Covers declaring EventHubTopics, scaffolding the /events channel, the auto-generated `PikkuRealtime` client, and publishing events from a function. TRIGGER when: the user asks for realtime updates, pub/sub, push notifications, server-sent events, websocket events, eventhub, or \"live\" data on the frontend. DO NOT TRIGGER when: the user wants RPC-style request/response (use pikku-rpc / pikku-react-query) or a custom one-off WebSocket channel (use pikku-websocket).'\ninstallGroups: [core]\n---\n\n# Pikku Realtime\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nMost realtime UI is just typed pub/sub: a server pushes `todo-created`, the client\nrenders it. Pikku ships exactly that, two ways — both use the same `EventHubService`\nand the same publish call, so choose by transport, not by code shape:\n\n- **WebSocket** at `/events` — one connection, many topic subscriptions.\n- **SSE** at `GET /events/:topic` — one connection per topic, auto-cleanup on\n disconnect. Good when WebSocket is blocked or for trivially streaming one topic.\n\n## 1. Declare your topics\n\nIn your project's types file (e.g. `types/eventhub-topics.d.ts`):\n\n```ts\nimport type { Todo } from '../src/schemas.js'\n\nexport type EventHubTopics = {\n 'todo-created': { todo: Todo }\n 'todo-updated': { todo: Todo }\n 'todo-deleted': { todoId: string }\n}\n```\n\nReference it in `application-types.d.ts` and instantiate it in `services.ts`:\n\n```ts\n// application-types.d.ts\nimport type { EventHubService } from '@pikku/core/channel'\nimport type { EventHubTopics } from './eventhub-topics.js'\n\nexport interface SingletonServices extends CoreSingletonServices<Config> {\n eventHub?: EventHubService<EventHubTopics>\n}\n\n// services.ts\nimport { LocalEventHubService } from '@pikku/core/channel'\nconst eventHub = new LocalEventHubService<EventHubTopics>()\n```\n\nFor multi-instance deployments use `CloudflareEventHubService` /\n`LambdaEventHubService` / `UWSEventHubService` instead — same interface.\n\n## 2. Enable the server side\n\n```bash\nyarn pikku enable events # auth required by default\nyarn pikku enable events --noAuth # public events\n```\n\nThis sets `scaffold.events` in `pikku.config.json`. The next `pikku all` generates\n`events.gen.ts` in your scaffold dir, wiring (using whatever `eventHub` is in your\nsingletons — you write neither by hand):\n\n- A WebSocket channel at `/events` handling `{action: 'subscribe' | 'unsubscribe', topic}` messages.\n- An SSE handler at `GET /events/:topic`.\n\n## 3. Generate the typed client\n\nAdd to `pikku.config.json`:\n\n```jsonc\n{\n \"clientFiles\": {\n \"realtimeFile\": \"packages/sdk/src/pikku/realtime.gen.ts\",\n // Optional: full type inference for subscribe/unsubscribe\n \"realtimeEventHubTopicsImport\": \"../../../functions/types/eventhub-topics.js#EventHubTopics\",\n },\n}\n```\n\nRun `pikku all` (or `pikku realtime` to regenerate just this file). The generated\nfile exports two surfaces:\n\n```ts\nexport class PikkuRealtime {\n constructor(options: { url: string; reconnect?: boolean; ... })\n subscribe<K extends keyof EventHubTopics>(topic: K, handler: (data: EventHubTopics[K]) => void): () => void\n unsubscribe<K extends keyof EventHubTopics>(topic: K, handler?: ...): void\n close(): void\n}\n\nexport function subscribeToTopicViaSSE<K extends keyof EventHubTopics>(\n baseUrl: string, topic: K, handler: (data: EventHubTopics[K]) => void\n): { close: () => void }\n```\n\nWithout `realtimeEventHubTopicsImport`, the client falls back to\n`Record<string, unknown>` — usable but untyped. Set the import for full typed\nsubscribe/unsubscribe.\n\n## 4. Publish events from a function\n\nThe `/events` channel listens for client subscriptions; the eventHub fans out\npublishes. Envelope the payload with `topic` so the client dispatcher works; the\n`null` channelId means \"broadcast to all subscribers\" (pass a specific channel id\nto exclude/include a single connection):\n\n```ts\nimport { pikkuFunc } from '#pikku'\n\nexport const createTodo = pikkuFunc({\n input: CreateTodoInput,\n output: CreateTodoOutput,\n func: async ({ kysely, eventHub }, data) => {\n const todo = await kysely\n .insertInto('todos').values(data).returningAll()\n .executeTakeFirstOrThrow()\n\n if (eventHub) {\n await eventHub.publish('todo-created', null, {\n topic: 'todo-created',\n data: { todo },\n })\n }\n return { id: todo.id }\n },\n})\n```\n\nA thin helper removes the duplication:\n\n```ts\nasync function publishEvent<K extends keyof EventHubTopics>(\n hub: EventHubService<EventHubTopics>, topic: K, data: EventHubTopics[K]\n) {\n return hub.publish(topic, null, { topic, data })\n}\n// usage: await publishEvent(eventHub, 'todo-created', { todo })\n```\n\n## 5. Wire it up — share fetch with PikkuRPC\n\n`PikkuRealtime` mirrors `PikkuRPC`: it wraps the same `PikkuFetch`, so server URL +\nauth are configured **once** and shared across HTTP, RPC, and realtime transports.\n\n```tsx\nimport { createPikku, PikkuProvider } from '@pikku/react'\nimport { PikkuFetch } from './pikku/pikku-fetch.gen'\nimport { PikkuRPC } from './pikku/pikku-rpc.gen'\nimport { PikkuRealtime } from './pikku/realtime.gen'\n\nconst pikku = createPikku(\n PikkuFetch,\n PikkuRPC,\n PikkuRealtime, // pass the realtime class as the third arg\n { serverUrl: apiUrl() } // shared env helper — see pikku-react\n)\n// pikku.fetch / pikku.rpc / pikku.realtime — all share the same fetch.\n\ncreateRoot(document.getElementById('root')!).render(\n <PikkuProvider pikku={pikku}><App /></PikkuProvider>\n)\n```\n\nOr wire manually:\n\n```ts\nconst realtime = new PikkuRealtime()\nrealtime.setPikkuFetch(pikku.fetch) // inherits serverUrl + auth\n```\n\n## 6. Subscribe from React\n\nSubscribe inside `useEffect` (never the render path, or you create a subscription\nper render). `subscribe` returns an unsubscribe function; SSE's `subscribeToTopic`\nreturns a handle with `close()`:\n\n```tsx\nimport { useEffect, useState } from 'react'\n\nfunction TodoList() {\n const { realtime } = usePikku() // a hook over your context\n const [todos, setTodos] = useState<Todo[]>([])\n\n useEffect(() => {\n // WebSocket multi-topic:\n const off = realtime.subscribe('todo-created', ({ todo }) =>\n setTodos((prev) => [...prev, todo]))\n return off\n\n // Single-topic SSE (auto-cleanup on close) instead:\n // const sub = realtime.subscribeToTopic('todo-created', ({ todo }) =>\n // setTodos((prev) => [...prev, todo]))\n // return () => sub.close()\n }, [realtime])\n\n return <ul>{todos.map((t) => <li key={t.id}>{t.title}</li>)}</ul>\n}\n```\n\n## Other SSE / WebSocket routes\n\nThe same client also subscribes to generic `sse: true` routes and raw `wireChannel`\nsockets (`subscribeToSSE`, `connectToChannel`). See\n[references/other-routes.md](references/other-routes.md).\n\n## When to pick which transport\n\n| Need | Use |\n| ------------------------------------------ | ----------------------------- |\n| Many topics in one connection | **PikkuRealtime** (WebSocket) |\n| Single live stream, simple cleanup | **subscribeToTopicViaSSE** |\n| Bidirectional (client also sends messages) | **PikkuRealtime** |\n| WebSockets blocked by infra | **subscribeToTopicViaSSE** |\n\nBoth auto-clean on the server (the eventHub's `onChannelClosed` hook unsubscribes\nall topics for the dead channel id). Don't write manual cleanup unless you're\nunsubscribing partway through a session.\n\n## What NOT to do\n\n- Don't call `eventHub.publish(topic, ..., rawData)` without the `{topic, data}`\n envelope — clients use `topic` to dispatch handlers.\n- Don't create your own `/events` channel by hand — `pikku enable events` already\n does it correctly with disconnect cleanup.\n- Don't subscribe inside the render path — use `useEffect`.\n- Don't subscribe to topics that don't exist in `EventHubTopics`. The generated\n client's types prevent it; if you reach for `as any` to subscribe to a string,\n declare the topic first.\n", "pikku-redis/SKILL.md": "---\nname: pikku-redis\ndescription: >-\n Use when setting up Redis-backed services in a Pikku app. Covers channel stores, workflow\n services, secret services, event hubs, agent runs, and deployment services backed by Redis.\n TRIGGER when: code uses RedisChannelStore, RedisWorkflowService, RedisSecretService, or user\n asks about Redis setup with Pikku. DO NOT TRIGGER when: user asks about BullMQ queues (use\n pikku-queue) or SQL databases (use pikku-kysely).\n---\n\n# Pikku Redis\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/redis` provides Redis-backed implementations of Pikku's core service interfaces using [ioredis](https://github.com/redis/ioredis).\n\n## Installation\n\n```bash\nyarn add @pikku/redis\n```\n\n## API Reference\n\n### Available Services\n\nAll services accept a Redis connection (ioredis `Redis` instance, `RedisOptions`, or connection string) in their constructor.\n\n| Service | Interface | Purpose |\n| ------------------------- | ---------------------- | ---------------------------------------------- |\n| `RedisChannelStore` | `ChannelStore` | WebSocket channel state persistence |\n| `RedisEventHubStore` | `EventHubStore` | Event hub state persistence |\n| `RedisWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |\n| `RedisWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |\n| `RedisDeploymentService` | `DeploymentService` | Deployment state management |\n| `RedisAgentRunService` | `AgentRunService` | Agent execution tracking |\n| `RedisSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |\n\n### Secret Service\n\n```typescript\nimport { RedisSecretService } from '@pikku/redis'\n\nconst secrets = new RedisSecretService(\n connectionOrConfig: Redis | RedisOptions | string,\n config: { kekSecret: string; salt: string }\n)\n\nawait secrets.getSecret<T = string>(key: string): Promise<T>\nawait secrets.getSecrets<T>(keys: (keyof T & string)[]): Promise<Partial<T>>\nawait secrets.hasSecret(key: string): Promise<boolean>\nawait secrets.setSecret(key: string, value: unknown): Promise<void>\nawait secrets.deleteSecret(key: string): Promise<void>\nawait secrets.rotateKEK(): Promise<number>\nawait secrets.close(): Promise<void>\n```\n\n## Usage Patterns\n\n### Full Setup\n\n```typescript\nimport {\n RedisChannelStore,\n RedisWorkflowService,\n RedisSecretService,\n} from '@pikku/redis'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n\n const channelStore = new RedisChannelStore(config.redisUrl)\n const workflowService = new RedisWorkflowService(config.redisUrl)\n\n const secrets = new RedisSecretService(config.redisUrl, {\n kekSecret: config.kekSecret,\n salt: config.salt,\n })\n\n return { config, logger, channelStore, workflowService, secrets }\n})\n```\n", "pikku-rpc/SKILL.md": "---\nname: pikku-rpc\ndescription: >-\n Use when making internal function-to-function calls within a Pikku app, composing functions, or\n exposing RPC endpoints. Covers rpc.invoke, rpc.remote, rpc.exposed, and generated RPC client.\n TRIGGER when: code uses wire.rpc or expose: true, user asks about calling one Pikku function\n from another, function composition, or RPC endpoints. DO NOT TRIGGER when: user asks about HTTP\n routes (use pikku-http) or addon cross-package calls (use pikku-addon).\ninstallGroups: [core]\n---\n\n# Pikku RPC Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nCall Pikku functions from other Pikku functions internally with full type safety. Use RPC to compose business logic without importing functions directly.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and which could be called via RPC\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### RPC Methods (on `wire.rpc`)\n\nFour ways to call functions via RPC:\n\n| Method | Purpose |\n| -------------------------------- | ----------------------------------------- |\n| `rpc.invoke(name, data)` | Internal call to any wired function |\n| `rpc.remote(name, data)` | Remote call via DeploymentService |\n| `rpc.exposed(name, data)` | Call functions marked with `expose: true` |\n| `rpc.startWorkflow(name, input)` | Start a workflow |\n\n### Exposed Functions\n\nMark a function as externally callable via RPC:\n\n```typescript\nconst greet = pikkuSessionlessFunc({\n title: 'Greet',\n expose: true, // ← callable via rpc.exposed()\n func: async ({}, { name }) => {\n return { message: `Hello, ${name}!` }\n },\n})\n```\n\n### HTTP RPC Endpoint\n\nExpose all `expose: true` functions over HTTP:\n\n```typescript\nwireHTTP({\n route: '/rpc/:rpcName',\n method: 'post',\n auth: false,\n func: rpcCaller,\n})\n```\n\n## Usage Patterns\n\n### Internal Function Composition\n\n```typescript\nconst calculateTax = pikkuSessionlessFunc({\n title: 'Calculate Tax',\n func: async ({}, { amount, rate }) => {\n return { tax: amount * rate }\n },\n})\n\nconst processOrder = pikkuFunc({\n title: 'Process Order',\n func: async ({ db }, { orderId }, { rpc }) => {\n const order = await db.getOrder(orderId)\n\n // Call another pikku function internally — fully typed\n const { tax } = await rpc.invoke('calculateTax', {\n amount: order.total,\n rate: 0.08,\n })\n\n return { orderId, total: order.total + tax }\n },\n})\n```\n\n### When to Use RPC vs Direct Imports\n\n| Approach | Use When |\n| -------------- | -------------------------------------------------------------------------------------------- |\n| `rpc.invoke()` | Cross-domain calls, maintaining separation of concerns, function may be in different package |\n| Direct import | Same module, tightly coupled logic, performance critical |\n\nRPC calls go through Pikku's middleware and permission pipeline. Direct imports skip them.\n\n### Generated RPC Client\n\nAfter `npx pikku all`:\n\n```typescript\nimport { pikkuRPC } from '.pikku/pikku-rpc.gen.js'\n\npikkuRPC.setServerUrl('http://localhost:4002')\n\nconst result = await pikkuRPC.invoke('calculateTax', {\n amount: 100,\n rate: 0.08,\n})\n\npikkuRPC.setAuthorizationJWT(token)\n```\n\n## Complete Example\n\n```typescript\n// functions/billing.functions.ts\nexport const calculateTax = pikkuSessionlessFunc({\n title: 'Calculate Tax',\n func: async ({}, { amount, region }) => {\n const rates = { US: 0.08, EU: 0.2, UK: 0.2 }\n return { tax: amount * (rates[region] || 0) }\n },\n})\n\nexport const calculateShipping = pikkuSessionlessFunc({\n title: 'Calculate Shipping',\n func: async ({}, { weight, region }) => {\n const base = region === 'US' ? 5 : 15\n return { shipping: base + weight * 0.5 }\n },\n})\n\n// functions/orders.functions.ts\nexport const processOrder = pikkuFunc({\n title: 'Process Order',\n func: async ({ db }, { orderId }, { rpc }) => {\n const order = await db.getOrder(orderId)\n\n const { tax } = await rpc.invoke('calculateTax', {\n amount: order.total,\n region: order.region,\n })\n\n const { shipping } = await rpc.invoke('calculateShipping', {\n weight: order.totalWeight,\n region: order.region,\n })\n\n const finalTotal = order.total + tax + shipping\n await db.updateOrder(orderId, { tax, shipping, finalTotal })\n\n return { orderId, total: finalTotal, tax, shipping }\n },\n})\n```\n", "pikku-rtl/SKILL.md": "---\nname: pikku-rtl\ndescription: 'Make a Pikku frontend work in both English (LTR) and Arabic / right-to-left languages. Direction is derived from the active locale, applied once at the document root, and the layout mirrors itself — but only if styling is written flow-relative (margin-inline-start, text-align: start, Mantine ms/me) instead of left/right. TRIGGER when: adding Arabic (or Hebrew/Farsi/Urdu), asked to \"support RTL / right-to-left / bidi / mirror the layout\", or writing layout styles in an app that may run RTL. Builds on pikku-i18n (an RTL language is just another locale file). DO NOT TRIGGER for backend functions or for LTR-only copy changes.'\ninstallGroups: [core]\n---\n\n# Pikku RTL (Arabic + English)\n\nThis skill sits **on top of** `pikku-i18n`. That skill maps a locale to `t()`\ntokens; this one adds the second axis: a locale also has a **direction**.\nArabic is not special-cased — it is just another locale file (`ar.json`,\nregistered `satisfies typeof en`) plus the document being told it is `rtl`.\n\n## The one idea\n\nSet `dir` **once at the document root** from the active locale, then let the\nbrowser and Mantine mirror everything — _provided_ every custom style is written\n**flow-relative** (start/end), never **physical** (left/right). Get those two\nthings right and Arabic, Hebrew, Farsi and Urdu all work with zero per-component\n*layout* code — directional icons still need one manual step, covered below.\n\n## Agent Operating Procedure\n\n1. **Tokens first.** Every visible string is already a `t()` token via\n `pikku-i18n`. Arabic copy goes in `i18n/ar.json`, mirroring `en.json`'s keys,\n registered with `satisfies typeof en` so a missing key is a compile error.\n2. **Add the direction helper** to the i18n config (one home for locale→dir):\n ```ts\n const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur'])\n export function localeDir(locale: string = defaultLocale): 'rtl' | 'ltr' {\n return RTL_LOCALES.has(locale.split('-')[0]) ? 'rtl' : 'ltr'\n }\n ```\n (The bundled templates already ship this helper — use it, don't reinvent it.)\n3. **Apply `dir` + `lang` at the root**, once, from the active locale — pick the\n recipe for your framework below.\n4. **Write every layout style flow-relative.** This is the part that actually\n makes mirroring work; see the rules. When editing existing UI to be\n RTL-ready, the job is mostly a search-and-replace of physical properties.\n5. **Flip directional icons** (chevrons, back/forward arrows) — the one thing\n logical properties can't do for you.\n6. Validate with the app's `tsc`, then load `?i18n-debug` / set `dir` and\n eyeball that the layout mirrors and nothing is stuck on the wrong edge.\n\n## Flow-relative, not physical — the rules that make it mirror\n\nUse the **inline-axis logical** property; never the physical one:\n\n| Don't (physical) | Do (flow-relative) |\n| ---------------------------- | -------------------------------------------- |\n| `margin-left` / `marginLeft` | `margin-inline-start` / `marginInlineStart` |\n| `margin-right` | `margin-inline-end` / `marginInlineEnd` |\n| `padding-left/right` | `padding-inline-start/end` |\n| `left: 0` / `right: 0` | `inset-inline-start: 0` / `inset-inline-end` |\n| `text-align: left/right` | `text-align: start / end` |\n| `border-top-left-radius` | `border-start-start-radius` |\n| `float: left/right` | `float: inline-start / inline-end` |\n\nIn **Mantine**, use the logical style props — they emit the logical CSS above:\n\n| Don't | Do |\n| ----------- | ----------- |\n| `ml` / `mr` | `ms` / `me` |\n| `pl` / `pr` | `ps` / `pe` |\n\nMantine's own components already use logical properties internally, so once the\ndirection is set they mirror automatically — you only have to be disciplined in\n**your** styles.\n\n**Leave flexbox and grid alone.** `display:flex` already follows `dir`:\n`justify-content: flex-start` resolves to the right edge under RTL on its own.\nNever \"fix\" RTL by swapping to `flex-direction: row-reverse` or reordering DOM —\nthat double-flips and breaks the moment direction changes. The DOM order is\nlogical order; let `dir` handle the visual order.\n\n## Applying direction at the root\n\n### Mantine app (e.g. environment-template)\n\nMantine ships first-class RTL: wrap the tree in `DirectionProvider` and set the\nmatching `dir` on `<html>`.\n\n```tsx\nimport { DirectionProvider, MantineProvider } from '@mantine/core'\nimport i18n, { detectLocale, localeDir } from './i18n/config'\n\nconst locale =\n typeof window !== 'undefined' ? detectLocale(window.location.pathname) : 'en'\nconst dir = localeDir(locale)\n\nif (typeof document !== 'undefined') {\n document.documentElement.lang = locale\n document.documentElement.dir = dir // Mantine + browser read this\n}\n\nroot.render(\n <DirectionProvider initialDirection={dir}>\n <MantineProvider theme={theme} defaultColorScheme=\"dark\">\n {/* …app… */}\n </MantineProvider>\n </DirectionProvider>\n)\n```\n\nTo flip direction live (a language switcher) call\n`document.documentElement.setAttribute('dir', localeDir(next))` and Mantine's\n`useDirection().setDirection(dir)`; both read the same value.\n\n### Plain Vite SPA (kanban, test-harness vite-spa)\n\nNo Mantine — just put `dir`/`lang` on `<html>` at bootstrap, after the locale is\ndetected (the same `detectLocale` the i18n config uses):\n\n```ts\nimport { detectLocale, localeDir } from './i18n/config'\n\nconst locale = detectLocale(window.location.pathname)\ndocument.documentElement.lang = locale\ndocument.documentElement.dir = localeDir(locale)\n```\n\nEverything below inherits `dir` from `<html>`; logical CSS does the mirroring.\n\n### Vite SSR (test-harness vite-ssr)\n\nThe worker renders the full HTML, so set `lang`/`dir` on the server `<html>`\nfrom the **URL** locale (the client inherits it on hydration — no flash):\n\n```tsx\nimport { detectLocale, localeDir } from './i18n/config'\n\nconst locale = detectLocale(new URL(request.url).pathname)\nconst dir = localeDir(locale)\nconst html = `<!doctype html>\n<html lang=\"${locale}\" dir=\"${dir}\">\n …\n</html>`\n```\n\ni18next's active language must match: call `i18n.changeLanguage(locale)` before\n`renderToString` so the SSR'd text and `dir` agree.\n\n### Next.js app-router (test-harness next-ssr / next-static)\n\nSet it on the `<html>` in `app/layout.tsx`. With locale-prefixed routes the\nsegment gives the locale; for a single-locale build it's a constant:\n\n```tsx\nimport { localeDir, defaultLocale } from './i18n/config'\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n const locale = defaultLocale // or the [lang] route segment / params\n return (\n <html lang={locale} dir={localeDir(locale)}>\n <body>{children}</body>\n </html>\n )\n}\n```\n\nFor `output: 'export'` with `/ar` prefixes, derive `locale` from the route\nsegment so each statically-exported tree carries the right `dir`.\n\n## Directional icons — the manual bit\n\nLogical properties mirror box layout, **not glyphs**. An icon that points\nsomewhere (chevron, back/next arrow, send, undo) must flip under RTL; a\nnon-directional icon (search, settings, avatar) must **not**. Flip with the\n`:dir()` selector — no JS, no per-locale branching:\n\n```css\n:dir(rtl) .icon-directional {\n transform: scaleX(-1);\n}\n```\n\nOr in CSS-in-JS / inline, gate on the resolved direction:\n`transform: localeDir(locale) === 'rtl' ? 'scaleX(-1)' : undefined`.\nPrefer logical icon components if your icon set ships them.\n\n## Arabic typography niceties\n\n- **Font:** the default Latin stack renders Arabic with the system fallback,\n which is inconsistent. Add an Arabic-capable family (e.g. _Noto Sans Arabic_,\n _IBM Plex Sans Arabic_) to `font-family` so both scripts look intentional.\n- **Numerals:** don't hardcode digits. Format numbers/dates with\n `Intl.NumberFormat`/`Intl.DateTimeFormat` (or i18next formatters) given the\n active locale, so Western vs Arabic-Indic digits follow the locale choice.\n- **Line height:** Arabic diacritics sit tall — a slightly larger `line-height`\n on Arabic body text avoids clipping. Keep it locale-scoped, not global.\n\n## Adding Arabic to an existing app — checklist\n\n1. `i18n/ar.json` mirroring `en.json`; register\n `ar: { translation: ar satisfies typeof en }` and add `'ar'` to\n `supportedLocales`. (Type-complete or it won't compile — the deploy blocks.)\n2. Confirm the `localeDir` helper includes `ar` (it does by default).\n3. Confirm the root sets `dir` from the locale (recipe above).\n4. Sweep the app's styles: replace every `left/right`, `ml/mr`, `text-align:\nleft` with the flow-relative equivalent; revert any manual `row-reverse`.\n5. Flip directional icons.\n6. `tsc`, then load the Arabic route and verify the whole layout mirrors —\n sidebar on the right, text right-aligned, arrows pointing the other way.\n\n## What NOT to do\n\n- Don't use physical `left`/`right` (or `ml`/`mr`) in any new layout style — even\n in an English-only app. Writing logical from the start is the seam Arabic\n slots into, exactly like tokens are for copy.\n- Don't fake RTL with `flex-direction: row-reverse`, reversed DOM order, or\n per-locale `if (rtl)` layout branches. Set `dir` once; let layout follow.\n- Don't set `dir` on individual components — it belongs on `<html>` so the whole\n document (and Mantine) agrees.\n- Don't translate Arabic copy outside the `t()` token system; an RTL language is\n a normal locale, governed by `pikku-i18n`.\n", "pikku-scenario/SKILL.md": "---\nname: pikku-scenario\ndescription: >-\n Use when writing or running Pikku scenarios, or when asked to test Pikku functions or improve\n test coverage. A scenario (pikkuScenario) drives the app the way users do — steps run as actors\n over the real transport against a running server — so a flow doubles as an e2e test and a\n staged/production health check. Covers scenario.do / expectEventually / expectError /\n expectService, declared steps via pikkuScenarioStep (including browser steps driven by\n @pikku/playwright), actors and environments in pikku.config.json, SCENARIO_ACTOR_SECRET, the\n `pikku scenario list|run` commands, live function coverage via `pikku dev --coverage`, and\n plain unit tests for pure function logic. TRIGGER when: user asks about scenarios, testing a\n Pikku function, test coverage, end-to-end flows, browser/UI e2e, or health checks. DO NOT\n TRIGGER when: user asks about running an existing test suite (use Bash) or CI configuration.\ninstallGroups: [core]\n---\n\n# Pikku Scenarios\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing: `pikku scenario list` for what exists, `pikku info functions --verbose` for what a scenario can call.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, or build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated.\n4. Validate with the narrowest relevant command first, then `pikku all --tsc` when functions, wirings or schemas may have changed.\n5. If validation fails, fix the source cause and rerun. Do not paper over generated errors by editing generated files.\n\n**`pikku tests` does not exist.** It was removed in #865 — scenarios own coverage now. Any reference you find to it is stale.\n\n## What a scenario is\n\nA scenario is a `pikkuScenario` export that drives the app **as real actors over the real transport**, against a running server. That is what lets one artifact serve as both an e2e test and a staged/production health check.\n\nConsequences that matter, and bite if ignored:\n\n- **There is no state reset.** A scenario runs against a live server. Scope what you create (unique ids, your own rows) and never assume a clean database.\n- **Every effect runs as somebody, or as a declared step.** `scenario.do(...)` without `{ actor }` throws `Scenario tried to run '<rpc>' as an internal step…` — there is no bare internal-RPC step. The other way to do work is `scenario.step/given/when/then`, which runs a `pikkuScenarioStep`; its actor is optional (setup steps have none) unless it declares `browser: true`.\n- **Actors must be configured and signed in**, or the scenario cannot run.\n\nScenarios live in `srcDirectories` like any other function — by convention `*.scenario.ts`.\n\n## Writing one\n\n`pikkuScenario` comes from the **generated** workflow types, not `@pikku/core`:\n\n```typescript\nimport { pikkuScenario } from '#pikku/workflow/pikku-workflow-types.gen.js'\n\nexport const orderSupportScenario = pikkuScenario<\n { value?: number },\n { doubled: number; message: string }\n>({\n title: 'Order support (scenario)',\n tags: ['scenario'],\n func: async ({ logger }, data, { scenario, actors }) => {\n if (!actors?.shopper || !actors?.support) {\n throw new Error(\n 'orderSupportScenario needs run actors (shopper + support) — run via `pikku scenario run <environment>`'\n )\n }\n\n const doubled = await scenario.do(\n 'shopper doubles their order',\n 'doubleValue',\n { value: data?.value ?? 21 },\n { actor: actors.shopper }\n )\n\n const settled = await scenario.expectEventually(\n 'support sees the greeting settle',\n 'formatMessage',\n { greeting: 'Hello', name: 'Support' },\n (out: { message: string }) => out.message.length > 0,\n { actor: actors.support, within: '5s', interval: 50 }\n )\n\n return { doubled: doubled.result, message: settled.message }\n },\n})\n```\n\nA scenario takes the same config fields as a workflow (`title`, `description`, `tags`, `input`/`output`, `auth`, `permissions`, `middleware`, `version`, …). The third argument is the scenario context: `{ scenario, actors }`.\n\n### The scenario API\n\n| Call | Purpose |\n| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |\n| `scenario.do(step, rpc, data, { actor })` | Run an RPC as that actor. The step name is what appears in the run output. |\n| `scenario.expectEventually(step, rpc, data, predicate, { actor, within, interval })` | Poll until `predicate(out)` passes or `within` elapses. For anything asynchronous — queues, workers, eventual state. |\n| `scenario.expectError(step, rpc, data, { actor, matches })` | Assert the call **fails**. For fault injection and negative paths. |\n| `scenario.expectService(step, 'service.method', { actor, calledWith })` | Assert a stubbed service was called. Requires the server to run with `--test`. |\n| `scenario.step(step, stepName, data, { actor })` | Run a declared `pikkuScenarioStep`. `given`/`when`/`then` are the same call with a keyword in the rendered prose. |\n\n`expectEventually` is **scenario-only**. Calling it from a `pikkuWorkflowFunc` is a critical inspector error (`PKU675`) pointing you at `pikkuScenario`.\n\nPrefer `expectEventually` over sleeping.\n\n### Setup and teardown (`before` / `after`)\n\nA scenario config takes `before` and `after`. Both have the **same signature as `func`** — `(services, data, wire)` — with the return value discarded:\n\n```typescript\nconst resetsCredentials = async (_services, _data, { actors }) => {\n await actors!.admin!.invoke('resetCredentials', {})\n}\n\nexport const credentialScenario = pikkuScenario({\n title: 'A credential is loaded on first use',\n tags: ['scenario', 'credential'],\n before: resetsCredentials,\n after: removesInstalledAddon,\n func: async (services, data, { scenario, actors }) => { /* … */ },\n})\n```\n\n| Rule |\n| -------------------------------------------------------------------------------------------------------- |\n| `before` throwing skips the body and fails the run — but `after` still runs. |\n| `after` always runs, in a `finally`, whether the scenario passed or failed. |\n| `after` throwing fails a run that would otherwise have passed. |\n| `after` throwing on an already-failed run attaches as the `cause` and never replaces the original error. |\n| Neither runs when the run is suspended or waiting — teardown only fires at a terminal outcome. |\n| Hooks are **not** ladder rows. The runner records nothing for them; a failure is labelled by phase. |\n\nA hook reaches the app the same way the body does: through `wire.actors`. If you want cleanup to be *visible* on the ladder, make it an ordinary `scenario.then(...)` instead.\n\nHooks are scenario-only. A `before`/`after` on a `pikkuWorkflowFunc` never runs — a workflow is durable and resumable, so a callback that reran on every replay would have no honest meaning.\n\n### Grouping scenarios (`pikkuFeature`)\n\n`pikkuFeature` groups scenarios the way gherkin's `Feature:` groups `Scenario:`. Scenarios are referenced by **imported identifier**, so a renamed or deleted scenario is a compile error rather than a silent skip:\n\n```typescript\nimport { pikkuFeature } from '#pikku/workflow/pikku-workflow-types.gen.js'\nimport {\n credentialLazyLoadScenario,\n credentialRoundTripScenario,\n} from './credential.scenario.js'\n\nexport const credentialFeature = pikkuFeature({\n name: 'Credential API',\n description: 'Credentials resolve lazily and are scoped per user',\n tags: ['credential'],\n before: startsMockOAuthServer,\n after: stopsMockOAuthServer,\n scenarios: [\n credentialLazyLoadScenario,\n ...['stripe', 'google', 'hmac-key'].map((name) => ({\n scenario: credentialRoundTripScenario,\n data: { name },\n })),\n ],\n})\n```\n\n| Rule |\n| ---------------------------------------------------------------------------------------------------------------------------------------- |\n| The **export identifier is the feature's id**; `name` is the human-readable label. Both must be exported or the build fails. |\n| A `{ scenario, data }` entry is gherkin's `Examples:` — one run per entry. `data` is typed against that scenario's input. |\n| Feature hooks run **once around the whole group** (`before → a → b → c → after`), _not_ per scenario. `after` runs in a `finally`. |\n| There is deliberately **no `Background:`**. Per-scenario setup is the scenario's own `before`, referencing a shared function. |\n| A scenario's effective tags are its own **plus** the feature's, so `--tags credential` selects through the feature. |\n| A scenario need not belong to a feature — one with no input still runs standalone. |\n| Membership is resolved by **object identity** at runtime, which is why a loop works and why a scenario built inline in a feature is an error. |\n\nThe **feature is the run unit**: `--flows` on a scenario whose every feature entry carries `data` errors and names the features containing it, because the feature is what supplies that data. Use `--features` for those. A scenario referenced bare anywhere, or in no feature at all, still runs standalone.\n\n### Declared steps (`pikkuScenarioStep`)\n\n`scenario.do` can only name an RPC. A **step** is a named, typed unit of scenario behaviour whose body is an ordinary pikku function — so it can call several RPCs as its actor, assert, or drive a browser.\n\n```typescript\nimport { pikkuScenarioStep } from '#pikku/workflow/pikku-workflow-types.gen.js'\nimport { requireActor } from '@pikku/core/workflow'\n\nexport const buysAnApple = pikkuScenarioStep<\n { qty: number },\n { orderId: string }\n>({\n name: 'buysAnApple',\n description: 'buys an apple',\n template: 'buys {qty} apples',\n func: async (_services, { qty }, { scenarioStep }) => {\n return await requireActor(scenarioStep).invoke('placeOrder', { qty })\n },\n})\n```\n\n```typescript\nawait scenario.given(\n 'buys an apple',\n 'buysAnApple',\n { qty: 1 },\n { actor: actors.shopper }\n)\n// reporter renders: Given the shopper buys 1 apples ✓ 412ms\n```\n\nRules that bite:\n\n- **The step is referenced by its typed string name, not by importing the const** — exactly like `workflow.do`. The name is the step's `pikkuFuncId` and is checked against the generated step map. A non-literal target is a critical error (`PKU678`).\n- **Steps are not RPCs.** They are deliberately never network-callable — a browser-driving step must not be.\n- **`actor.invoke` is typed over the exposed RPC map**, so the name and the payload are checked and the result comes back narrowed — no cast. `actor.invokeRaw(name, data, { headers })` is the same call reporting `{ status, ok, body }` instead of throwing; use it whenever the refusal *is* the assertion.\n- **`actor` and `env` are optional on the wire**, because a pure assertion step needs neither. Narrow them with `requireActor(scenarioStep)` and `requireScenarioEnv(scenarioStep)` from `@pikku/core/workflow` rather than a local guard — both name the step and say what to pass. `env` is `{ apiUrl, appUrl? }` from the environment the run targets, and is how a raw-HTTP step learns the target's URL: a step runs in the CLI process, where there is no `variables` service and `process.env` is not the answer.\n- **Steps default to `retries: 0`**, unlike ordinary workflow steps. Retrying a failed assertion is wrong; pass `retries` explicitly if a step is genuinely flaky-by-nature.\n- **Step results are persisted**, so return JSON-serialisable data — never a `Locator` or a client object.\n- **`description` documents the step; `template` is what the report renders.** `template`'s `{placeholders}` are filled from the input the step was called with, so one step reads differently for each call — `sees {state} addon {packageName}` reports as \"sees available addon @pikku/addon-stripe\". Reflect every input field in the template, and type the values so they read as words (`state?: 'installed' | 'available'`, not `installed?: boolean`). A placeholder with no value renders as nothing and the whitespace collapses.\n- Prose precedence is `options.description` → the step's `template` → the step's own `description` → the positional step name. Repeated names get `#1`, `#2` ordinals, so a `for` loop over a data set is how you write a Scenario Outline. A loop-generated step name is not statically known, so it is matched back to its declaration by step function instead — which works as long as that function's call sites agree on their phase, actor and prose. Two call sites that disagree make the loop step report under its bare runtime name.\n\n### Browser steps\n\nA step declaring `browser: true` gets `wire.browser` — a session bound to **its actor**, signed in through the same `signInPath` + `SCENARIO_ACTOR_SECRET` path the HTTP actors use, so the browser and the RPC calls are one identity. Calling such a step without an actor is a critical error (`PKU677`).\n\n```typescript\nexport const opensTheCart = pikkuScenarioStep<\n { path: string },\n { url: string },\n true\n>({\n name: 'opensTheCart',\n description: 'opens the cart',\n browser: true,\n func: async (_services, { path }, { browser }) => {\n await browser.goto(path)\n return { url: browser.page.url() }\n },\n})\n```\n\n- Install `@pikku/playwright` and `@playwright/test`, and import `@pikku/playwright` once (`import type {} from '@pikku/playwright'`) so `browser.page` is a typed Playwright `Page`. Without it you still get the structural `goto`/`screenshot` handle.\n- The environment needs an `appUrl` beside its `apiUrl`. `pikku scenario run` fails fast before running anything if a browser scenario has no `appUrl` or the driver is not installed.\n- `pikku scenario run <env> --no-browser` **skips** scenarios containing browser steps and reports them as skipped — it does not fail them. That is how a machine with no browser stays green.\n- Playwright auto-waits; do not wrap `page.click` in `expectEventually`.\n\n## Configuration\n\nPersonas, actors and environments live in `pikku.config.json`:\n\n```json\n{\n \"scenarios\": {\n \"personas\": {\n \"shopper\": { \"description\": \"Buys things here\", \"primary\": true },\n \"support\": { \"description\": \"Answers for the shop\", \"proficiency\": \"power\" },\n \"reminders\": { \"description\": \"The shop chasing abandoned carts\", \"kind\": \"system\" }\n },\n \"actors\": {\n \"shopper\": {\n \"email\": \"shopper@actors.local\",\n \"name\": \"Shopper\",\n \"jobTitle\": \"First-time buyer\",\n \"personality\": \"Impatient shopper who abandons slow checkouts\"\n },\n \"shopperB\": { \"persona\": \"shopper\", \"email\": \"shopper-b@actors.local\" }\n },\n \"environments\": {\n \"local\": {\n \"apiUrl\": \"http://localhost:4077\",\n \"signInPath\": \"/api/auth/sign-in/actor\"\n }\n }\n }\n}\n```\n\n### Personas and actors\n\nA **persona** is a kind of person; an **actor** is one body that signs in as one. Above, `support` is declared only as a persona — its actor is materialised (`support@actors.local`), so `actors.support` works without an `actors` entry. Write an actor by hand only when you need something the materialised one wouldn't have:\n\n- a **real email or personality** for it, like `shopper`;\n- a **second body of the same persona**, like `shopperB` — which is what tenant isolation, peer sharing, and \"another member's row\" scenarios are made of. Two actors of one persona must be two different users, so **two actors sharing an email is an error**.\n\nA persona holds only what is true of that kind of person for the app's whole lifetime — `description`, `primary` (whose experience the product is), `kind`, `proficiency`. What someone is trying to get done, and the circumstances they are doing it in, belong to the **scenario**, not to them.\n\n`kind: \"system\"` is the app acting on its own — a schedule, a cleanup, a send. It gets **no actor**: there is nobody to sign in. Give it one by hand only if it genuinely has a service account.\n\nAn actor with no `persona` is its own persona, so a project that never declares any keeps working unchanged.\n\n- `environments.<name>.apiUrl` is required. `signInPath` defaults to `/auth/sign-in/actor`, `rpcPath` to `/rpc`.\n- **`SCENARIO_ACTOR_SECRET` is an environment variable and never goes in `pikku.config.json`.** It signs actors in. `pikku scenario run` throws without it; a server auto-building actors warns and runs without them.\n\n## Running\n\n```bash\npikku scenario list # features with their scenarios indented, then ungrouped scenarios\nSCENARIO_ACTOR_SECRET=… pikku scenario run local\nSCENARIO_ACTOR_SECRET=… pikku scenario run local --flows orderSupportScenario\nSCENARIO_ACTOR_SECRET=… pikku scenario run local --features credentialFeature\nSCENARIO_ACTOR_SECRET=… pikku scenario run local --tags smoke,scenario\n```\n\n`run` takes the environment as a **required positional** — the key from `scenarios.environments`. `--flows`/`-f` filters by scenario name, `--features` by feature id, `--tags`/`-t` by tag (match-any). Every filter narrows the same plan, so narrowing a feature to two of its five scenarios still runs the feature's hooks exactly once around those two.\n\nOutput is `PASS <name> (<ms>) → <output>` / `FAIL <name> (<ms>): <error>`, then `N/M scenarios passed against '<env>'`. A scenario inside a feature is named `<Feature> › <scenario> <data>`.\n\n**Exit code is 1** if any scenario fails _or_ if no scenario matched the filter — a typo'd `--flows` is a hard error, not a silent zero-run pass. It throws outright on an unknown environment, an unknown flow name, or a missing `SCENARIO_ACTOR_SECRET`.\n\n## Coverage\n\nCoverage is attributed by running scenarios against a server that is collecting it. It is **not** derived from unit tests.\n\nPrerequisite in `pikku.config.json`:\n\n```json\n{ \"scaffold\": { \"scenarios\": \"auth\" } }\n```\n\n`scaffold.scenarios` generates the coverage and stub RPCs into your project (`pikkuScenarioTakeLiveCoverage`, `pikkuScenarioResetLiveCoverage`, `pikkuScenarioResetStubs`, `pikkuScenarioGetStubCalls`), so scenario runs work against any server. The coverage RPC reads `<outDir>/function/pikku-functions-meta-verbose.gen.json` off disk at request time — codegen always writes it, but it has to be deployed alongside the app or the RPC returns `null`.\n\n```bash\npikku dev --coverage # V8 precise coverage, in-process\npikku dev --coverage --test # also enable stubs (needed for expectService)\nSCENARIO_ACTOR_SECRET=… pikku scenario run local --coverage\n```\n\nThe run resets coverage before each scenario and snapshots after, writing **`<outDir>/coverage/scenario-coverage.json`**:\n\n```jsonc\n{\n \"generatedAt\": \"…\",\n \"environment\": \"local\",\n \"scenarios\": {\n \"<name>\": {\n /* FunctionCoverageReport */\n },\n },\n}\n```\n\nCoverage is best-effort: it disables itself with a warning if the server is not collecting or the first actor cannot invoke, and it needs at least one configured actor. If you get no coverage, check those first.\n\n**There is no AI-prompt output.** The old `--ai-out` flag died with `pikku tests`; nothing replaced it. To find what needs work, read `scenario-coverage.json` yourself and cross-reference `pikku meta functions list` for input/output schemas.\n\n### Filling coverage\n\n1. `pikku scenario run <env> --coverage`, then read `<outDir>/coverage/scenario-coverage.json` to see what is unexercised.\n2. `pikku meta functions list` for those functions' schemas.\n3. Write a `pikkuScenario` that reaches them **through a real user flow** with an actor — not a scenario per function. Scenarios are flows; coverage is a consequence.\n4. Re-run to confirm.\n\n## Unit tests for pure logic\n\nScenarios are the repo-idiomatic way to test functions, and the only thing that contributes to live coverage. For pure logic with heavy branching, a plain unit test calling `func` directly is still valid and cheap:\n\n```typescript\nimport { describe, test } from 'node:test'\nimport assert from 'node:assert'\n\ndescribe('createTodo', () => {\n test('creates a todo', async () => {\n const services = {\n todoStore: { add: async (title: string) => ({ id: '1', title }) },\n }\n const result = await createTodo.func(services as any, { title: 'Buy milk' })\n assert.equal(result.title, 'Buy milk')\n })\n})\n```\n\n```bash\nnode --import tsx --test src/**/*.test.ts\n```\n\nServices are plain objects — a Pikku function is pure business logic, so a mock is just the shape the function destructures. Build real services via the `pikkuServices` / `pikkuWireServices` factories when a test needs them.\n\n## Red flags\n\n| Smell | Why it's wrong |\n| --------------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| `pikku tests …` | Removed in #865. Use `pikku scenario`. |\n| `.feature` files / Gherkin for function tests | Scenarios are TypeScript, not Gherkin. The in-process cucumber function world was deleted. |\n| `scenario.do(...)` with no `{ actor }` | Throws. Every step runs as somebody. |\n| A scenario per function | Scenarios are user flows. One flow covers many functions; that is the point. |\n| Assuming a clean database | There is no state reset — it may be a staging server. Scope what you create. |\n| `sleep()` before asserting | Use `expectEventually`. |\n| `expectEventually` in a `pikkuWorkflowFunc` | `PKU675` — scenario-only. |\n| Coverage silently 0 | Server not run with `--coverage`, verbose functions meta not deployed, `scaffold.scenarios` unset, or no actors configured. |\n\n`@pikku/cucumber` is a **browser/e2e** harness (`Actor`, `BrowserWorld`, `PersonaData`, `DbUtils`) — out of scope here.\n\nSee `pikku-concepts` for the core mental model.\n", "pikku-schedule/SKILL.md": "---\nname: pikku-schedule\ndescription: >-\n Use when setting up in-memory cron scheduling in a Pikku app. Covers InMemorySchedulerService\n for running scheduled tasks. TRIGGER when: code uses InMemorySchedulerService,\n PikkuTaskScheduler, or user asks about in-memory scheduling, cron jobs without external\n dependencies, or @pikku/schedule. DO NOT TRIGGER when: user asks about cron wiring (use\n pikku-cron) or queue-based scheduling with BullMQ/PgBoss (use pikku-queue).\ninstallGroups: [core]\n---\n\n# Pikku Schedule (In-Memory Scheduler)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/schedule` provides an in-memory cron scheduler for running Pikku scheduled functions without external dependencies like Redis or PostgreSQL.\n\n## Installation\n\n```bash\nyarn add @pikku/schedule\n```\n\n## API Reference\n\n### `InMemorySchedulerService`\n\n```typescript\nimport { InMemorySchedulerService } from '@pikku/schedule'\n\nconst scheduler = new InMemorySchedulerService()\n```\n\nImplements the scheduler service interface. Schedules are held in memory — they do not survive process restarts. Suitable for development and single-instance deployments.\n\n## Usage Patterns\n\n### Basic Setup\n\n```typescript\nimport { InMemorySchedulerService } from '@pikku/schedule'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const scheduler = new InMemorySchedulerService()\n return { config, scheduler }\n})\n```\n\nFor distributed or persistent scheduling, use BullMQ (`BullSchedulerService`) or PgBoss (`PgBossSchedulerService`) from the queue packages instead. See `pikku-queue` for details.\n", "pikku-schema-ajv/SKILL.md": "---\nname: pikku-schema-ajv\ndescription: >-\n Use when setting up JSON schema validation with AJV in a Pikku app. Covers AjvSchemaService for\n request/response validation. TRIGGER when: code uses AjvSchemaService, user asks about AJV, JSON\n schema validation, or @pikku/schema-ajv. DO NOT TRIGGER when: user asks about Cloudflare Workers\n schema validation (use pikku-schema-cfworker).\ninstallGroups: [core]\n---\n\n# Pikku Schema AJV (JSON Schema Validation)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/schema-ajv` provides JSON schema validation using [AJV](https://ajv.js.org/). Implements the `SchemaService` interface from `@pikku/core`. This is the default schema validator for Node.js environments.\n\n## Installation\n\n```bash\nyarn add @pikku/schema-ajv\n```\n\n## API Reference\n\n### `AjvSchemaService`\n\n```typescript\nimport { AjvSchemaService } from '@pikku/schema-ajv'\n\nconst schema = new AjvSchemaService(logger: Logger)\n```\n\n**Methods:**\n\n- `compileSchema(schema: string, value: any): void` — Compile and register a JSON schema\n- `validateSchema(schemaName: string, json: any): void` — Validate data against a compiled schema (throws on failure)\n- `getSchemaNames(): Set<string>` — Get all registered schema names\n- `getSchemaKeys(schemaName: string): string[]` — Get property keys for a schema\n\n## Usage Patterns\n\n### With Pikku Services\n\n```typescript\nimport { AjvSchemaService } from '@pikku/schema-ajv'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new ConsoleLogger()\n const schema = new AjvSchemaService(logger)\n return { config, logger, schema }\n})\n```\n\nPikku automatically uses the schema service to validate function inputs and outputs when schemas are defined in your function definitions.\n", "pikku-schema-cfworker/SKILL.md": "---\nname: pikku-schema-cfworker\ndescription: >-\n Use when setting up JSON schema validation for Cloudflare Workers in a Pikku app. Covers\n CFWorkerSchemaService as a lightweight alternative to AJV. TRIGGER when: code uses\n CFWorkerSchemaService, user asks about schema validation on Cloudflare Workers, or\n @pikku/schema-cfworker. DO NOT TRIGGER when: user asks about AJV schema validation (use\n pikku-schema-ajv).\ninstallGroups: [core, fabric]\n---\n\n# Pikku Schema CFWorker (Cloudflare Workers Validation)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/schema-cfworker` provides JSON schema validation using [@cfworker/json-schema](https://github.com/cfworker/cfworker), a lightweight validator compatible with Cloudflare Workers (no `eval` or `new Function`). Implements the `SchemaService` interface from `@pikku/core`.\n\n## Installation\n\n```bash\nyarn add @pikku/schema-cfworker\n```\n\n## API Reference\n\n### `CFWorkerSchemaService`\n\n```typescript\nimport { CFWorkerSchemaService } from '@pikku/schema-cfworker'\n\nconst schema = new CFWorkerSchemaService(logger: Logger)\n```\n\n**Methods:**\n\n- `compileSchema(schema: string, value: any): void` — Compile and register a JSON schema\n- `validateSchema(schemaName: string, json: any): void` — Validate data against a compiled schema (throws on failure)\n- `getSchemaNames(): Set<string>` — Get all registered schema names\n- `getSchemaKeys(schemaName: string): string[]` — Get property keys for a schema\n\n## Usage Patterns\n\n### With Cloudflare Workers\n\n```typescript\nimport { CFWorkerSchemaService } from '@pikku/schema-cfworker'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new ConsoleLogger()\n const schema = new CFWorkerSchemaService(logger)\n return { config, logger, schema }\n})\n```\n\nUse this instead of `@pikku/schema-ajv` when deploying to Cloudflare Workers, as AJV uses `eval` which is not permitted in the Workers runtime.\n", "pikku-security/SKILL.md": "---\nname: pikku-security\ndescription: >-\n Use when adding authentication or session management to a Pikku app — pikkuAuth, session\n lifecycle (setSession/clearSession), built-in auth strategies (authBearer, authCookie,\n authAPIKey), or JWT setup. TRIGGER when: user asks about login, logout, session, bearer tokens,\n cookie auth, API keys, or JWT. DO NOT TRIGGER when: user asks about middleware (use\n pikku-middleware), permissions/authorization checks (use pikku-permissions), or secrets/env vars\n (use pikku-config).\ninstallGroups: [core]\n---\n\n# Pikku Security (Authentication & Sessions)\n\n## Agent Operating Procedure\n\n1. Discover before editing. Run `pikku info middleware --verbose` and `pikku info functions --verbose` to understand existing auth setup.\n2. Auth strategies live in wirings files — do not put `addHTTPMiddleware` calls inside function bodies.\n3. Validate with `pikku all --tsc` after changes — it regenerates and then type-checks in one pass, and fails on type errors. Use `--tsc-summary` for a compact one-line-per-error report.\n\nFor **middleware** (including tag middleware and service-to-service bearer auth) see `pikku-middleware`.\nFor **permissions** (pikkuPermission, pikkuAuth, per-function authorization) see `pikku-permissions`.\n\n## Session Management\n\n```typescript\n// Read session in pikkuFunc (session guaranteed to exist)\nconst getProfile = pikkuFunc({\n func: async ({ db }, _data, { session }) => {\n return await db.getUser(session.userId)\n },\n})\n\n// Set session (e.g., after login)\nconst login = pikkuFunc({\n auth: false,\n func: async ({ jwt, db }, { email, password }, { setSession }) => {\n const user = await db.verifyCredentials(email, password)\n setSession({ userId: user.id })\n return { token: jwt.sign({ userId: user.id }) }\n },\n})\n\n// Clear session (logout)\nconst logout = pikkuFunc({\n func: async ({}, _data, { clearSession }) => {\n clearSession()\n },\n})\n```\n\n## Built-in Auth Strategies\n\nApply these via `addHTTPMiddleware` in a wirings file:\n\n```typescript\nimport { authBearer, authCookie, authAPIKey } from '@pikku/core/middleware'\nimport { addHTTPMiddleware } from '@pikku/core/http'\n\n// JWT bearer token — reads Authorization header\naddHTTPMiddleware('*', [authBearer()])\n\n// Cookie-based sessions — auto-refreshes JWT\naddHTTPMiddleware('*', [\n authCookie({\n name: 'session',\n expiresIn: { value: 30, unit: 'day' },\n options: { httpOnly: true, secure: true },\n }),\n])\n\n// API key — from x-api-key header or ?apiKey= query param\naddHTTPMiddleware('*', [authAPIKey({ source: 'all' })])\n```\n\n## Complete Example\n\n```typescript\n// permissions.ts\nimport { pikkuAuth, pikkuPermission } from '#pikku'\n\nexport const isAuthenticated = pikkuAuth(async (_services, session) => !!session)\nexport const isVerified = pikkuAuth(async (_services, session) => !!session?.emailVerified)\n\n// wirings/auth.wiring.ts\nimport { authCookie } from '@pikku/core/middleware'\nimport { addHTTPMiddleware } from '@pikku/core/http'\n\naddHTTPMiddleware('*', [\n authCookie({ name: 'session', expiresIn: { value: 30, unit: 'day' } }),\n])\n\n// functions/auth.functions.ts\nexport const login = pikkuFunc({\n auth: false,\n func: async ({ jwt, db }, { email, password }, { setSession }) => {\n const user = await db.verifyCredentials(email, password)\n setSession({ userId: user.id })\n return { token: jwt.sign({ userId: user.id }) }\n },\n})\n\nexport const logout = pikkuFunc({\n func: async ({}, _data, { clearSession }) => {\n clearSession()\n },\n})\n```\n", "pikku-services/references/audit-wire-service.md": "# Audit Wire Service\n\n`createInvocationAudit` creates a per-request `InvocationAuditLog` that buffers audit events in memory and flushes them as a batch when the function-runner calls `closeWireServices` at the end of the request. If `singletonServices.audit` is not configured (local dev without Fabric), it returns a no-op `DisabledInvocationAudit` — no crash, events are silently dropped.\n\nPair with `createAuditedKysely` to auto-capture every Kysely query as an audit event.\n\n```typescript\n// services.ts\nimport { createInvocationAudit } from '@pikku/core/services'\nimport { createAuditedKysely } from '@pikku/kysely'\n\nexport const createWireServices = pikkuWireServices(async (singletonServices, wire) => {\n const audit = createInvocationAudit(singletonServices.audit, wire)\n const kysely = singletonServices.kysely\n ? createAuditedKysely(singletonServices.kysely, { audit })\n : undefined\n return { audit, ...(kysely ? { kysely } : {}) }\n})\n```\n\nThe `audit` wire service is typed as `AuditLog` (from `@pikku/core`). Functions that emit custom events use it directly:\n\n```typescript\nconst deleteUser = pikkuFunc({\n func: async ({ audit }, { userId }) => {\n await audit.audit({ type: 'user.deleted', actor_user_id: userId })\n // ...\n },\n})\n```\n\n`closeWireServices` (called automatically by the function-runner) invokes `audit.close()` → `singletonServices.audit.write(batch)` → platform-specific flush (e.g. CF Queue, libsql INSERT). No manual flushing needed.\n\n> **Fabric note:** Fabric provisions the audit queue and consumer worker automatically. The audit table schema is in `db/sqlite/0003-audit.sql` (starter-template). Run `pikku fabric validate` to confirm the migration is in place.\n", "pikku-services/SKILL.md": "---\nname: pikku-services\ndescription: >-\n Use when setting up dependency injection, creating custom services, or configuring the service\n layer in a Pikku app. Covers pikkuServices (singleton), pikkuWireServices (per-request), service\n typing, built-in services, and tree-shaking. TRIGGER when: code uses\n pikkuServices/pikkuWireServices, user asks about services.ts, dependency injection, service\n factories, or built-in services (ConsoleLogger, JoseJWTService). DO NOT TRIGGER when: user asks\n about auth middleware (use pikku-security) or secrets/variables (use pikku-config).\ninstallGroups: [core]\n---\n\n# Pikku Services (Dependency Injection)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nPikku uses factory functions for dependency injection. Singleton services are created once at startup; wire services are created fresh per request/job/command. See `pikku-concepts` for the core mental model.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See which services existing functions use\npikku info tags --verbose # Understand project organization\n```\n\n## API Reference\n\n### `pikkuServices(factory)` — singleton services (created once at startup)\n\n```typescript\nimport { pikkuServices } from '#pikku'\nimport { ConsoleLogger } from '@pikku/core/services'\nimport { JoseJWTService } from '@pikku/jose'\n\nexport const createSingletonServices = pikkuServices(\n async (config, existingServices?) => {\n // config: your CoreConfig object\n // existingServices: optional, for chaining factories\n const logger = new ConsoleLogger()\n const database = new DatabasePool(config.database)\n await database.connect()\n const jwt = new JoseJWTService(\n async () => [{ id: 'my-key', value: config.jwtSecret }],\n logger\n )\n return { config, logger, database, jwt, books: new BookService() }\n }\n)\n```\n\n### `pikkuWireServices(factory)` — per-request services (fresh per HTTP request, queue job, CLI command, etc.)\n\n```typescript\nimport { pikkuWireServices } from '#pikku'\n\nexport const createWireServices = pikkuWireServices(\n async (singletonServices, wire) => {\n // singletonServices: all singleton services\n // wire: transport context (session, channel, etc.)\n // Pikku merges these with singleton services automatically\n return {\n userSession: createUserSessionService(wire),\n dbTransaction: new DatabaseTransaction(singletonServices.database),\n }\n }\n)\n```\n\n### Auto-Generated Service Manifest\n\nAfter `npx pikku all`, Pikku generates `.pikku/pikku-services.gen.ts`, a manifest of which services are actually used by wired functions:\n\n```typescript\nexport const requiredSingletonServices = {\n database: true, // used by getUser, deleteUser\n audit: true, // used by deleteUser\n cache: false, // not used by any wired function\n jwt: true, // used by auth middleware\n} as const\n\nexport type RequiredSingletonServices = Pick<\n SingletonServices,\n 'database' | 'audit' | 'jwt'\n> &\n Partial<Omit<SingletonServices, 'database' | 'audit' | 'jwt'>>\n```\n\n## Usage Patterns\n\n### Using Services in Functions\n\n**Every service must be declared in `SingletonServices` (or `Services`) in `application-types.d.ts`.** Never access a service via a body-level cast (`services as typeof services & { myService: MyService }`) — that means the type is missing. Add the import and the field to `SingletonServices`, then destructure inline in the function signature. The inspector emits `SERVICES_NOT_DESTRUCTURED` and tree-shaking breaks when the first param is a plain identifier rather than an object pattern. Never `new` a service inside a function — services arrive only via injection.\n\n```typescript\n// ✅ Correct — inline destructure, no cast\nconst getUser = pikkuFunc({\n title: 'Get User',\n func: async ({ db, logger, jwt }, { userId }) => {\n logger.info('Fetching user', { userId })\n return { user: await db.getUser(userId) }\n },\n})\n\n// ❌ Wrong — named param + body cast; inspector warns + tree-shaking breaks\nconst getUser = pikkuFunc({\n func: async (services, { userId }) => {\n const { db } = services as typeof services & { db: DbService }\n // ...\n },\n})\n```\n\n### Services Are Never Optional Inside a Function\n\n**Never write a `if (!service) throw ...` existence guard in a function body.** It is dead code, and it defeats the platform.\n\nOptionality lives in exactly one place — `services.ts` / the `SingletonServices` declaration — and it means *\"this may not be created\"*, not *\"this may be missing at call time\"*. A service is optional precisely because **nothing destructures it**, and `requireSingletonServices` therefore never creates it. The moment any wired function destructures it, Pikku creates it and guarantees it is there.\n\nThe types enforce this rather than merely documenting it. The inspector records the services destructured by every wired `func`, `permissions` **and** `middleware`, and emits them as `RequiredSingletonServices`. The generated function types then default their service parameter to:\n\n```typescript\nexport type WiredSingletonServices = RequiredSingletonServices & SingletonServices\nexport type WiredServices = RequiredSingletonServices & Services\n```\n\nso a service that is `foo?: Foo` in `SingletonServices` arrives as a non-optional `Foo` in every function, permission and middleware that uses it. There is nothing to guard against.\n\n```typescript\n// ✅ Correct — destructure and use; creation is guaranteed by the manifest\nconst listThreads = pikkuFunc({\n func: async ({ agentRunService }, { threadId }) => {\n return await agentRunService.getThreadMessages(threadId)\n },\n})\n\n// ❌ Wrong — unreachable guard; signals a misunderstanding of service wiring\nconst listThreads = pikkuFunc({\n func: async ({ agentRunService }, { threadId }) => {\n if (!agentRunService) throw new MissingServiceError('agentRunService')\n return await agentRunService.getThreadMessages(threadId)\n },\n})\n```\n\nIf a service really is conditional at runtime (e.g. an optional integration a deployment may not configure), that is a **configuration** concern: branch on config, or fail fast at startup in `services.ts` — not per-request in every function.\n\n### Dynamic Import Optimization\n\nUse the generated manifest to conditionally import heavy dependencies — only the services actually wired get instantiated:\n\n```typescript\nimport { requiredSingletonServices } from '.pikku/pikku-services.gen.js'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new ConsoleLogger()\n\n let jwt: JWTService | undefined\n if (requiredSingletonServices.jwt) {\n const { JoseJWTService } = await import('@pikku/jose')\n jwt = new JoseJWTService(keys, logger)\n }\n\n let database: Database | undefined\n if (requiredSingletonServices.database) {\n database = await createDatabase(config.databaseUrl)\n }\n\n return { config, logger, jwt, database }\n})\n```\n\n### Audit Wire Service\n\n`createInvocationAudit` + `createAuditedKysely` add per-request audit buffering that flushes on request close (no-op if `audit` is unconfigured). For the full pattern, no-op behavior, custom-event usage, and Fabric notes, read `references/audit-wire-service.md`.\n\n### Built-in Services\n\n| Service | Package | Purpose |\n| -------------------------- | ---------------------- | -------------------------------- |\n| `ConsoleLogger` | `@pikku/core/services` | Console-based logging |\n| `JoseJWTService` | `@pikku/jose` | JWT sign/verify via jose |\n| `LocalSecretService` | `@pikku/core/services` | Local development secrets |\n| `LocalVariablesService` | `@pikku/core/services` | Local environment variables |\n| `PinoLogger` | `@pikku/pino` | Structured logging via Pino |\n| `createInvocationAudit` | `@pikku/core/services` | Per-request audit buffer |\n| `createAuditedKysely` | `@pikku/kysely` | Auto-capture DB queries as audit events |\n\n## Complete Example\n\n```typescript\n// services.ts\nimport { pikkuServices, pikkuWireServices } from '#pikku'\nimport { ConsoleLogger } from '@pikku/core/services'\nimport { JoseJWTService } from '@pikku/jose'\n\n// Custom service\nclass TodoStore {\n private todos: Map<string, Todo> = new Map()\n async create(title: string, priority: string) {\n const todo = { id: crypto.randomUUID(), title, priority, completed: false }\n this.todos.set(todo.id, todo)\n return todo\n }\n async get(id: string) { return this.todos.get(id) }\n async list() { return [...this.todos.values()] }\n async delete(id: string) { this.todos.delete(id) }\n}\n\nexport const createSingletonServices = pikkuServices(async (config) => {\n const logger = new ConsoleLogger()\n const jwt = new JoseJWTService(\n async () => [{ id: 'my-key', value: config.jwtSecret }],\n logger\n )\n return {\n config,\n logger,\n jwt,\n secrets: new LocalSecretService(),\n variables: new LocalVariablesService(),\n todoStore: new TodoStore(),\n }\n})\n\nexport const createWireServices = pikkuWireServices(\n async (singletonServices, wire) => ({\n scopedLogger: new ScopedLogger(wire.session?.initial?.userId),\n })\n)\n\n// functions/todos.functions.ts — services are auto-injected\nexport const createTodo = pikkuFunc({\n title: 'Create Todo',\n func: async ({ todoStore, logger }, { title, priority }) => {\n const todo = await todoStore.create(title, priority)\n logger.info('Created todo', { id: todo.id })\n return { todo }\n },\n})\n```\n", "pikku-software-archaeology/README.md": "# pikku-software-archaeology\n\nReverse-engineers an existing repository into a **Product Blueprint**: the product intelligence hidden inside an implementation (domains, entities, commands, queries, events, policies, workflows, invariants, integrations, gaps), extracted as schema-validated JSON that a generator — in our case Pikku — can rebuild from.\n\n```\nExisting Repository → pikku-software-archaeology → .knowledge/ blueprint → new Pikku application\n```\n\nThis is **not** a code indexer or doc generator. It extracts *intent over implementation*: `POST /api/users/:id/status` becomes the command `ActivateUser`; three scattered `if (inv.user_id !== req.user.id)` checks become one `InvoiceOwnerOnly` policy with three `enforcedAt` citations.\n\n## Design decision: the AI is the parser\n\nThere is deliberately **no scanner/AST tooling** in this skill. Static extraction is brittle and per-language (the first prototype's regex scanner broke before it ran once); the analyzing model already reads every language — JS, TS, Ruby, Python, PHP, Go — follows indirection, and understands intent. Determinism lives in the **output contract** instead: fixed file names, schema-validated shapes, sorted unordered collections (sequence-bearing arrays keep their observed order), and stable concept names, all enforced by a dumb JSON validator (`scripts/validate.mjs`). The audit is expensive; that's the trade we chose.\n\n## How to run it\n\nIn Claude Code, from (or pointing at) the target repo:\n\n> Use the pikku-software-archaeology skill to extract a product blueprint from /path/to/repo\n\nThe agent then:\n1. **Surveys** the repo (manifests, entry points, routes, jobs, webhooks, schema, config, TODO/HACK markers) — facts only.\n2. **Excavates the test suite** — `describe`/`it` names become workflow scenarios; assertions confirm policies and upgrade confidence; rules that exist *only* in tests are captured.\n3. **Extracts** through twelve lenses (domains, entities, commands, …) per the pipeline in `SKILL.md`. Large repos fan out subagents per lens and merge.\n4. **Cross-checks and validates**:\n ```bash\n node .claude/skills/pikku-software-archaeology/scripts/validate.mjs <repo>/.knowledge\n ```\n The validator checks every file against `references/blueprint.schema.json` plus referential integrity across files (commands reference defined domains, api surfaces map to defined commands/queries, events have producers, …).\n5. Writes `blueprint.md`, the human synthesis.\n\nOutput lands in `<repo>/.knowledge/` — 14 core JSON files + `blueprint.md` (see `SKILL.md` for the full listing). Repos with a frontend and/or non-HTTP consumer channels also get an **optional consumer-surface layer**: `interfaces.json` (every way the product is used — web UI, CLI, MCP server for AI agents, OpenAPI/REST, SDK, realtime, webhooks) plus `frontend.json` / `frontend-routes.json` / `frontend-components.json`. The frontend component inventory's `rebuild` field is the key output: it separates trivially-rebuildable standard components from the **custom-logic** pieces (charts, complex tables, editors) that must be carefully ported. Backend-only repos omit these and the validator does not complain.\n\n### Incremental re-analysis\n\nConcept names are the stable IDs. On re-run after code changes, re-extract only the affected lens/domain, diff against the existing `.knowledge/`, and leave unrelated entries verbatim. Unordered collections are sorted so diffs stay reviewable; sequence-bearing arrays stay in observed order.\n\n## How Pikku consumes the blueprint\n\nFull mapping table in `references/pikku-mapping.md`. Summary: entities → Kysely migrations + Zod schemas; commands/queries → `pikkuFunc`s; api surfaces → `wireHTTP`; policies → shared permission functions (collapsing duplicated legacy checks); system workflows → `wireScheduler`/`wireQueueWorker`/`pikkuWorkflowFunc`; integrations → injected services with `wireSecret`/`wireCredential`; test-derived scenarios → `pikkuUserFlow` stories / e2e tests. Humans resolve `migration.json.decisionsNeeded` before any generation starts.\n\n## How uncertainty is represented\n\nEvery extracted concept carries:\n\n```json\n{ \"evidence\": [{ \"file\": \"controllers/invoices.js\", \"lines\": \"52\", \"note\": \"guard: only drafts editable\" }],\n \"confidence\": \"high\" }\n```\n\n- **high** — the behavior itself is in the cited code/schema/test. Generates directly.\n- **medium** — inferred from converging signals. Generates with a review marker.\n- **low** — plausible reconstruction. Never auto-generated; surfaced for human review.\n\nTwo further distinctions keep facts and guesses separate:\n- `events[].explicit: false` — the event was *reconstructed* from side-effect clusters (email + status flip), not emitted by the code.\n- Comments/docs vs code: comments describe intent, code describes behavior. Disagreements are recorded as the code's behavior plus a `gaps.json` entry.\n\n## Repo layout\n\n```\npikku-software-archaeology/\n├── SKILL.md # skill definition + extraction pipeline\n├── README.md # this file\n├── references/\n│ ├── blueprint.schema.json # the output contract (JSON Schema)\n│ └── pikku-mapping.md # blueprint → Pikku primitives\n└── scripts/\n └── validate.mjs # schema + cross-file validation (node, no deps)\n```\n", "pikku-software-archaeology/references/blueprint.schema.json": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"Product Blueprint (.knowledge/) contract\",\n \"description\": \"One schema per output file, keyed by filename under the `files` map. Every extracted concept carries evidence[] and confidence. Facts (observed in code) and inferences (reconstructed intent) must never be merged silently: confidence expresses how directly the evidence supports the claim.\",\n \"$defs\": {\n \"evidence\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"file\", \"lines\"],\n \"properties\": {\n \"file\": { \"type\": \"string\", \"description\": \"repo-relative path\" },\n \"lines\": { \"type\": \"string\", \"description\": \"e.g. '42' or '42-58'\" },\n \"note\": { \"type\": \"string\", \"description\": \"what this location shows\" }\n }\n }\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\"high\", \"medium\", \"low\"],\n \"description\": \"high = behavior directly observed in code/schema; medium = strong inference from multiple signals; low = plausible guess, needs human confirmation\"\n },\n \"conceptName\": {\n \"type\": \"string\",\n \"pattern\": \"^[A-Z][A-Za-z0-9]*$\",\n \"description\": \"PascalCase domain-language name (SendInvoice, InvoicePaid), never a route or filename\"\n }\n },\n \"files\": {\n \"product.json\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"purpose\", \"actors\", \"capabilities\", \"terminology\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"purpose\": { \"type\": \"string\", \"description\": \"1-3 sentences: what problem, for whom\" },\n \"targetUsers\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"actors\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"kind\", \"description\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"human\", \"system\", \"external\"] },\n \"description\": { \"type\": \"string\" }\n }\n }\n },\n \"capabilities\": { \"type\": \"array\", \"minItems\": 1, \"items\": { \"type\": \"string\" } },\n \"terminology\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"term\", \"meaning\"],\n \"properties\": { \"term\": { \"type\": \"string\" }, \"meaning\": { \"type\": \"string\" } }\n }\n },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n },\n \"domains.json\": {\n \"type\": \"object\",\n \"required\": [\"domains\"],\n \"properties\": {\n \"domains\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"description\", \"entities\", \"commands\", \"queries\", \"events\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\" },\n \"description\": { \"type\": \"string\" },\n \"entities\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"commands\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"queries\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"events\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"policies\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" }, \"description\": \"policy names from policies.json owned by this domain\" },\n \"sourcePaths\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"where this domain currently lives (usually scattered)\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"entities.json\": {\n \"type\": \"object\",\n \"required\": [\"entities\"],\n \"properties\": {\n \"entities\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"domain\", \"description\", \"attributes\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\" },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"description\": { \"type\": \"string\" },\n \"attributes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"type\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"type\": { \"type\": \"string\" },\n \"required\": { \"type\": \"boolean\" },\n \"notes\": { \"type\": \"string\" }\n }\n }\n },\n \"relationships\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"kind\", \"target\"],\n \"properties\": {\n \"kind\": { \"type\": \"string\", \"enum\": [\"belongs-to\", \"has-many\", \"has-one\", \"references\", \"many-to-many\"] },\n \"target\": { \"$ref\": \"#/$defs/conceptName\" },\n \"description\": { \"type\": \"string\" }\n }\n }\n },\n \"states\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"transitions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"from\", \"to\", \"trigger\"],\n \"properties\": {\n \"from\": { \"type\": \"string\" },\n \"to\": { \"type\": \"string\" },\n \"trigger\": { \"type\": \"string\", \"description\": \"command or event name that causes it\" }\n }\n }\n },\n \"ownership\": { \"type\": \"string\", \"description\": \"which actor/tenant owns rows of this entity\" },\n \"constraints\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"commands.json\": {\n \"type\": \"object\",\n \"required\": [\"commands\"],\n \"properties\": {\n \"commands\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"domain\", \"actor\", \"input\", \"preconditions\", \"effects\", \"eventsProduced\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\", \"description\": \"imperative VerbNoun: ApproveInvoice\" },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"actor\": { \"type\": \"string\" },\n \"trigger\": { \"type\": \"string\", \"description\": \"how it is invoked today (route, job, webhook, cron)\" },\n \"input\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"type\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"type\": { \"type\": \"string\" },\n \"required\": { \"type\": \"boolean\" }\n }\n }\n },\n \"preconditions\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"effects\": { \"type\": \"array\", \"minItems\": 1, \"items\": { \"type\": \"string\" } },\n \"eventsProduced\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"policies\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"policy names from policies.json enforced here\" },\n \"currentImplementation\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"file paths implementing it today\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"queries.json\": {\n \"type\": \"object\",\n \"required\": [\"queries\"],\n \"properties\": {\n \"queries\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"domain\", \"actor\", \"description\", \"returns\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\", \"description\": \"GetX / ListX / SearchX\" },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"actor\": { \"type\": \"string\" },\n \"description\": { \"type\": \"string\" },\n \"input\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"type\"],\n \"properties\": { \"name\": { \"type\": \"string\" }, \"type\": { \"type\": \"string\" }, \"required\": { \"type\": \"boolean\" } }\n }\n },\n \"returns\": { \"type\": \"string\" },\n \"scoping\": { \"type\": \"string\", \"description\": \"tenancy/visibility rule applied (e.g. 'rows owned by caller only')\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"events.json\": {\n \"type\": \"object\",\n \"required\": [\"events\"],\n \"properties\": {\n \"events\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"domain\", \"description\", \"producedBy\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\", \"description\": \"past-tense business fact: InvoicePaid. No technical events (ButtonClicked).\" },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"description\": { \"type\": \"string\" },\n \"producedBy\": { \"type\": \"array\", \"minItems\": 1, \"items\": { \"type\": \"string\" }, \"description\": \"command/workflow names\" },\n \"consumedBy\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"what reacts today (email send, status flip, webhook out)\" },\n \"payloadHints\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"explicit\": { \"type\": \"boolean\", \"description\": \"true if the code emits a real event; false if reconstructed from inline side-effects\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"policies.json\": {\n \"type\": \"object\",\n \"required\": [\"policies\"],\n \"properties\": {\n \"policies\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"rule\", \"type\", \"domain\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\" },\n \"rule\": { \"type\": \"string\", \"description\": \"plain-language statement: 'Only the invoice owner can send it'\" },\n \"type\": { \"type\": \"string\", \"enum\": [\"authorization\", \"validation\", \"state\", \"business-constraint\", \"rate-limit\"] },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"enforcedAt\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"every code location enforcing it — multiple locations = divergence risk, note in gaps.json\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"workflows.json\": {\n \"type\": \"object\",\n \"required\": [\"workflows\"],\n \"properties\": {\n \"workflows\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"kind\", \"actor\", \"trigger\", \"steps\", \"result\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"user\", \"admin\", \"system\"] },\n \"actor\": { \"type\": \"string\" },\n \"trigger\": { \"type\": \"string\" },\n \"steps\": { \"type\": \"array\", \"minItems\": 1, \"items\": { \"type\": \"string\" } },\n \"result\": { \"type\": \"string\" },\n \"commandsInvolved\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"eventsInvolved\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"schedule\": { \"type\": \"string\", \"description\": \"cron expression for system workflows, if any\" },\n \"scenarios\": {\n \"type\": \"array\",\n \"description\": \"concrete use cases, primarily excavated from the test suite (describe/it names, fixtures, assertions). A scenario sourced from a test is high-confidence by definition.\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"outcome\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"given\": { \"type\": \"string\" },\n \"when\": { \"type\": \"string\" },\n \"outcome\": { \"type\": \"string\" },\n \"fromTest\": { \"type\": \"string\", \"description\": \"test file path + test name, when sourced from a test\" }\n }\n }\n },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"api.json\": {\n \"type\": \"object\",\n \"required\": [\"surfaces\"],\n \"properties\": {\n \"surfaces\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"kind\", \"path\", \"auth\", \"mapsTo\", \"evidence\"],\n \"properties\": {\n \"kind\": { \"type\": \"string\", \"enum\": [\"rest\", \"graphql\", \"rpc\", \"webhook-in\", \"webhook-out\", \"page\", \"cli\", \"websocket\", \"sse\"] },\n \"method\": { \"type\": \"string\" },\n \"path\": { \"type\": \"string\" },\n \"auth\": { \"type\": \"string\", \"description\": \"none | session | jwt | api-key | signature | capability-url | ...\" },\n \"mapsTo\": {\n \"type\": \"object\",\n \"required\": [\"type\", \"name\"],\n \"properties\": {\n \"type\": { \"type\": \"string\", \"enum\": [\"command\", \"query\", \"event-ingress\"], \"description\": \"command/query for normal surfaces. event-ingress ONLY for surfaces that relay an external event without a domain command of their own; its name must be an events.json event. A webhook whose handler changes state maps to a command (usual case).\" },\n \"name\": { \"$ref\": \"#/$defs/conceptName\" }\n }\n },\n \"notes\": { \"type\": \"string\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" }\n }\n }\n }\n }\n },\n \"integrations.json\": {\n \"type\": \"object\",\n \"required\": [\"integrations\"],\n \"properties\": {\n \"integrations\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"category\", \"purpose\", \"direction\", \"importance\", \"replacementDifficulty\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"category\": { \"type\": \"string\", \"description\": \"payments | email | auth | storage | analytics | llm | sms | ...\" },\n \"purpose\": { \"type\": \"string\" },\n \"dataExchanged\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"direction\": { \"type\": \"string\", \"enum\": [\"outbound\", \"inbound\", \"both\"] },\n \"importance\": { \"type\": \"string\", \"enum\": [\"critical\", \"important\", \"peripheral\"] },\n \"replacementDifficulty\": { \"type\": \"string\", \"enum\": [\"trivial\", \"moderate\", \"hard\"] },\n \"configVia\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"env vars / secrets used\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"architecture.json\": {\n \"type\": \"object\",\n \"required\": [\"components\", \"datastores\"],\n \"properties\": {\n \"components\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"kind\", \"responsibility\", \"evidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"frontend\", \"api\", \"worker\", \"job\", \"webhook-handler\", \"cli\", \"service\", \"proxy\", \"other\"] },\n \"responsibility\": { \"type\": \"string\" },\n \"dependsOn\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"runtime\": { \"type\": \"string\", \"description\": \"how it runs today (process, cron, lambda, ...)\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" }\n }\n }\n },\n \"datastores\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"kind\", \"evidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\" },\n \"usedBy\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"notes\": { \"type\": \"string\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" }\n }\n }\n },\n \"notes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"deployment constraints worth preserving (ports, raw-body routes, ordering)\" }\n }\n },\n \"invariants.json\": {\n \"type\": \"object\",\n \"required\": [\"invariants\"],\n \"properties\": {\n \"invariants\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"statement\", \"domain\", \"enforcedBy\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"statement\": { \"type\": \"string\", \"description\": \"must ALWAYS hold: 'Invoice numbers are sequential per user with no gaps'\" },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"enforcedBy\": { \"type\": \"string\", \"description\": \"db-constraint | code-guard | convention | nothing (!)\" },\n \"atRiskBecause\": { \"type\": \"string\", \"description\": \"why current enforcement is fragile, if it is\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"gaps.json\": {\n \"type\": \"object\",\n \"required\": [\"gaps\"],\n \"properties\": {\n \"gaps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"problem\", \"kind\", \"impact\", \"recommendation\", \"evidence\"],\n \"properties\": {\n \"problem\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"incomplete-feature\", \"todo\", \"duplication\", \"bug\", \"hack\", \"dead-code\", \"unclear-ownership\", \"architecture\", \"security\", \"open-product-decision\"] },\n \"impact\": { \"type\": \"string\" },\n \"recommendation\": { \"type\": \"string\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" }\n }\n }\n }\n }\n },\n \"migration.json\": {\n \"type\": \"object\",\n \"required\": [\"mappings\", \"decisionsNeeded\"],\n \"properties\": {\n \"mappings\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"current\", \"future\", \"recommendation\"],\n \"properties\": {\n \"current\": { \"type\": \"array\", \"minItems\": 1, \"items\": { \"type\": \"string\" }, \"description\": \"file paths in the existing repo\" },\n \"future\": {\n \"type\": \"object\",\n \"required\": [\"domain\"],\n \"properties\": {\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"concepts\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"commands/queries/events/entities this code becomes\" }\n }\n },\n \"recommendation\": { \"type\": \"string\" }\n }\n }\n },\n \"dropped\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"path\", \"reason\"],\n \"properties\": { \"path\": { \"type\": \"string\", \"description\": \"file path; for sub-file drops use 'path (symbolName)' when part of a file survives\" }, \"reason\": { \"type\": \"string\" } }\n }\n },\n \"decisionsNeeded\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"question\"],\n \"properties\": {\n \"question\": { \"type\": \"string\" },\n \"options\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"blockedConcepts\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n }\n }\n }\n }\n },\n \"interfaces.json\": {\n \"x-optional\": true,\n \"type\": \"object\",\n \"description\": \"Every way the product is CONSUMED, one entry per channel. The web UI is one channel (detailed further in frontend*.json); CLI, MCP server (for AI agents), OpenAPI/REST, generated SDK, realtime, and webhooks are others. Answers 'who can drive this product and how'.\",\n \"required\": [\"interfaces\"],\n \"properties\": {\n \"interfaces\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"kind\", \"audience\", \"purpose\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"web-ui\", \"cli\", \"mcp\", \"openapi-rest\", \"graphql\", \"sdk\", \"websocket-realtime\", \"webhook-in\", \"webhook-out\", \"email\", \"other\"] },\n \"audience\": { \"type\": \"string\", \"enum\": [\"human\", \"developer\", \"ai-agent\", \"external-system\", \"internal\"] },\n \"purpose\": { \"type\": \"string\" },\n \"surfaceCount\": { \"type\": \"number\", \"description\": \"how many ops/tools/commands/routes this channel exposes\" },\n \"generated\": { \"type\": \"boolean\", \"description\": \"true if generated from another source (OpenAPI from routes, typed SDK, MCP tools from funcs) rather than hand-written\" },\n \"domainsServed\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"status\": { \"type\": \"string\", \"enum\": [\"complete\", \"partial\", \"stub\", \"deprecated\"] },\n \"notes\": { \"type\": \"string\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"frontend.json\": {\n \"x-optional\": true,\n \"type\": \"object\",\n \"description\": \"Web-UI app-level shape. Names the framework/router/styling/data/auth so the rebuild can weigh their tradeoffs (e.g. TanStack Start + better-auth + Mantine).\",\n \"required\": [\"framework\", \"styling\", \"dataLayer\", \"auth\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"framework\": { \"type\": \"string\", \"description\": \"e.g. TanStack Start, Next.js, Remix, Vite SPA\" },\n \"rendering\": { \"type\": \"string\", \"enum\": [\"spa\", \"ssr\", \"ssg\", \"streaming-ssr\", \"mixed\"] },\n \"router\": { \"type\": \"string\", \"description\": \"e.g. TanStack Router (file-based), React Router\" },\n \"styling\": { \"type\": \"string\", \"description\": \"the design system, e.g. Mantine, Tailwind, MUI, CSS modules\" },\n \"designSystemConsistency\": { \"type\": \"string\", \"enum\": [\"single-system\", \"mostly-consistent\", \"mixed\", \"ad-hoc\"], \"description\": \"how uniformly ONE component/theme system is used — the rebuild target is 'everything Mantine', so divergence is porting work\" },\n \"designFindings\": {\n \"type\": \"array\",\n \"description\": \"Concrete broken/inconsistent design patterns observed across the UI. Each is a specific, cited observation of a consistency defect — not a taste opinion. These become the design recommendations in the second-opinion report.\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"pattern\", \"observation\", \"recommendation\", \"evidence\"],\n \"properties\": {\n \"pattern\": {\n \"type\": \"string\",\n \"enum\": [\n \"interaction-inconsistency\",\n \"theming-not-tokenized\",\n \"cross-page-inconsistency\",\n \"component-duplication\",\n \"spacing-typography-scale\",\n \"design-system-bypass\",\n \"accessibility\",\n \"responsive\",\n \"other\"\n ],\n \"description\": \"interaction-inconsistency = same job done different ways (modal here, drawer there; inconsistent confirm dialogs). theming-not-tokenized = hardcoded colors/spacing/fonts instead of theme tokens/variables. cross-page-inconsistency = the same element (button/header/card) styled differently across pages. component-duplication = several near-identical components for one purpose. spacing-typography-scale = ad-hoc magic numbers off any scale. design-system-bypass = raw HTML/CSS where a design-system component exists. accessibility / responsive = a11y or breakpoint defects.\"\n },\n \"observation\": { \"type\": \"string\", \"description\": \"what is inconsistent, with concrete examples (e.g. 'Add-site uses a Drawer, Edit-site uses a Modal for the same task')\" },\n \"impact\": { \"type\": \"string\", \"description\": \"what it does to the user (feels unpolished/confusing) or to maintenance (a color change means hunting every file)\" },\n \"recommendation\": { \"type\": \"string\", \"description\": \"the fix — usually standardize on one pattern, move values to theme tokens, or extract one shared component\" },\n \"severity\": { \"type\": \"string\", \"enum\": [\"minor\", \"worth-fixing\", \"serious\"] },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n },\n \"stateManagement\": { \"type\": \"string\" },\n \"dataLayer\": { \"type\": \"string\", \"description\": \"how the UI talks to the backend: e.g. pikku-react-query, REST fetch helpers, tRPC, GraphQL client\" },\n \"auth\": { \"type\": \"string\", \"description\": \"client auth mechanism: e.g. better-auth, next-auth, custom JWT\" },\n \"buildTool\": { \"type\": \"string\" },\n \"i18n\": { \"type\": \"string\", \"description\": \"internationalization approach, or 'none'\" },\n \"notes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n },\n \"frontend-routes.json\": {\n \"x-optional\": true,\n \"type\": \"object\",\n \"description\": \"The page/route tree — what a user can navigate to and do, mapped back to the data (queries/commands) each route uses and the components it renders.\",\n \"required\": [\"routes\"],\n \"properties\": {\n \"routes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"path\", \"purpose\", \"evidence\"],\n \"properties\": {\n \"path\": { \"type\": \"string\", \"description\": \"URL path, e.g. /app/sites/:id\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"page\", \"layout\", \"index\", \"modal-or-drawer\", \"redirect\"] },\n \"purpose\": { \"type\": \"string\", \"description\": \"what the user does/sees here, in product terms\" },\n \"auth\": { \"type\": \"string\", \"description\": \"none | authenticated | admin | role:xyz\" },\n \"dataFrom\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"query/command names (from queries.json/commands.json) this route reads/calls\" },\n \"usesComponents\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"component names from frontend-components.json\" },\n \"userFlows\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"workflows.json (kind=user) names this route participates in\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"frontend-components.json\": {\n \"x-optional\": true,\n \"type\": \"object\",\n \"description\": \"Component inventory. The load-bearing field is `rebuild`: it separates components that are trivially rebuildable in the target design system (Mantine) from those carrying bespoke logic that must be carefully PORTED (custom charts, complex tables, canvas, drag/drop). That split is the frontend's real migration cost.\",\n \"required\": [\"components\"],\n \"properties\": {\n \"components\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"role\", \"rebuild\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"role\": { \"type\": \"string\", \"enum\": [\"layout\", \"navigation\", \"presentational\", \"feature\", \"form\", \"data-display\", \"chart\", \"table\", \"overlay\", \"provider\", \"other\"] },\n \"purpose\": { \"type\": \"string\" },\n \"reuse\": { \"type\": \"string\", \"enum\": [\"shared\", \"one-off\"], \"description\": \"used across features vs single-use\" },\n \"rebuild\": {\n \"type\": \"string\",\n \"enum\": [\"mantine-standard\", \"mantine-composition\", \"custom-style\", \"custom-logic\"],\n \"description\": \"mantine-standard = maps 1:1 to a Mantine component (trivial); mantine-composition = built from Mantine primitives (straightforward); custom-style = diverges visually from the design system (normalize to Mantine); custom-logic = bespoke behavior (chart/table/canvas/drag/editor) that must be PORTED, not re-skinned\"\n },\n \"customLogic\": { \"type\": \"string\", \"description\": \"REQUIRED when rebuild=custom-logic: what the bespoke behavior is and why a stock component can't replace it\" },\n \"dependencies\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"notable libs it pulls in (charting/table/editor) — replacement considerations for the port\" },\n \"usedBy\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"routes/components that use it\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n }\n }\n}\n", "pikku-software-archaeology/references/pikku-mapping.md": "# How Pikku Consumes a Product Blueprint\n\nThe `.knowledge/` blueprint is designed so each concept maps onto exactly one Pikku primitive. A generator (or an agent following `pikku-feature`) walks the JSON files in this order:\n\n| Blueprint source | Pikku target |\n|---|---|\n| `entities.json` attributes + relationships + constraints | Kysely migrations + generated `DB` types; Zod schemas per entity |\n| `entities.json` states/transitions | a `status` column + transition guards inside the owning commands (or a state-machine helper) |\n| `commands.json` | `pikkuFunc` / `pikkuSessionlessFunc` with `input:` Zod schema built from `input[]`; `preconditions` become guard clauses; name is the camelCased command name (`SendInvoice` → `sendInvoice`) |\n| `queries.json` | `pikkuFunc` reads; `scoping` becomes the mandatory `WHERE` / session filter |\n| `events.json` | EventHub topics (realtime) or queue messages; `consumedBy` become `wireQueueWorker` handlers — implicit events (`explicit: false`) get promoted to real emissions |\n| `policies.json` (authorization) | Pikku `permissions` / middleware; one policy = one named permission function, wired everywhere `enforcedAt` listed — this collapses duplicated legacy checks into a single definition |\n| `policies.json` (validation) | Zod schema refinements on the command's `input` |\n| `workflows.json` kind=user | frontend flows + the commands they chain |\n| `workflows.json` kind=system, with `schedule` | `wireScheduler` entries |\n| `workflows.json` multi-step / checkpointing | `pikkuWorkflowFunc` with one `workflow.do(...)` step per blueprint step |\n| `workflows.json` `scenarios[]` | **`pikkuUserFlow` stories — this is the canonical target.** Each scenario's given/when/outcome maps 1:1 onto a user-flow step sequence; group scenarios by their workflow into one flow per journey. Only scenarios with no user-facing surface (pure system workflows: cron sweeps, webhook ingest) fall back to API/e2e tests |\n| `api.json` | `wireHTTP` routes: keep `path`+`method` for compatibility, point at the mapped command/query func; `auth: none`/capability-URL surfaces get `auth: false` |\n| `api.json` kind=webhook-in | `wireHTTP` with `auth: false` + signature-verification middleware from the integration |\n| `integrations.json` | services in `services.ts` (constructor-injected classes); `configVia` env vars become `wireSecret` / config; per-user credentials become `wireCredential` |\n| `architecture.json` notes | deployment config (ports, raw-body routes, proxy expectations) |\n| `invariants.json` enforcedBy=db-constraint | migration constraints (UNIQUE, CHECK, FK) |\n| `invariants.json` enforcedBy=code-guard/nothing | guard clauses + a test each; `atRiskBecause` entries get a hardening task |\n| `gaps.json` | excluded from generation; `open-product-decision` + `migration.json.decisionsNeeded` go to a human BEFORE generation starts |\n| `migration.json.mappings` | the work plan: one mapping = one migration slice |\n| `interfaces.json` kind=cli | `wireCLI` entrypoints — the CLI commands are the same funcs the routes expose |\n| `interfaces.json` kind=mcp | `wireMCP` — each MCP tool IS a `pikkuFunc` (reuse the command/query funcs; don't author tool duplicates) |\n| `interfaces.json` kind=openapi-rest / sdk | generated, not hand-written: the OpenAPI spec + typed client SDK fall out of the `wireHTTP` routes + codegen |\n| `interfaces.json` kind=websocket-realtime | `pikku-realtime` EventHub topics / channels |\n| `frontend.json` | `apps/app` (TanStack Start) shell: router, `@pikku/mantine` theme, `pikku-react-query` data layer, `better-auth` client — the target stack the legacy UI is rebuilt onto |\n| `frontend-routes.json` | TanStack Router routes under `apps/app/src/routes/**` (thin data containers calling `usePikkuQuery`); `dataFrom` names become the generated hooks; subpath routes for rich detail views |\n| `frontend-components.json` rebuild=`mantine-standard`/`mantine-composition` | components in `packages/components` composed from `@pikku/mantine` — the trivial/straightforward bulk |\n| `frontend-components.json` rebuild=`custom-logic` | the PORT list — each becomes a `packages/components` component that reimplements the bespoke behavior (chart/table/editor); its `dependencies` inform whether the lib is kept or replaced. These are the frontend's real work items |\n| `frontend-components.json` rebuild=`custom-style` | normalize to Mantine/theme tokens; usually deleted-and-recomposed, not ported |\n\n## Order of generation\n\n1. Human resolves `decisionsNeeded`.\n2. Entities → migrations + types.\n3. Policies → permission functions (before commands, so commands can reference them).\n4. Commands + queries → funcs; api.json → wirings.\n5. Events → topics/queues; system workflows → schedulers/workers/workflows.\n6. Scenarios → tests. Run them against the new implementation; they encode the legacy behavior worth preserving.\n\n## Uncertainty handling\n\n- `confidence: high` concepts generate directly.\n- `confidence: medium` concepts generate, but are listed for review in the pre-generation report — id, evidence summary, and what is uncertain — rather than carrying a marker comment in the generated code. The report is the review surface; the generated code stays clean.\n- `confidence: low` concepts are NOT generated automatically — they surface in the pre-generation review along with `decisionsNeeded`.\n", "pikku-software-archaeology/scripts/validate.mjs": "#!/usr/bin/env node\n// Validates a .knowledge/ blueprint directory against references/blueprint.schema.json,\n// then runs cross-file referential checks (does every command's domain exist, does every\n// api surface map to a real command/query, ...). Exit 0 = valid, 1 = errors.\n//\n// Usage: node validate.mjs <path-to-.knowledge-dir>\n\nimport { readFileSync, existsSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst here = dirname(fileURLToPath(import.meta.url));\nconst schemaDoc = JSON.parse(readFileSync(join(here, '..', 'references', 'blueprint.schema.json'), 'utf8'));\n\nconst dir = process.argv[2];\nif (!dir) { console.error('usage: node validate.mjs <.knowledge dir>'); process.exit(2); }\n\nconst errors = [];\nconst warnings = [];\n\n// --- minimal JSON-Schema-subset validator (type, required, properties, items, enum, minItems, pattern, $ref -> $defs) ---\nfunction resolveRef(ref) {\n const m = /^#\\/\\$defs\\/(\\w+)$/.exec(ref);\n if (!m || !schemaDoc.$defs[m[1]]) throw new Error(`unresolvable $ref ${ref}`);\n return schemaDoc.$defs[m[1]];\n}\n\nfunction check(value, schema, path) {\n if (schema.$ref) schema = { ...resolveRef(schema.$ref), ...schema, $ref: undefined };\n if (schema.enum && !schema.enum.includes(value)) {\n errors.push(`${path}: expected one of [${schema.enum.join(', ')}], got ${JSON.stringify(value)}`);\n return;\n }\n const t = schema.type;\n if (t === 'object') {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n errors.push(`${path}: expected object`); return;\n }\n for (const req of schema.required || []) {\n if (!(req in value)) errors.push(`${path}: missing required field \"${req}\"`);\n }\n for (const [k, v] of Object.entries(value)) {\n if (schema.properties?.[k]) check(v, schema.properties[k], `${path}.${k}`);\n }\n } else if (t === 'array') {\n if (!Array.isArray(value)) { errors.push(`${path}: expected array`); return; }\n if (schema.minItems && value.length < schema.minItems) {\n errors.push(`${path}: needs at least ${schema.minItems} item(s), has ${value.length}`);\n }\n if (schema.items) value.forEach((v, i) => check(v, schema.items, `${path}[${i}]`));\n } else if (t === 'string') {\n if (typeof value !== 'string') { errors.push(`${path}: expected string`); return; }\n if (schema.pattern && !new RegExp(schema.pattern).test(value)) {\n errors.push(`${path}: \"${value}\" does not match ${schema.pattern}`);\n }\n } else if (t === 'boolean' && typeof value !== 'boolean') {\n errors.push(`${path}: expected boolean`);\n } else if (t === 'number' && typeof value !== 'number') {\n errors.push(`${path}: expected number`);\n }\n}\n\n// --- load + per-file validation ---\n// Files marked `x-optional` (the frontend layer) only validate when present, so a\n// backend-only repo does not fail for lacking them.\nconst docs = {};\nfor (const [filename, fileSchema] of Object.entries(schemaDoc.files)) {\n const p = join(dir, filename);\n if (!existsSync(p)) {\n if (!fileSchema['x-optional']) errors.push(`${filename}: missing`);\n continue;\n }\n try {\n docs[filename] = JSON.parse(readFileSync(p, 'utf8'));\n } catch (e) {\n errors.push(`${filename}: invalid JSON (${e.message})`); continue;\n }\n check(docs[filename], fileSchema, filename);\n}\nif (!existsSync(join(dir, 'blueprint.md'))) errors.push('blueprint.md: missing');\n\n// --- cross-file referential checks ---\nif (docs['domains.json'] && docs['commands.json']) {\n const domains = new Set((docs['domains.json'].domains || []).map((d) => d.name));\n const commandNames = new Set((docs['commands.json'].commands || []).map((c) => c.name));\n const queryNames = new Set((docs['queries.json']?.queries || []).map((q) => q.name));\n const eventNames = new Set((docs['events.json']?.events || []).map((e) => e.name));\n\n const wantDomain = (owner, d) => {\n if (d && !domains.has(d)) errors.push(`${owner}: domain \"${d}\" not defined in domains.json`);\n };\n for (const c of docs['commands.json'].commands || []) {\n wantDomain(`commands.json:${c.name}`, c.domain);\n for (const ev of c.eventsProduced || []) {\n if (!eventNames.has(ev)) warnings.push(`commands.json:${c.name} produces \"${ev}\" which is not in events.json`);\n }\n }\n for (const q of docs['queries.json']?.queries || []) wantDomain(`queries.json:${q.name}`, q.domain);\n for (const e of docs['entities.json']?.entities || []) wantDomain(`entities.json:${e.name}`, e.domain);\n for (const ev of docs['events.json']?.events || []) wantDomain(`events.json:${ev.name}`, ev.domain);\n\n for (const s of docs['api.json']?.surfaces || []) {\n const { type, name } = s.mapsTo || {};\n if (type === 'command' && !commandNames.has(name)) errors.push(`api.json:${s.method || ''} ${s.path}: maps to unknown command \"${name}\"`);\n if (type === 'query' && !queryNames.has(name)) errors.push(`api.json:${s.method || ''} ${s.path}: maps to unknown query \"${name}\"`);\n if (type === 'event-ingress' && !eventNames.has(name)) errors.push(`api.json:${s.method || ''} ${s.path}: event-ingress maps to unknown event \"${name}\" (state-changing webhooks should map to a command instead)`);\n }\n // every domain's listed concepts should exist\n for (const d of docs['domains.json'].domains || []) {\n for (const c of d.commands || []) if (!commandNames.has(c)) warnings.push(`domains.json:${d.name}: lists command \"${c}\" not in commands.json`);\n for (const q of d.queries || []) if (!queryNames.has(q)) warnings.push(`domains.json:${d.name}: lists query \"${q}\" not in queries.json`);\n for (const e of d.events || []) if (!eventNames.has(e)) warnings.push(`domains.json:${d.name}: lists event \"${e}\" not in events.json`);\n const policyNames = new Set((docs['policies.json']?.policies || []).map((p) => p.name));\n for (const p of d.policies || []) if (!policyNames.has(p)) warnings.push(`domains.json:${d.name}: lists policy \"${p}\" not in policies.json`);\n }\n // commands with no policies and no preconditions are suspicious for mutating ops\n for (const c of docs['commands.json'].commands || []) {\n if (!(c.policies || []).length && !(c.preconditions || []).length) {\n warnings.push(`commands.json:${c.name}: no policies or preconditions — really unguarded, or missed extraction?`);\n }\n }\n}\n\n// --- frontend layer cross-checks (only when the optional frontend files exist) ---\nif (docs['frontend-components.json']) {\n const componentNames = new Set(\n (docs['frontend-components.json'].components || []).map((c) => c.name),\n );\n // routes should reference components that were actually inventoried\n for (const r of docs['frontend-routes.json']?.routes || []) {\n for (const c of r.usesComponents || []) {\n if (!componentNames.has(c)) {\n warnings.push(`frontend-routes.json:${r.path}: uses component \"${c}\" not in frontend-components.json`);\n }\n }\n }\n // a component flagged as needing a port must say WHY (the custom logic), or the\n // port-risk is unactionable\n for (const c of docs['frontend-components.json'].components || []) {\n if (c.rebuild === 'custom-logic' && !c.customLogic) {\n warnings.push(`frontend-components.json:${c.name}: rebuild=custom-logic but no customLogic description — port risk is unactionable`);\n }\n }\n // data-fetching queries named on routes should resolve to a real query/command\n if (docs['queries.json'] || docs['commands.json']) {\n const known = new Set([\n ...(docs['queries.json']?.queries || []).map((q) => q.name),\n ...(docs['commands.json']?.commands || []).map((c) => c.name),\n ]);\n for (const r of docs['frontend-routes.json']?.routes || []) {\n for (const d of r.dataFrom || []) {\n if (!known.has(d)) {\n warnings.push(`frontend-routes.json:${r.path}: reads \"${d}\" which is not a known query/command`);\n }\n }\n }\n }\n}\n\n// an inconsistent UI with no specific design findings = under-extraction\n// (guarded on frontend.json alone — independent of the component inventory)\nif (docs['frontend.json']) {\n const consistency = docs['frontend.json'].designSystemConsistency;\n const findingCount = (docs['frontend.json'].designFindings || []).length;\n if ((consistency === 'mixed' || consistency === 'ad-hoc') && findingCount === 0) {\n warnings.push(`frontend.json: designSystemConsistency=\"${consistency}\" but designFindings is empty — name the specific broken patterns (interaction/theming/cross-page/…)`);\n }\n}\n\nfor (const w of warnings) console.log(`WARN ${w}`);\nfor (const e of errors) console.log(`ERROR ${e}`);\nconsole.log(`\\n${errors.length} error(s), ${warnings.length} warning(s) across ${Object.keys(docs).length} files`);\nprocess.exit(errors.length ? 1 : 0);\n", "pikku-software-archaeology/SKILL.md": "---\nname: pikku-software-archaeology\ndescription: 'Use when reverse-engineering an existing repository into a Product Blueprint — recovering what product an undocumented or organically-grown codebase implements so it can be rebuilt cleanly (e.g. as a Pikku app). TRIGGER when: user says \"extract a blueprint\", \"reverse engineer this app\", \"what does this codebase actually do as a product\", \"prepare this repo for a rewrite/migration\", or points at a legacy repo (any language — JS, TS, Ruby, Python, PHP, Go) and asks for its domains, workflows, business rules, or a rebuild plan. DO NOT TRIGGER for: documenting code structure, generating API docs from an already-clean codebase, or code review.'\ninstallGroups: [fabric]\n---\n\n# Software Archaeology\n\n## Overview\n\nExtract **intent over implementation**. A repository is a fossil record of product decisions; your job is to recover the product — domains, entities, commands, queries, events, policies, workflows, invariants — not to describe the code. The output is a `.knowledge/` directory of schema-validated JSON plus a human-readable `blueprint.md`, consumable by a generator (Pikku) to rebuild the application cleanly.\n\n**You are the parser.** Do not build or rely on regex/AST scanners — read the code with your own tools (Grep, Read, subagents). This is what makes the skill language-agnostic: an Express app, a Rails app, and a Django app all yield the same blueprint shape.\n\n**Two layers, never merged silently:**\n- **Facts** — behavior directly observed in code, schema, or tests. Cite them.\n- **Inferred intent** — the product reasoning you reconstruct. Mark it with `confidence` and say what evidence it rests on.\n\nNever present a guess as a fact. `confidence: \"high\"` requires file:line evidence of the behavior itself.\n\n## Output Contract\n\nEverything goes in `<repo>/.knowledge/` (or a caller-specified directory):\n\n```\n.knowledge/\n├── product.json # purpose, actors, capabilities, terminology\n├── domains.json # business domains (NEVER folder names)\n├── entities.json # domain entities: attributes, relationships, states, transitions\n├── commands.json # state-changing actions (SendInvoice, not POST /invoices/:id/send)\n├── queries.json # read operations and views\n├── events.json # business facts, past tense (InvoicePaid) — no technical events\n├── policies.json # authorization, validation, state, business-constraint rules\n├── workflows.json # user + admin + system workflows, with test-derived scenarios\n├── api.json # every surface, each mapped to a command/query/event-ingress\n├── integrations.json # external services: purpose, direction, replaceability\n├── architecture.json # components, datastores, deployment constraints worth keeping\n├── invariants.json # what must ALWAYS be true, and what enforces it today\n├── gaps.json # TODOs, hacks, duplication, dead code, open product decisions\n├── migration.json # current files -> future concepts, drops, decisions needed\n├── blueprint.md # human synthesis of all of the above\n│\n│ # OPTIONAL — the consumer-surface layer. Emit these when the repo has a\n│ # frontend and/or non-HTTP consumer channels. A backend-only repo omits them\n│ # and the validator does not complain.\n├── interfaces.json # every way the product is consumed: web-ui, cli, mcp, openapi-rest, sdk, realtime, webhooks\n├── frontend.json # web-UI app shape: framework, router, styling, data layer, auth (e.g. TanStack Start + better-auth + Mantine)\n├── frontend-routes.json # the page/route tree — what a user navigates to, its data + components\n└── frontend-components.json# component inventory; `rebuild` splits trivial-Mantine from custom-logic-to-port\n```\n\nThe exact field shapes live in `references/blueprint.schema.json` (in this skill's directory — read it before writing any output file). After writing, ALWAYS run:\n\n```bash\nnode <skill-dir>/scripts/validate.mjs <repo>/.knowledge\n```\n\nand fix every ERROR (WARNs are prompts to double-check, not necessarily wrong). Do not declare the extraction done with validation errors outstanding.\n\n## The Pipeline\n\nWork in phases. For small repos (< ~50 source files) do them inline; for larger repos, fan out subagents per phase-3 lens and merge (see \"Scaling up\").\n\n### Phase 1 — Survey (facts only)\n\nBuild an inventory before interpreting anything:\n- Manifests (`package.json`, `Gemfile`, `pyproject.toml`, `go.mod`, `composer.json`): dependencies are integration hints; scripts are entry points.\n- Entry points: servers, route registrations, cron/scheduler setup, queue workers, CLI binaries.\n- Data layer: migrations, schema files, model classes, raw DDL (check comments too — schemas hide in comments in migration-less repos).\n- Every HTTP/GraphQL/RPC surface, webhook, scheduled job, queue consumer.\n- **All consumer channels, not just HTTP**: a frontend app (`apps/`, `frontend/`, `web/`, `client/`), a CLI (`bin/`, `wireCLI`, a `cli/` dir, an `openapi`-generated command tool), an MCP server (`wireMCP`, `@modelcontextprotocol`, a `mcp`/`tools` dir), an OpenAPI/Swagger spec (`openapi.json`, `swagger`), a published/generated SDK, realtime channels (websocket/SSE). Each is an `interfaces.json` entry.\n- Env vars and config files.\n- TODO / FIXME / HACK / XXX / deprecated markers — each is a gaps.json candidate.\n- **The test suite** — locate it now, excavate it in Phase 2.\n\nWhere intent hides, per ecosystem (read these first):\n\n| Ecosystem | Highest-yield locations |\n|---|---|\n| Rails | `config/routes.rb`, model validations + callbacks + `aasm`/state machines, `app/policies` (Pundit) / `ability.rb` (CanCan), Sidekiq/ActiveJob workers, `db/schema.rb`, specs (esp. request + model specs) |\n| Express/Node | route registration files, middleware chains (auth!), inline `if` guards in handlers, SQL/ORM models, `jobs/`+crontab refs, webhook handlers |\n| Django | `urls.py`, model `Meta`/constraints/`clean()`, DRF serializers + permissions classes, celery tasks, admin.py (reveals internal workflows) |\n| Laravel | `routes/`, FormRequests (validation), Policies/Gates, Jobs + scheduler in `Kernel.php`, migrations |\n| Go | mux/router setup, middleware, struct tags, `cmd/` binaries (each is a component) |\n| Frontend (React/Vue/etc.) | router config / file-based routes (pages a user reaches), the component tree, the design-system import (`@mantine/*`, `@mui/*`, Tailwind config) to judge consistency, the data layer (react-query/tRPC/fetch wrappers) to tie UI back to backend queries, charts/tables/editors (the `custom-logic` port risk), auth wiring |\n\n### Phase 2 — Test excavation (do not skip)\n\nTests are the closest thing to an executable product spec. For every test file:\n- `describe`/`context`/`it` names → **scenarios** (attach to the matching workflow in `workflows.json` under `scenarios[]`, with `fromTest` set).\n- User-flow/journey harnesses (`pikkuUserFlow` stories, cucumber `.feature` files, Playwright journeys) are the highest-grade scenario source — they already ARE given/when/outcome sequences; extract them verbatim.\n- Assertions → confirmations of policies and invariants (upgrade their `confidence` to `high`, add the test as evidence).\n- Fixtures/factories → entity attribute shapes and realistic example data.\n- Edge-case tests → business rules that exist **nowhere else in the code** (e.g. \"replayed webhook events are idempotent\" may only be stated in a test).\n- Untested-but-critical paths, or a test suite that can't run (missing helpers, broken setup) → `gaps.json`.\n\nA rule attested by both an implementation guard AND a test is your strongest possible evidence — cite both.\n\n### Phase 3 — Extraction lenses\n\nRun each lens over the surveyed material. Rules that counter the classic failure modes:\n\n**Domains** — infer from data ownership, workflows, and vocabulary; NEVER from folder names. `controllers/` is not a domain; \"Billing\" is. A domain owns entities and the commands that mutate them.\n\n**Entities** — domain concepts, not tables. Include: attributes (from schema + serializers + fixtures), relationships, **lifecycle states and transitions** (grep status/state columns, then find every write to them — each write site is a transition with a trigger), ownership (which actor's rows), constraints.\n\n**Commands** — every way state changes: routes, jobs, webhooks, CLI, admin consoles, DB triggers. Name them imperative `VerbNoun` in domain language: `POST /api/users/:id/status` → `ActivateUser`. For each: actor, preconditions (every `if (...) return 4xx` guard is a precondition or policy), effects, events produced. Convention: authentication/token issuance is a command (`LogInUser`, effect: \"issues a session/JWT\") even though it writes no rows — it changes the caller's security state.\n\n**Queries** — reads and views, `GetX`/`ListX`/`SearchX`. Record the tenancy scoping each applies (a missing `WHERE user_id=` that exists elsewhere is a gaps.json security entry).\n\n**Events** — meaningful business facts, past tense. Most legacy apps have **implicit** events: an email send, a status flip, and a counter bump inside one handler are the event's consumers — reconstruct `InvoicePaid` from them and set `explicit: false`. Exclude technical noise (ButtonClicked, FunctionCalled). Inclusion threshold for implicit events: at least one observed consumer beyond the row write itself (an email, a downstream job, a webhook out, a derived-state flip). Plain CRUD facts with no reaction (`ClientCreated` that nothing listens to) do not become events.\n\n**Policies** — authorization, validation, state rules, business constraints. Record **every** location enforcing each rule in `enforcedAt`; the same rule enforced in 3 places (or worse, 2 slightly different versions) is a gaps.json duplication entry.\n\n**Workflows** — ALL of them: user journeys, admin/support operations, and **system workflows** (cron jobs, queue consumers, webhook reactions, syncs, notification sweeps). A crontab line in a comment is a workflow. Attach Phase-2 scenarios.\n\n**API** — list every surface but map each to its concept (`mapsTo: {type: command, name: SendInvoice}`). The route is evidence; the command is the deliverable. Auth per surface (including \"none\" and capability-URLs like tokened public links). A webhook whose handler changes state maps to a **command** (`RecordInvoicePayment`); reserve `event-ingress` for pure relay surfaces, where `name` must be an events.json event.\n\n**Integrations** — from deps + config + calls: purpose, data exchanged, direction, importance, replacement difficulty, env vars.\n\n**Architecture** — components as they actually run (API process, worker, cron job, SPA), datastores, and deployment constraints that must survive the rewrite (hardcoded ports with upstream expectations, raw-body middleware ordering, webhook retry semantics).\n\n**Invariants** — what must always hold, and `enforcedBy`: db-constraint, code-guard, convention, or `nothing` (an unenforced invariant is a gap). Include `atRiskBecause` when enforcement is fragile (e.g. read-then-insert sequence numbering races).\n\n**Gaps** — incomplete features, TODOs, hacks (hardcoded admin emails), duplicated logic that drifted, dead code, unclear ownership, and **open product decisions** the code never resolved (a FIXME asking \"should deleting a client void invoices?\" is a product decision, record it in both gaps.json and migration.json `decisionsNeeded`).\n\n**Migration** — map current file clusters → future domain + concepts; list files to drop with reasons; list decisions a human must make before rebuild.\n\n**Interfaces** (`interfaces.json`, optional) — every way the product is CONSUMED, one entry per channel, not per route. A product is usually driven through several: a **web UI** (humans), a **CLI** (developers/operators), an **MCP server** (AI agents — in a Pikku app each MCP tool IS a `pikkuFunc`), an **OpenAPI/REST** surface (developers/external systems, often *generated* from the routes), a **generated SDK**, **realtime** (websocket/SSE), and **webhooks** (in/out). For each: `kind`, `audience`, `purpose`, roughly how many ops it exposes, whether it's `generated` vs hand-written, which domains it serves, and `status` (complete/partial/stub — an MCP server with two tools is `stub`). This layer answers \"who can drive this, and how\" — it is the map the second-opinion skill needs to explain that the app is usable by people, developers, and agents.\n\n**Frontend** (`frontend.json` + `frontend-routes.json` + `frontend-components.json`, optional) — the web UI, which needs its own treatment because frontends vary wildly (framework, router, styling, state, data, auth) and the rebuild target is opinionated: **everything in one component system (Mantine), one data layer, one auth**.\n- `frontend.json` records the stack as FACTS: framework (e.g. TanStack Start), rendering (SSR/streaming/SPA), router, styling/design system, `designSystemConsistency`, state management, data layer (e.g. pikku-react-query vs REST helpers), auth (e.g. better-auth), i18n. Name the real technologies — the second-opinion skill weighs their tradeoffs, so record them precisely (do NOT editorialize here; this file is facts).\n- `frontend-routes.json` is the page tree: each route's `purpose` in product terms, `auth`, the `dataFrom` (query/command names it reads — reuse the backend concept names so the UI ties back to the domain), the `usesComponents`, and the `userFlows` it belongs to.\n- `frontend.json.designFindings` captures **broken/inconsistent design patterns** as concrete, cited observations (not taste). Actively hunt for: *interaction inconsistency* (the same job done as a modal in one place and a drawer in another; inconsistent confirm dialogs); *theming not tokenized* (hardcoded hex colors, magic spacing/font sizes, inline styles instead of theme tokens/variables — grep for `#[0-9a-f]{3,6}`, `style={{`, raw `px` values); *cross-page inconsistency* (the same element — button, page header, card — styled differently across routes); *component duplication* (three near-identical cards/tables for one purpose); *design-system bypass* (raw HTML/CSS where a library component exists). Each finding gets an example, its impact (feels unpolished / a color change means hunting every file), and a fix (standardize on one pattern / move to tokens / extract one shared component). These are almost always cheap cleanups, and they are exactly what a non-technical owner perceives as \"the app looks off\" without being able to say why.\n- `frontend-components.json` is where the frontend's real migration cost lives, in the **`rebuild`** field: `mantine-standard` (maps 1:1 to a Mantine component — trivial), `mantine-composition` (built from Mantine primitives — straightforward), `custom-style` (diverges only visually — normalize to Mantine), or **`custom-logic`** (bespoke behavior — a custom chart, a virtualized/complex table, a canvas, drag-and-drop, a rich editor — that must be **ported**, not re-skinned). A `custom-logic` component MUST fill `customLogic` explaining the behavior, and should list the `dependencies` (charting/table/editor libs) that make it a real port. This split — \"trivially re-Mantine-able\" vs \"carries logic that must survive the port\" — is the single most useful thing the frontend extraction produces.\n- **Server-rendered / non-React frontends still get all three files — do NOT skip them.** The `rebuild` enum is named for the target stack, but the distinction it draws is target-agnostic: *trivial stock element* vs *composed from primitives* vs *visual divergence only* vs **bespoke behavior that must be ported**. A Rails app (Slim/ERB + ViewComponents + Hotwire/Stimulus), a Django app (templates + HTMX), or a Laravel app (Blade + Livewire) all have that same split, and it is just as load-bearing there. Use the enum values verbatim (the validator enforces them), classify by what the thing actually *does*, and record the vocabulary mismatch in `frontend.json.notes`. Concretely: treat a template partial + its behavior controller (Stimulus/Alpine/Livewire) as ONE component and classify the pair; a server-rendered app's `custom-logic` is the same list as a SPA's (maps, charts, video players, payment elements, drag-to-reorder, rich text, QR, live-updating regions), plus anything whose behavior rides on a streaming/partial-update contract (Turbo Streams, HTMX swaps) — get that mapping wrong on rebuild and pages show stale data. Omitting these files because \"it isn't a React app\" hides the frontend's entire migration cost, which is the one thing this file exists to expose.\n- The `designFindings` grep hints above are React-flavored; the server-rendered equivalents are inline `style=` attributes in templates, hardcoded hex in the stylesheet tree, per-locale forked templates instead of i18n, and design-system bypass = raw markup where a component/partial already exists. Same findings, different needles.\n\n### Phase 4 — Cross-check, validate, synthesize\n\n1. Cross-checks before writing: every command has an actor and at least one precondition or policy (or you explain why it is genuinely unguarded); every state transition appears in some command/workflow — if a state is only reachable by manual/DB intervention, record the transition with trigger `\"none (manual/DB-only)\"` AND add a gaps.json entry; every event has a producer; every api surface maps to a defined concept; every integration is used by some command/workflow.\n2. Write all 14 JSON files, sorting **unordered identity collections** by `name` (or `path`) — on first extraction too, not just re-runs. Arrays whose order carries meaning (`workflows[].steps`, and any other observed sequence) stay in their observed order: sorting them would rewrite the behavior you extracted. Run the validator: done means `0 error(s)` and exit 0, and every WARN explicitly reviewed and either fixed or justified in your summary.\n3. Write `blueprint.md`: product summary → domain map → per-domain narrative (entities/commands/events with the interesting rules) → workflows → integrations/architecture → invariants → gaps and open decisions → rebuild recommendation. Write it for the engineer who will rebuild the product and has never seen the legacy code.\n\n## Evidence & Confidence Discipline\n\n- Every concept object carries `evidence: [{file, lines, note}]` and `confidence`. (In `api.json` and `architecture.json`, `confidence` is optional — include it whenever a surface/component is inferred rather than directly observed, e.g. an SPA known only from comments.)\n- `high` — the behavior itself is in the cited code/schema/test.\n- `medium` — inferred from multiple converging signals (naming + partial code + a test name).\n- `low` — plausible reconstruction; MUST also be phrased tentatively in blueprint.md and usually deserves a `decisionsNeeded` entry.\n- Comments and docs describe *intended* behavior; code describes *actual* behavior. When they disagree, record the code's behavior as the fact and the disagreement as a gap.\n\n## Scaling up (large repos)\n\n- Fan out one subagent per lens (or per candidate domain) with: the survey notes, the schema file path, and instructions to return JSON fragments with evidence. Merge, dedupe by concept name, then run Phase 4 yourself.\n- Fix the domain cut YOURSELF after the survey and hand every lens agent the same canonical domain list — domains are the shared IDs everything cross-references.\n- Give each lens agent ownership of whole files (never two agents writing one file), and pair coupled files under one agent (commands+queries+api must share names).\n- **Budget a reconciliation pass — parallel agents WILL drift on names** (observed at real scale: 472 validator warnings from one 7-agent run). The pattern: rebuild `domains.json`'s entity/command/query/event/policy roll-up lists LAST, generated by grouping the authoritative files by their `domain` field — never hand-written before those files exist; reconcile `eventsProduced` against curated `events.json` by rename (spelling variant) / drop (CRUD noise, no consumer) / add (only with verified consumer evidence); fill empty command policies by joining `api.json`'s per-surface `auth` against `policies.json` names. The validator's warning list is the reconciliation worklist.\n- **Incremental re-runs:** keep concept names stable (they are the IDs). Re-run the affected lens only, diff against the existing `.knowledge/` files, and preserve unrelated entries verbatim. Sort every array by `name` (or `path`) so diffs are meaningful.\n\n## Red Flags — you are about to produce a worthless blueprint\n\n| Thought | Reality |\n|---|---|\n| \"I'll write it up as markdown docs\" | Only `blueprint.md` is prose. The 14 JSON files ARE the deliverable; a generator consumes them. |\n| \"The routes are the API contract\" | Routes are evidence. Lift each to a command/query or you've documented plumbing, not product. |\n| \"This is obvious, no citation needed\" | Uncited claims are indistinguishable from hallucinations. Evidence on everything. |\n| \"The folder structure tells me the domains\" | Folders are how it grew, not what it is. Derive domains from ownership + vocabulary. |\n| \"Tests are just tests, skip them\" | Tests are the spec. Some rules exist ONLY in tests. Phase 2 is mandatory. |\n| \"No events are emitted, so events: []\" | Reconstruct implicit events from side-effect clusters; mark `explicit: false`. |\n| \"Cron jobs aren't workflows\" | System workflows are workflows. Include schedules, queue consumers, webhook reactions. |\n| \"The frontend is just the web routes\" | The web UI is ONE interface. Inventory the CLI, MCP server, OpenAPI/SDK, realtime, webhooks in `interfaces.json` too — the product is driven by people, developers, AND agents. |\n| \"A component list is enough\" | Without the `rebuild` split, you've hidden the frontend's real cost. Flag every `custom-logic` component (chart/table/canvas/editor) and say what the logic is — that's the port work. |\n| \"I'll skip the validator, the JSON looks right\" | Run it. Missing domains refs, dangling event names, and undescribed custom-logic components are exactly what it catches. |\n\n## Quick Reference\n\n```bash\n# 1. survey + excavate + extract (you, with Read/Grep/subagents)\n# 2. write <repo>/.knowledge/*.json + blueprint.md per references/blueprint.schema.json\n# 3. validate:\nnode <skill-dir>/scripts/validate.mjs <repo>/.knowledge\n```\n\n- Schema/contract: `references/blueprint.schema.json`\n- How Pikku consumes the blueprint: `references/pikku-mapping.md`\n", "pikku-tag-middleware/SKILL.md": "---\nname: pikku-tag-middleware\ndescription: 'Deprecated — use pikku-middleware instead. Tag middleware (addTagMiddleware) is now documented as a section within the pikku-middleware skill, alongside global HTTP middleware, execution order, and the service-to-service bearer auth pattern.'\n---\n\n# Deprecated: use `pikku-middleware`\n\nTag middleware is covered in the **`pikku-middleware`** skill, which also covers:\n- `addHTTPMiddleware` (global / prefix-based)\n- `addTagMiddleware` (tag-scoped)\n- Middleware execution order and priority\n- Service-to-service bearer auth pattern\n- Session-setting middleware pattern\n", "pikku-template-clone/SKILL.md": "---\nname: pikku-template-clone\ndescription: 'Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `npm create pikku`, `git clone <template>`, or the user says \"I cloned the kanban template / starter / template\"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.'\nallowed-tools: Bash(git status *), Bash(git add *), Bash(git commit *), Bash(git rm *), Bash(git mv *), Bash(git log *)\ninstallGroups: [core]\n---\n\n# Pikku Template Post-Clone Cleanup\n\n## Agent Operating Procedure\n\nRun this **once**, right after a template is cloned or scaffolded into a new\nproject. The goal is to turn template scaffolding into a real project. Make the\nsmallest changes and land them as one focused `chore: post-clone cleanup`\ncommit, separate from any feature work.\n\n1. **Replace the template README.** The shipped `README.md` describes the\n _template_, not the user's project — leaving it in place is misleading.\n Either delete it (`git rm README.md`) or rewrite it with the new project's\n name and purpose. Never ship a clone with the generic template README.\n2. **Keep the lockfile committed.** Templates ship a committed `yarn.lock`; do\n NOT re-add `yarn.lock` to `.gitignore`. A real project commits its lockfile\n for reproducible installs. The correct pattern is `yarn.lock` followed by\n `!/yarn.lock`, which commits the root lockfile while keeping generated\n per-unit lockfiles under `.deploy/` (and `e2e/`) ignored.\n3. **Rename template identifiers.** Update `name` in the root `package.json`\n (and any `@project/*` or other placeholder names) to the real project.\n4. **Drop template-only artifacts.** Remove any `TEMPLATE.md`, demo docs, or\n placeholder content that only made sense for the template.\n\nDo not touch generated files (`.pikku/`, `*.gen.*`) or run a full reinstall as\npart of cleanup — this step is project hygiene, not a build.\n\n## Why this exists\n\nTemplates are structure-only starting points. Without this pass, clones carry a\nmisleading README, a placeholder package name, and (historically) a gitignored\nlockfile — all of which leak template assumptions into a real project. Running\nit immediately after clone keeps every Pikku project, OSS or Fabric, starting\nfrom a clean, honest baseline.\n", "pikku-trigger/SKILL.md": "---\nname: pikku-trigger\ndescription: >-\n Use when adding event-driven functions that respond to system events like Redis pub/sub,\n PostgreSQL LISTEN/NOTIFY, or custom event sources. Covers wireTrigger, wireTriggerSource, and\n pikkuTriggerFunc. TRIGGER when: code uses wireTrigger/wireTriggerSource/pikkuTriggerFunc, user\n asks about event-driven functions, Redis pub/sub, PostgreSQL LISTEN/NOTIFY, or reacting to\n external events. DO NOT TRIGGER when: user asks about scheduled tasks (use pikku-cron) or\n background job queues (use pikku-queue).\ninstallGroups: [core]\n---\n\n# Pikku Trigger Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions to fire when external events occur. Triggers connect event sources (Redis pub/sub, PostgreSQL LISTEN/NOTIFY, polling, webhooks) to Pikku functions.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their types\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireTrigger(config)`\n\nDefine the target function that handles trigger events:\n\n```typescript\nimport { wireTrigger } from '@pikku/core/trigger'\n\nwireTrigger({\n name: string, // Trigger name (matches source)\n func: PikkuFunc, // Function to call when event fires\n})\n```\n\n### `wireTriggerSource(config)`\n\nDefine the event source that fires triggers:\n\n```typescript\nimport { wireTriggerSource } from '@pikku/core/trigger'\n\nwireTriggerSource({\n name: string, // Must match wireTrigger name\n func: PikkuTriggerFunc, // Source function (sets up listener)\n input: object, // Configuration for the source\n})\n```\n\n### `pikkuTriggerFunc<TInput, TEvent>`\n\nDefine a trigger source function. Returns a cleanup function.\n\n```typescript\nimport { pikkuTriggerFunc } from '#pikku'\n\nconst source = pikkuTriggerFunc<\n InputType, // Configuration input\n EventType // Shape of events it emits\n>(async (services, input, { trigger }) => {\n // Set up listener...\n trigger.invoke(eventData) // Fire the trigger\n\n // Return cleanup function\n return async () => {\n /* teardown */\n }\n})\n```\n\n## Usage Patterns\n\n### Redis Pub/Sub Source\n\n```typescript\nconst redisSubscribe = pikkuTriggerFunc<\n { channels: string[] },\n { channel: string; message: any }\n>(async ({ redis }, { channels }, { trigger }) => {\n const subscriber = redis.duplicate()\n\n subscriber.on('message', (channel, message) => {\n trigger.invoke({ channel, message: JSON.parse(message) })\n })\n\n await subscriber.subscribe(...channels)\n\n return async () => {\n await subscriber.unsubscribe()\n await subscriber.quit()\n }\n})\n\n// Target function\nconst onOrderEvent = pikkuSessionlessFunc({\n title: 'On Order Event',\n func: async ({ db, logger }, { channel, message }) => {\n logger.info(`Order event on ${channel}`, message)\n await db.processOrderEvent(message)\n },\n})\n\n// Wire them together\nwireTrigger({\n name: 'order-events',\n func: onOrderEvent,\n})\n\nwireTriggerSource({\n name: 'order-events',\n func: redisSubscribe,\n input: { channels: ['orders:created', 'orders:updated'] },\n})\n```\n\n### Triggers vs Queues\n\n| Feature | Trigger | Queue |\n| ----------- | ---------------------------------- | ------------------------------ |\n| Execution | Synchronous, in-process | Async, distributed |\n| Reliability | At-most-once | At-least-once (with retries) |\n| Use case | React to events immediately | Reliable background processing |\n| Source | External systems (Redis, PG, etc.) | Enqueued programmatically |\n\nUse triggers for real-time reactions. Use queues for reliable, retryable background work.\n\n## Complete Example\n\n```typescript\n// functions/triggers.functions.ts\nconst pgListen = pikkuTriggerFunc<{ channel: string }, { payload: any }>(\n async ({ db }, { channel }, { trigger }) => {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(channel)) {\n throw new Error(`Invalid channel name: ${channel}`)\n }\n const client = await db.pool.connect()\n\n client.on('notification', (msg) => {\n trigger.invoke({ payload: JSON.parse(msg.payload) })\n })\n\n await client.query(`LISTEN ${channel}`)\n\n return async () => {\n await client.query(`UNLISTEN ${channel}`)\n client.release()\n }\n }\n)\n\nconst onUserCreated = pikkuSessionlessFunc({\n title: 'On User Created',\n func: async ({ emailService, logger }, { payload }) => {\n logger.info('New user created', { userId: payload.id })\n await emailService.sendWelcome(payload.email)\n },\n})\n\n// wirings/triggers.wiring.ts\nwireTrigger({ name: 'user-created', func: onUserCreated })\nwireTriggerSource({\n name: 'user-created',\n func: pgListen,\n input: { channel: 'user_created' },\n})\n```\n", "pikku-versioning/SKILL.md": "---\nname: pikku-versioning\ndescription: >-\n Use when versioning Pikku function contracts, detecting breaking changes, or managing API\n backward compatibility. Covers the version property, versions.pikku.json manifest, contract\n hashing, and CI integration. TRIGGER when: code uses version: on a pikkuFunc, user asks about\n API versioning, breaking changes, contract hashes, backward compatibility, or \"pikku versions\"\n CLI commands. DO NOT TRIGGER when: user asks about secrets/variables/OAuth2 (use pikku-config)\n or general function definitions (use pikku-concepts).\ninstallGroups: [core]\n---\n\n# Pikku Function Versioning\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nTrack and protect function contracts across releases. Pikku hashes each function's input/output schema into a manifest so you can detect breaking changes before they ship.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their versions\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## Function Versioning\n\nWhen you need to introduce a breaking change, keep the old function as a pinned version and let the new one become the latest.\n\n**The pattern:**\n\n1. Create a new file `my-function-v1.function.ts` — export a variable with the `V1` suffix\n2. Set `override: 'myFunction'` — this is the contract key the manifest groups under\n3. Set `version: 1` — pins this as version 1 of the contract\n4. The existing `my-function.function.ts` (no `version:` field) automatically becomes the latest version\n\n```typescript\n// my-function-v1.function.ts — old contract, kept for running workflows/agents\nexport const getBookV1 = pikkuFunc({\n override: 'getBook', // REQUIRED — links this to the 'getBook' contract family\n version: 1,\n input: z.object({ bookId: z.string() }),\n output: z.object({ title: z.string() }),\n func: async ({ db }, { bookId }) => {\n return db.getBook(bookId)\n },\n})\n\n// my-function.function.ts — latest contract, no version: field\nexport const getBook = pikkuFunc({\n input: z.object({\n bookId: z.string(),\n format: z.enum(['full', 'summary']),\n }),\n output: z.object({\n title: z.string(),\n author: z.string(),\n isbn: z.string(),\n }),\n func: async ({ db }, { bookId, format }) => {\n return db.getBook(bookId, format)\n },\n})\n```\n\n**Why `override` is required:** The manifest groups functions by a shared contract key. Without `override: 'getBook'`, `getBookV1` is stored internally as `getBookV1@v1` (key: `getBookV1`), which is a different contract family from `getBook`. With `override: 'getBook'`, it becomes `getBook@v1` (key: `getBook`), which groups with the unversioned `getBook` — and the unversioned one is automatically promoted to `getBook@v2`.\n\n## Version Manifest (`versions.pikku.json`)\n\nPikku tracks contract hashes to detect breaking changes:\n\n```json\n{\n \"manifestVersion\": 1,\n \"contracts\": {\n \"createTodo\": {\n \"latest\": 1,\n \"versions\": {\n \"1\": { \"inputHash\": \"a1b2c3d4\", \"outputHash\": \"e5f6a7b8\" }\n }\n },\n \"getTodos\": {\n \"latest\": 2,\n \"versions\": {\n \"1\": { \"inputHash\": \"i9j0k1l2\", \"outputHash\": \"m3n4o5p6\" },\n \"2\": { \"inputHash\": \"q7r8s9t0\", \"outputHash\": \"u1v2w3x4\" }\n }\n }\n }\n}\n```\n\nEach hash is derived from the function's input and output schemas plus the contract key. If a schema changes without a version bump, `pikku versions check` will fail.\n\n## CLI Commands\n\n```bash\nnpx pikku versions init # Initialize versioning manifest (run once)\nnpx pikku versions check # Detect contract changes (use in CI)\nnpx pikku versions update # Update contract hashes after version bump\n```\n\n**Workflow:**\n\n1. `pikku versions init` — run once to create `versions.pikku.json`\n2. Develop normally — add/modify functions\n3. `pikku versions check` — CI catches unversioned breaking changes\n4. If intentional: create `my-function-v1.function.ts` with `override` + `version: 1`, then `pikku versions update`\n\n## CI Integration\n\n```yaml\n# .github/workflows/ci.yml\nname: CI\non: [push, pull_request]\n\njobs:\n check:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: npm ci\n - run: npx pikku versions check\n```\n\n## Complete Example\n\n```typescript\n// create-todo-v1.function.ts — v1 locked contract\nexport const createTodoV1 = pikkuSessionlessFunc({\n override: 'createTodo', // groups under 'createTodo' contract family\n version: 1,\n input: z.object({ title: z.string() }),\n output: z.object({ id: z.string(), title: z.string() }),\n func: async ({ todoStore }, { title }) => todoStore.add(title),\n})\n\n// create-todo.function.ts — v2 (latest), called by default\nexport const createTodo = pikkuSessionlessFunc({\n input: z.object({\n title: z.string(),\n priority: z.enum(['low', 'medium', 'high']),\n }),\n output: z.object({\n id: z.string(),\n title: z.string(),\n priority: z.string(),\n }),\n func: async ({ todoStore }, { title, priority }) =>\n todoStore.add(title, priority),\n})\n```\n\nResult in manifest:\n\n```json\n\"createTodo\": {\n \"latest\": 2,\n \"versions\": {\n \"1\": { \"inputHash\": \"...\", \"outputHash\": \"...\" },\n \"2\": { \"inputHash\": \"...\", \"outputHash\": \"...\" }\n }\n}\n```\n", "pikku-websocket/SKILL.md": "---\nname: pikku-websocket\ndescription: >-\n Use when adding real-time features, WebSocket channels, live updates, chat, or pub/sub to a\n Pikku app. Covers wireChannel, action routing, auth, EventHub pub/sub, channel middleware, and\n generated WebSocket client. TRIGGER when: code uses wireChannel, user asks about WebSocket,\n real-time, live updates, chat, pub/sub, or the generated WebSocket client. DO NOT TRIGGER when:\n user asks about HTTP/REST (use pikku-http), SSE (use pikku-http with sse: true), or WebSocket\n deployment specifics (use pikku-deploy-uws).\n---\n\n# Pikku WebSocket Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions to WebSocket channels with structured message routing, auth per-action, pub/sub via EventHub, and auto-generated type-safe clients.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their types\npikku info tags --verbose # Understand project organization\n```\n\nFollow existing patterns. See `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireChannel(config)`\n\n```typescript\nimport { wireChannel } from '@pikku/core/channel'\n\nwireChannel({\n name: string, // Channel name (e.g. 'todos')\n onConnect: async () => {}, // Called when client connects\n onDisconnect: async () => {}, // Called when client disconnects\n onMessageWiring: { // Action → function mapping\n [actionName: string]: {\n func: PikkuFunc,\n auth?: boolean, // Override channel-level auth\n permissions?: Record<string, PikkuPermission | PikkuPermission[]>,\n }\n },\n channelMiddleware?: PikkuChannelMiddleware[],\n})\n```\n\n### `pikkuChannelMiddleware(fn)`\n\n```typescript\nimport { pikkuChannelMiddleware } from '@pikku/core'\n\nconst middleware = pikkuChannelMiddleware(async (services, event, next) => {\n // Transform or filter events before/after\n await next(event) // Pass modified event, or next(null) to drop\n})\n```\n\n### `addChannelMiddleware(domain, middlewares)`\n\n```typescript\naddChannelMiddleware('todos', [addTimestamp, filterSensitive])\n```\n\n## Usage Patterns\n\n### Basic Channel\n\n```typescript\nwireChannel({\n name: 'todos',\n onConnect: async () => {},\n onDisconnect: async () => {},\n onMessageWiring: {\n create: { func: createTodo },\n list: { func: listTodos, auth: false },\n },\n})\n```\n\n### Action Routing with Auth\n\nClients send `{ action: 'create', data: {...} }`. Pikku routes to the matching function.\n\n```typescript\nconst authenticate = pikkuFunc({\n title: 'Authenticate',\n func: async ({ setSession }, { token }) => {\n const session = await verifyJWT(token)\n setSession(session)\n return { success: true }\n },\n})\n\nwireChannel({\n name: 'todos',\n onConnect: async () => {},\n onDisconnect: async () => {},\n onMessageWiring: {\n auth: { func: authenticate, auth: false }, // No session required\n subscribe: { func: subscribeTodos }, // Session required\n create: { func: createTodo },\n },\n})\n```\n\n### Pub/Sub with EventHub\n\nUse EventHub for real-time broadcasting across connections:\n\n```typescript\nwireChannel({\n name: 'todos',\n onConnect: async ({ eventHub, channel }) => {\n eventHub.subscribe('todos:updated', (data) => {\n channel.send(data)\n })\n },\n onDisconnect: async () => {},\n onMessageWiring: {\n create: {\n func: pikkuFunc({\n title: 'Create Todo',\n func: async ({ db, eventHub }, { text }) => {\n const todo = await db.createTodo({ text })\n eventHub.publish('todos:updated', {\n event: 'created',\n todo,\n })\n return { todo }\n },\n }),\n },\n },\n})\n```\n\n### Channel Middleware\n\n```typescript\nconst addTimestamp = pikkuChannelMiddleware(\n async ({ logger }, event, next) => {\n logger.info({ phase: 'before-send', event })\n await next({ ...event, sentAt: Date.now() })\n }\n)\n\nconst filterSensitive = pikkuChannelMiddleware(\n async (_services, event, next) => {\n if (event.internal) return await next(null) // Drop event\n await next(event)\n }\n)\n\n// Apply globally to a domain\naddChannelMiddleware('todos', [addTimestamp, filterSensitive])\n\n// Or inline on wiring\nwireChannel({\n name: 'todos',\n channelMiddleware: [addTimestamp],\n onConnect: async () => {},\n onDisconnect: async () => {},\n onMessageWiring: { ... },\n})\n```\n\n### Generated WebSocket Client\n\nAfter `npx pikku all`:\n\n```typescript\nimport { PikkuWebSocket } from '.pikku/pikku-websocket.gen.js'\n\nconst pikku = new PikkuWebSocket(ws)\nconst todosRoute = pikku.getRoute('todos')\n\n// Send action (type-safe)\nconst result = await todosRoute.send('create', { text: 'Buy milk' })\n\n// Subscribe to events\ntodosRoute.subscribe('todos:updated', (data) => {\n console.log(data.event, data.todo)\n})\n```\n\n## Complete Example\n\n```typescript\n// functions/chat.functions.ts\nexport const authenticate = pikkuFunc({\n title: 'Authenticate',\n func: async ({ jwt }, { token }, { setSession }) => {\n const payload = await jwt.verify(token)\n setSession({ userId: payload.userId })\n return { success: true }\n },\n})\n\nexport const sendMessage = pikkuFunc({\n title: 'Send Message',\n func: async ({ db, eventHub }, { text }, { session }) => {\n const message = await db.createMessage({\n text,\n userId: session.userId,\n })\n eventHub.publish('chat:message', { message })\n return { message }\n },\n})\n\nexport const listMessages = pikkuSessionlessFunc({\n title: 'List Messages',\n func: async ({ db }, { limit }) => {\n return { messages: await db.listMessages(limit) }\n },\n})\n\n// wirings/chat.channel.ts\nwireChannel({\n name: 'chat',\n onConnect: async ({ eventHub, channel }) => {\n eventHub.subscribe('chat:message', (data) => {\n channel.send(data)\n })\n },\n onDisconnect: async () => {},\n onMessageWiring: {\n auth: { func: authenticate, auth: false },\n send: { func: sendMessage },\n history: { func: listMessages, auth: false },\n },\n})\n```\n", "pikku-workflow/references/workflow-reference.md": "# Pikku Workflow Reference\n\n## Step execution: inline vs queue dispatch\n\nWhether a step runs **inline** (same process/session, no queue round-trip) or is **dispatched to the queue** is decided **purely by the step's function** — there is no workflow-level or per-call `inline` flag. `workflow.do(...)` options are only `retries`/`retryDelay`/`description`.\n\n- **Steps default to inline.** Most steps don't need their own worker; running them inline avoids a queue round-trip per step, so a normally-started workflow executes its whole chain in one orchestrator pass.\n- **`inline: false` opts a function out.** Set `inline: false` on the **function config** (`pikkuFunc` / `pikkuSessionlessFunc`, same level as `auth`/`expose`) to dispatch that step via the queue — for expensive/long-running steps that deserve their own worker, retry isolation, and concurrency limits.\n- **Run-level `inline` is separate** and only controls whether the *whole run* executes in-process without queue infrastructure (set automatically when there is no `queueService`, or via `startWorkflow(..., { inline: true })`). It governs sleep handling, not per-step dispatch.\n\nThe rule (`dispatchStep`):\n\n| Function `inline` | `queueService` present? | Result |\n|---|---|---|\n| default / `true` | any | **inline** |\n| `false` | yes | **queued** (own worker) |\n| `false` | no | **inline + a `logger.warn`** (misconfiguration: can't dispatch) |\n\n```typescript\n// Push this one expensive step onto the queue; every other step stays inline:\nexport const renderLargeReport = pikkuSessionlessFunc({\n inline: false, // dispatch via queue instead of running inline\n input: ReportInput,\n output: ReportOutput,\n func: async (services, data) => { /* ... */ },\n})\n```\n\n`inline: false` requires a `queueService`; without one the step still runs (so the workflow progresses) but emits a `logger.warn` so the misconfiguration is visible.\n\n## HTTP workflow wiring (manual)\n\nUsually auto-scaffolded via `scaffold.workflow`. To wire by hand:\n\n```typescript\n// Start a workflow\nwireHTTP({ method: 'post', route: '/onboard', func: workflowStart('onboardUser') })\n\n// Execute workflow steps (called by the orchestrator)\nwireHTTP({ method: 'post', route: '/onboard/run', func: workflow('onboardUser') })\n\n// Check workflow status\nwireHTTP({ method: 'get', route: '/onboard/status/:runId', func: workflowStatus('onboardUser') })\n```\n\n## Suspend / resume example\n\n```typescript\nimport { z } from 'zod'\nimport { pikkuWorkflowFunc } from '#pikku/workflow/pikku-workflow-types.gen.js'\n\nexport const approval = pikkuWorkflowFunc({\n description: 'Submit a request and wait for approval',\n input: z.object({ requestId: z.string() }),\n output: z.object({ approved: z.boolean() }),\n func: async (services, data, { workflow }) => {\n await workflow.do('Submit request', 'submitRequest', data)\n await workflow.suspend('Awaiting approval') // pauses here until externally resumed\n const result = await workflow.do('Check result', 'getApprovalResult', data)\n return { approved: result.approved }\n },\n})\n```\n", "pikku-workflow/SKILL.md": "---\nname: pikku-workflow\ndescription: >-\n Use when building multi-step workflows, state machines, or orchestration pipelines with Pikku.\n Covers pikkuWorkflowFunc, workflow steps (do, sleep, suspend), graph workflows, and HTTP wiring.\n TRIGGER when: code uses pikkuWorkflowFunc/pikkuWorkflowGraph, user asks about workflows,\n multi-step processes, durable execution, suspend/resume, or DAG orchestration. DO NOT TRIGGER\n when: user asks about simple background jobs (use pikku-queue) or scheduled tasks (use\n pikku-cron).\ninstallGroups: [core]\n---\n\n# Pikku Workflow Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Capture baseline. Run `pikku-verify` (or `pikku all`) BEFORE writing code; note existing errors — only NEW errors are yours to fix.\n2. Discover before editing. Prefer `pikku-meta` / `pikku info functions --verbose` and `pikku info tags --verbose` to see functions usable as steps and project organization; inspect only the focused output you need.\n3. Identify the source files that own the behavior. Do not start from generated output, `.pikku`, `node_modules`, vendored packages, or build artifacts.\n4. Make the smallest source change. Keep generated files generated — never hand-edit SDKs, schema output, or typegen to paper over errors; fix the source cause.\n5. Validate with the narrowest relevant command, then re-run `pikku-verify`. If only files you did not touch still error, those are pre-existing — leave them unless asked.\n6. Call `pikku-workflow-view` only when `pikku-verify` fully passes (codegen AND type check both green) — never after a partial pass.\n\nSee `pikku-concepts` for the core mental model.\n\nBuild durable, multi-step workflows with automatic retry, sleep, suspend/resume, and parallel execution. Steps are cached for replay safety.\n\n## Choosing the right factory\n\n| Factory | When to use | Step-graph view? |\n|---|---|---|\n| `pikkuWorkflowFunc` | **Default for all new workflows.** Sequential + conditional logic; DSL mode (serialisable, replay-safe). ALL `const`/`let` declarations must be at the top level of the function body (not inside blocks). | ✅ Yes |\n| `pikkuWorkflowGraph` | DAG / fan-out with nodes and typed refs between them. | ✅ Yes |\n| `pikkuWorkflowComplexFunc` | Escape hatch only — arbitrary TypeScript, no top-level restriction (e.g. dynamic inline functions the DSL extractor cannot handle). | ❌ No (loses step-graph view) |\n\n**Default to `pikkuWorkflowFunc`.** Use `pikkuWorkflowGraph` ONLY with explicit user approval AND only for a genuine cyclic dependency or Node.js-only import DSL cannot express. Use `pikkuWorkflowComplexFunc` ONLY with explicit user approval — a last-resort escape hatch. Never switch to either just to dodge a PKU641 error; restructure the code instead.\n\n### PKU641 — DSL static analysis error\n\n`pikkuWorkflowFunc` statically analyzes the body: **every `const`/`let` must be top-level, not inside any block (`if`, `for`, `while`, …).** Assignments inside blocks are fine — only declarations trigger it.\n\n```typescript\n// ❌ PKU641 — declaration inside block\nif (priority === 'high') {\n const bugCard = await workflow.do(...)\n}\n\n// ✅ hoist the declaration, assign inside the block\nlet bugCard: Awaited<ReturnType<typeof workflow.do>>\nif (priority === 'high') {\n bugCard = await workflow.do(...)\n}\n```\n\n## Import path\n\n```typescript\n// CORRECT — workflow factories come from the generated types file\nimport { pikkuWorkflowFunc, pikkuWorkflowGraph, pikkuWorkflowComplexFunc } from '#pikku/workflow/pikku-workflow-types.gen.js'\n\n// WRONG — '#pikku' does not re-export them (TS2305)\nimport { pikkuWorkflowFunc } from '#pikku'\n```\n\n## Defining a workflow\n\nDeclare input/output as Zod schemas (like any function) — never TypeScript generic params (no `pikkuWorkflowFunc<In, Out>(...)`; that skips runtime validation). `data` is typed from the input schema.\n\n```typescript\nimport { z } from 'zod'\nimport { pikkuWorkflowFunc } from '#pikku/workflow/pikku-workflow-types.gen.js'\n\nconst ProcessOrderInput = z.object({ orderId: z.string(), amount: z.number() })\nconst ProcessOrderOutput = z.object({ status: z.string(), discount: z.number().optional() })\n\nexport const processOrder = pikkuWorkflowFunc({\n description: 'Process an order through payment and fulfillment',\n tags: ['orders'],\n input: ProcessOrderInput,\n output: ProcessOrderOutput,\n func: async (services, data, { workflow }) => {\n // Declare ALL variables at top level — even those only assigned in branches (PKU641)\n let discount: number | undefined\n let status: string\n\n if (data.amount > 1000) {\n const d = await workflow.do('Apply bulk discount', 'calcDiscount', { amount: data.amount })\n discount = d.discountPercent\n }\n\n const payment = await workflow.do('Charge', 'chargePayment', {\n orderId: data.orderId,\n amount: discount ? data.amount * (1 - discount / 100) : data.amount,\n })\n\n if (payment.success) {\n await workflow.do('Fulfill', 'fulfillOrder', { orderId: data.orderId })\n status = 'fulfilled'\n } else {\n status = 'payment-failed'\n }\n\n return { status, discount }\n },\n})\n```\n\n### Workflow step types\n\n```typescript\n// RPC step — run a registered Pikku function as a step (opts: retries, retryDelay, description)\nconst result = await workflow.do('Step name', 'rpcFunctionName', { ...data }, { retries: 3, retryDelay: '1s' })\n\n// Inline closure step — immediate execution, cached for replay\nconst msg = await workflow.do('Generate', async () => `Welcome, ${data.email}!`)\n\n// Sleep — durable pause (duration: '30s', '5min', '1h', '1d')\nawait workflow.sleep('Wait 5 minutes', '5min')\n\n// Suspend — pause until externally resumed (e.g. awaiting approval), then continue\nawait workflow.suspend('Awaiting approval')\n```\n\n### Parallel fan-out\n\n```typescript\nconst users = await Promise.all(\n data.userIds.map((userId) => workflow.do(`Fetch user ${userId}`, 'getUser', { userId }))\n)\n```\n\n### Graph workflow (DAG)\n\n`pikkuWorkflowGraph` derives types from the RPC map — no explicit `input`/`output`. Nodes map `nodeName → Pikku function name`; `config.<node>.next` lists nodes to run after it (in parallel); `config.<node>.input: (ref) => ...` transforms input using refs to prior node outputs.\n\n```typescript\nimport { pikkuWorkflowGraph } from '#pikku/workflow/pikku-workflow-types.gen.js'\n\nexport const userOnboarding = pikkuWorkflowGraph({\n description: 'Onboard a new user',\n nodes: {\n createProfile: 'createUserProfile',\n sendWelcome: 'sendEmail',\n setupDefaults: 'createDefaultTodos',\n },\n config: {\n createProfile: { next: ['sendWelcome', 'setupDefaults'] }, // run in parallel\n sendWelcome: {\n input: (ref) => ({ to: ref('createProfile', 'email'), subject: 'Welcome!' }),\n },\n },\n})\n```\n\n## File conventions\n\n- Place workflows in `packages/functions/src/wirings/*.workflow.ts`; export the variable so the inspector discovers it (no manual registration).\n- HTTP start/run/status routes are auto-scaffolded via `scaffold.workflow` in `pikku.config.json`.\n\n## Step dispatch & HTTP wiring\n\nFor per-step inline-vs-queue dispatch (`inline: false` and the `dispatchStep` rules), the manual `workflowStart`/`workflow`/`workflowStatus` HTTP wirings, and a suspend/resume example, read `references/workflow-reference.md`.\n\n## After writing\n\n1. `pikku-verify` (codegen + tsc).\n2. PKU641 → a `const`/`let` is inside a block; hoist it to the top of the function body.\n3. Import errors → use `#pikku/workflow/pikku-workflow-types.gen.js`, not `#pikku`.\n4. Type errors only in files you did not touch → pre-existing template errors; safe to ignore.\n5. Both green → call `pikku-workflow-view` with the workflow name.\n", "pikku-workflows-client/SKILL.md": "---\nname: pikku-workflows-client\ndescription: 'Run Pikku workflows from a React frontend and track their progress. Covers `useRunWorkflow` (run-and-wait), `useStartWorkflow` (fire-and-poll), and `useWorkflowStatus` (live status). TRIGGER when: a React component needs to invoke or display the status of a Pikku workflow, the user mentions long-running tasks / background jobs / progress UI tied to a workflow, or asks how to start/track a workflow from the client. DO NOT TRIGGER when: the user is wiring the workflow itself (use pikku-workflow) or only making regular RPC calls (use pikku-react-query).'\ninstallGroups: [core]\n---\n\n# Pikku Workflows — Client Hooks\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWhen a project has `pikkuWorkflowGraph` workflows, three React Query\nhooks are auto-generated alongside the standard RPC hooks. They handle\nthe two common shapes: **run-and-wait** (short workflows where the\nclient waits for the result) and **fire-and-poll** (long workflows where\nthe client gets a `runId` and polls status).\n\n## Discover what workflows exist\n\n```bash\nyarn pikku meta clients --json | jq '.workflows'\n```\n\nEach entry has `name`, `description`, `mode` (inline | distributed), plus\n`input` / `output` type names. Pass the workflow **name** to the hooks\nbelow.\n\n## Setup\n\nThese hooks are generated into the same `api.gen.ts` as `usePikkuQuery` —\nno extra setup beyond `PikkuProvider` + `QueryClientProvider` (see the\n**pikku-react** and **pikku-react-query** skills).\n\n## `useRunWorkflow(name, options?)` — run and wait\n\nFor short, synchronous-feeling workflows. Returns a mutation that\nresolves to the workflow's output.\n\n```tsx\nimport { useRunWorkflow } from './pikku/api.gen'\n\nfunction ChargeButton({ orderId }: { orderId: string }) {\n const run = useRunWorkflow('chargeOrder', {\n onSuccess: (output) => toast.success(`Charged: $${output.amount}`),\n })\n return (\n <button onClick={() => run.mutate({ orderId })} disabled={run.isPending}>\n {run.isPending ? 'Charging…' : 'Charge'}\n </button>\n )\n}\n```\n\nUse this when the workflow finishes in seconds and the UI can hold open\na loading state until done.\n\n## `useStartWorkflow(name, options?)` — fire-and-poll\n\nReturns a mutation that resolves to `{ runId: string }` immediately. The\nworkflow keeps running on the server. Pair with `useWorkflowStatus` to\nrender progress.\n\n```tsx\nconst start = useStartWorkflow('processVideo', {\n onSuccess: ({ runId }) => setActiveRunId(runId),\n})\n\nstart.mutate({ videoId: '123' })\n```\n\nUse this for long-running workflows (uploads, batch jobs, AI generation,\nanything you'd want a progress bar for).\n\n## `useWorkflowStatus(workflowName, runId, options?)` — observe\n\nPolls the workflow runtime for a run's status. Returns a typed status\nobject with `status`, optional `output`, and optional `error`.\n\n```tsx\nimport { useWorkflowStatus } from './pikku/api.gen'\n\nfunction VideoStatus({ runId }: { runId: string }) {\n const { data: status } = useWorkflowStatus('processVideo', runId, {\n refetchInterval: (query) =>\n query.state.data?.status === 'running' ? 1000 : false,\n })\n\n if (!status) return null\n if (status.status === 'running') return <Spinner />\n if (status.status === 'completed') return <Result {...status.output} />\n if (status.status === 'failed')\n return <Error message={status.error?.message} />\n return null\n}\n```\n\nStatus values: `'running' | 'suspended' | 'completed' | 'failed' | 'cancelled'`.\n\nThe hook stops auto-polling when the run reaches a terminal state (set\n`refetchInterval` to false in those cases — pattern shown above).\n\n## Putting it together — start + observe\n\n```tsx\nfunction ProcessVideoFlow({ videoId }: { videoId: string }) {\n const [runId, setRunId] = useState<string>()\n const start = useStartWorkflow('processVideo', {\n onSuccess: ({ runId }) => setRunId(runId),\n })\n const status = useWorkflowStatus('processVideo', runId)\n\n if (!runId) {\n return (\n <button\n onClick={() => start.mutate({ videoId })}\n disabled={start.isPending}\n >\n Start\n </button>\n )\n }\n return <ProgressBar status={status.data?.status} />\n}\n```\n\n## Backend: streaming richer progress\n\nThe status hook returns a coarse-grained state machine (`running`,\n`completed`, etc.). For step-by-step updates inside a long workflow,\npublish events from the workflow itself via `eventHub` or open a\nWebSocket channel — out of scope for this skill (see workflow + channel\ndocs).\n\n## What NOT to do\n\n- Don't poll status manually — use `useWorkflowStatus` with\n `refetchInterval`. It dedupes and stops on terminal states.\n- Don't call `useRunWorkflow` for workflows that take more than a few\n seconds. The user-facing component will hold a long-running pending\n state with no progress indication; use start + status instead.\n- Don't use these hooks for non-workflow RPCs — they only resolve\n workflow-shaped names. Regular RPCs go through `usePikkuQuery` /\n `usePikkuMutation`.\n", "pikku-ws/SKILL.md": "---\nname: pikku-ws\ndescription: >-\n Use when setting up a WebSocket server with the ws library in a Pikku app. Covers the ws runtime\n adapter for Pikku channels. TRIGGER when: code uses @pikku/ws, user asks about ws library\n WebSocket server, or Node.js WebSocket runtime. DO NOT TRIGGER when: user asks about WebSocket\n wiring/channels (use pikku-websocket) or uWebSockets (use pikku-deploy-uws).\n---\n\n# Pikku WS (WebSocket Server Runtime)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/ws` provides a WebSocket server runtime using the [ws](https://github.com/websockets/ws) library, connecting Pikku's channel system to a Node.js WebSocket server.\n\n## Installation\n\n```bash\nyarn add @pikku/ws ws\n```\n\n## Usage Patterns\n\n### Basic Setup\n\n```typescript\nimport { PikkuWSServer } from '@pikku/ws'\n\nconst wsServer = new PikkuWSServer({\n server: httpServer, // Node.js HTTP server\n singletonServices,\n createWireServices,\n channelStore,\n})\n\nawait wsServer.init()\n```\n\nThis runtime bridges the `ws` WebSocket library with Pikku's channel wiring. See `pikku-websocket` for channel wiring details and `pikku-deploy-fastify`/`pikku-deploy-express` for integrating with HTTP servers.\n" };
7
+ export const SKILL_FILES = { "pikku-addon/references/addon-package-manifest.md": "# Addon Package Manifest Reference\n\n`npx pikku new addon` scaffolds these files. You rarely hand-edit them — consult this when wiring exports or config by hand.\n\n## Package Structure\n\n```text\nmy-addon/\n├── package.json # Exports .pikku/* and dist/\n├── pikku.config.json # addon: true + metadata\n├── tsconfig.json # #pikku path mapping\n├── src/\n│ ├── services.ts # createSingletonServices (required)\n│ └── functions/\n│ └── *.function.ts # Function definitions\n├── types/\n│ └── application-types.d.ts # SingletonServices interface\n└── .pikku/ # Generated (gitignored)\n```\n\n## pikku.config.json\n\n```json\n{\n \"tsconfig\": \"./tsconfig.json\",\n \"srcDirectories\": [\"src\", \"types\"],\n \"outDir\": \"./.pikku\",\n \"addon\": true,\n \"node\": {\n \"displayName\": \"My Addon\",\n \"description\": \"What this addon does\",\n \"categories\": [\"General\"]\n }\n}\n```\n\n## package.json (key fields)\n\n```json\n{\n \"name\": \"@my-org/addon-todos\",\n \"imports\": {\n \"#pikku\": \"./.pikku/pikku-types.gen.ts\",\n \"#pikku/*\": \"./.pikku/*\"\n },\n \"exports\": {\n \".\": { \"types\": \"./dist/src/index.d.ts\", \"import\": \"./dist/src/index.js\" },\n \"./.pikku/*\": \"./.pikku/*\",\n \"./.pikku/pikku-metadata.gen.json\": \"./.pikku/pikku-metadata.gen.json\",\n \"./.pikku/rpc/pikku-rpc-wirings-map.internal.gen.js\": {\n \"types\": \"./.pikku/rpc/pikku-rpc-wirings-map.internal.gen.d.ts\"\n }\n },\n \"files\": [\"dist\", \".pikku\"],\n \"peerDependencies\": {\n \"@pikku/core\": \"*\"\n },\n \"scripts\": {\n \"pikku\": \"pikku all\",\n \"build\": \"tsc && cp -r .pikku dist/\"\n }\n}\n```\n", "pikku-addon/SKILL.md": "---\nname: pikku-addon\ndescription: >-\n Use when creating or consuming reusable function packages (addons) in Pikku. Covers wireAddon,\n addon(), pikkuAddonServices, pikkuAddonWireServices, addon package structure, and cross-project\n function sharing. TRIGGER when: code uses wireAddon/addon()/pikkuAddonServices, user asks about\n addons, reusable function packages, cross-project sharing, or addon package structure. DO NOT\n TRIGGER when: user asks about internal function composition (use pikku-rpc) or general function\n definitions (use pikku-concepts).\ninstallGroups: [core]\n---\n\n# Pikku Addons\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nAddons are reusable Pikku function packages that can be shared across projects. They bundle functions, services, secrets, and variables into a self-contained NPM package.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and addons\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireAddon(config)`\n\nRegister an addon in the consuming project:\n\n```typescript\nimport { wireAddon } from '#pikku'\n\nwireAddon({\n name: string, // Namespace for addon functions (e.g. 'todos')\n package: string, // NPM package name (e.g. '@pikku/addon-todos')\n rpcEndpoint?: string, // Optional remote RPC endpoint for distributed execution\n auth?: boolean, // Whether addon functions require authentication\n tags?: string[], // Tags applied to all addon functions\n secretOverrides?: Record<string, string>, // Remap secret names\n variableOverrides?: Record<string, string>, // Remap variable names\n})\n```\n\n### `addon(name)`\n\nType-safe reference to an addon function — use when wiring to HTTP, agents, etc.:\n\n```typescript\nimport { addon } from '#pikku'\n\naddon('todos:addTodo') // Returns a typed function config\naddon('emails:sendEmail') // Namespace:functionName format\n```\n\n### `pikkuAddonServices(factory)`\n\nDefine singleton services for an addon package (created once at startup):\n\n```typescript\nimport { pikkuAddonServices } from '#pikku'\n\nexport const createSingletonServices = pikkuAddonServices(\n async (config, parentServices?) => {\n // parentServices: logger, variables, secrets from the consuming app\n return {\n myStore: new MyStore(),\n }\n }\n)\n```\n\n### `pikkuAddonWireServices(factory)`\n\nDefine per-request services for an addon package (created fresh per HTTP request, queue job, etc.):\n\n```typescript\nimport { pikkuAddonWireServices } from '#pikku'\n\nexport const createWireServices = pikkuAddonWireServices(\n async (singletonServices, wire) => {\n // wire: transport context (http, channel, session, etc.)\n const authHeader = wire.http?.request?.header('authorization')\n return {\n myService: new MyService(authHeader),\n }\n }\n)\n```\n\n## Creating an Addon\n\n### Scaffold\n\n```bash\nnpx pikku new addon\n```\n\nThis generates `package.json` (exports `.pikku/*` + `dist/`), `pikku.config.json` (`addon: true`), `tsconfig.json` (`#pikku` path mapping), `src/services.ts`, `src/functions/`, and `types/application-types.d.ts`. For the full file contents/exports you rarely hand-edit, read `references/addon-package-manifest.md`.\n\n### Services\n\n```typescript\n// src/services.ts\nimport { pikkuAddonServices, pikkuAddonWireServices } from '#pikku'\nimport { TodoStore } from './todo-store.service.js'\n\nexport const createSingletonServices = pikkuAddonServices(async () => {\n const todoStore = new TodoStore()\n return { todoStore }\n})\n\n// Optional — only needed if addon functions require per-request services\nexport const createWireServices = pikkuAddonWireServices(\n async (singletonServices, wire) => {\n return {}\n }\n)\n```\n\n### Functions\n\n```typescript\n// src/functions/addTodo.function.ts\nimport { z } from 'zod'\nimport { pikkuSessionlessFunc } from '#pikku'\n\nconst AddTodoInput = z.object({ title: z.string() })\nconst AddTodoOutput = z.object({ id: z.string(), title: z.string() })\n\nexport const addTodo = pikkuSessionlessFunc({\n description: 'Adds a new todo',\n input: AddTodoInput,\n output: AddTodoOutput,\n func: async ({ todoStore }, { title }) => {\n return todoStore.add(title)\n },\n})\n```\n\nOptional approval gating (e.g. for agent tools) — add `approvalRequired: true` plus an `approvalDescription` resolver:\n\n```typescript\napprovalRequired: true,\napprovalDescription: async (_services, { title }) => `Add a todo called \"${title}\"`,\n```\n\n### Build\n\n```bash\nnpx pikku all # Generate types\nyarn tsc # Compile TypeScript\ncp -r .pikku dist/ # Include generated files in dist\n```\n\n## Consuming an Addon\n\n### Install & Register\n\n```bash\nyarn add @my-org/addon-todos\n```\n\n```typescript\n// wirings/todos.wirings.ts\nimport { wireAddon } from '#pikku'\n\nwireAddon({ name: 'todos', package: '@my-org/addon-todos' })\n```\n\nAfter registration, run `npx pikku all` to generate types for the addon's functions.\n\n### Call via RPC\n\n```typescript\nexport const myFunc = pikkuFunc({\n func: async (_services, data, { rpc }) => {\n const todo = await rpc.invoke('todos:addTodo', { title: 'Buy milk' })\n return todo\n },\n})\n```\n\n### Wire to HTTP\n\n```typescript\nimport { wireHTTP, addon } from '#pikku'\n\nwireHTTP({\n method: 'get',\n route: '/todos',\n func: addon('todos:listTodos'),\n auth: false,\n})\n```\n\nOr batch multiple addon routes with `defineHTTPRoutes` + `wireHTTPRoutes`:\n\n```typescript\nimport { wireHTTPRoutes, defineHTTPRoutes, addon } from '#pikku'\n\nconst todoRoutes = defineHTTPRoutes({\n tags: ['todos'],\n auth: false,\n routes: {\n list: { method: 'get', route: '/todos', func: addon('todos:listTodos') },\n add: { method: 'post', route: '/todos', func: addon('todos:addTodo') },\n },\n})\n\nwireHTTPRoutes({ basePath: '/api', routes: { todos: todoRoutes } })\n```\n\n### Use in AI Agents\n\n```typescript\nimport { pikkuAIAgent } from '#pikku'\nimport { addon } from '#pikku'\n\nexport const todoAgent = pikkuAIAgent({\n name: 'todo-agent',\n description: 'Manages a todo list',\n instructions: 'You help users manage their todos.',\n model: 'openai/gpt-4o',\n tools: [\n addon('todos:listTodos'),\n addon('todos:addTodo'),\n addon('todos:deleteTodo'),\n ],\n maxSteps: 5,\n})\n```\n", "pikku-ai-agent/SKILL.md": "---\nname: pikku-ai-agent\ndescription: >-\n Use when building AI agents, chatbots, or LLM-powered assistants with Pikku. Covers\n pikkuAIAgent, tool registration, memory, streaming, and agent invocation. TRIGGER when: code\n uses pikkuAIAgent/runAIAgent/streamAIAgent, user asks about AI agents, chatbots, LLM assistants,\n tool-calling agents, or agent memory/streaming. DO NOT TRIGGER when: user asks about MCP tool\n exposure (use pikku-mcp) or general function definitions (use pikku-concepts).\ninstallGroups: [core]\n---\n\n# Pikku AI Agent Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nBuild AI agents that use Pikku functions as tools. Agents support conversation memory, streaming, and multi-step tool execution.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions that can be used as agent tools\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `pikkuAIAgent(config)`\n\n```typescript\nimport { pikkuAIAgent } from '#pikku'\n\npikkuAIAgent({\n name: string, // Unique agent identifier\n description: string, // What the agent does\n instructions: string | string[], // System prompt / behavior instructions\n model: string, // LLM model (e.g. 'openai/gpt-5-mini')\n tools?: PikkuFunc[], // Pikku functions the agent can call\n agents?: AIAgentConfig[], // Sub-agents this agent can delegate to\n memory?: {\n storage?: string, // Service name for persistence (e.g. 'aiStorage')\n vector?: string, // Vector store service name\n embedder?: string, // Embedding service name\n lastMessages?: number, // How many messages to retain in context\n workingMemory?: ZodSchema, // Schema for structured working memory\n },\n maxSteps?: number, // Max tool-call rounds per invocation\n temperature?: number, // LLM temperature (0-1)\n toolChoice?: 'auto' | 'required' | 'none',\n input?: ZodSchema, // Input validation schema\n output?: ZodSchema, // Output validation schema\n tags?: string[], // For grouping and middleware targeting\n aiMiddleware?: PikkuAIMiddlewareHooks[], // AI-specific middleware\n middleware?: PikkuMiddleware[],\n permissions?: PermissionGroup,\n})\n```\n\n### `runAIAgent(name, input, options)` — Non-streaming\n\n```typescript\nconst result = await runAIAgent(\n agentName,\n {\n message: string, // User message\n threadId: string, // Conversation thread ID\n resourceId: string, // User/resource identifier\n },\n { singletonServices }\n)\n\nresult.text // Agent's text response\nresult.steps // Array of tool calls made\nresult.usage // Token usage { inputTokens, outputTokens }\n```\n\n### `streamAIAgent(name, input, channel, options)` — Streaming\n\n```typescript\nawait streamAIAgent(\n agentName,\n {\n message: string,\n threadId: string,\n resourceId: string,\n },\n channel,\n { singletonServices }\n)\n\n// Channel receives events:\n// { type: 'step-start', stepNumber: 1 }\n// { type: 'text-delta', text: '...' }\n// { type: 'reasoning-delta', text: '...' }\n// { type: 'tool-call', toolCallId, toolName, args }\n// { type: 'tool-result', toolCallId, toolName, result }\n// { type: 'agent-call', agentName, session, input }\n// { type: 'agent-result', agentName, session, result }\n// { type: 'approval-request', toolCallId, toolName, args, reason? }\n// { type: 'usage', tokens: { input, output }, model }\n// { type: 'error', message }\n// { type: 'done' }\n```\n\n## Usage Patterns\n\n### Define an Agent\n\n```typescript\nconst todoAssistant = pikkuAIAgent({\n name: 'todo-assistant',\n description: 'A helpful assistant that manages todos',\n instructions:\n 'You help users manage their todo lists. Be concise and helpful.',\n model: 'openai/gpt-5-mini',\n tools: [listTodos, createTodo, completeTodo],\n memory: {\n storage: 'aiStorage',\n lastMessages: 20,\n },\n maxSteps: 5,\n temperature: 0.7,\n})\n```\n\n### Invoke Non-Streaming\n\n```typescript\nconst result = await runAIAgent(\n 'todo-assistant',\n {\n message: 'Create a task for tomorrow: buy groceries',\n threadId: 'thread-123',\n resourceId: 'user-456',\n },\n { singletonServices }\n)\n\nconsole.log(result.text) // \"I've created a task 'buy groceries' for tomorrow.\"\nconsole.log(result.steps) // [{ tool: 'createTodo', args: {...}, result: {...} }]\nconsole.log(result.usage) // { inputTokens: 150, outputTokens: 42 }\n```\n\n### Stream Responses\n\n```typescript\nawait streamAIAgent(\n 'todo-assistant',\n {\n message: 'Create a task for tomorrow',\n threadId: 'thread-123',\n resourceId: 'user-456',\n },\n channel,\n { singletonServices }\n)\n```\n\n## Complete Example\n\n```typescript\n// functions/todos.functions.ts\nexport const listTodos = pikkuSessionlessFunc({\n description: 'List all todo items',\n func: async ({ db }, { status }) => {\n return { todos: await db.listTodos(status) }\n },\n})\n\nexport const createTodo = pikkuFunc({\n description: 'Create a new todo item',\n func: async ({ db }, { text, priority, dueDate }) => {\n return await db.createTodo({ text, priority, dueDate })\n },\n})\n\nexport const completeTodo = pikkuFunc({\n description: 'Mark a todo as complete',\n func: async ({ db }, { todoId }) => {\n return await db.completeTodo(todoId)\n },\n})\n\n// agents/todo-assistant.ts\nconst todoAssistant = pikkuAIAgent({\n name: 'todo-assistant',\n description: 'A helpful assistant that manages todos',\n instructions: `You help users manage their todo lists.\n - Be concise and helpful\n - When creating todos, infer priority if not specified\n - When listing todos, summarize the results`,\n model: 'openai/gpt-5-mini',\n tools: [listTodos, createTodo, completeTodo],\n memory: {\n storage: 'aiStorage',\n lastMessages: 20,\n },\n maxSteps: 5,\n temperature: 0.7,\n})\n\n// Wire to HTTP for chat endpoint\nwireHTTP({\n method: 'post',\n route: '/chat',\n func: pikkuFunc({\n title: 'Chat',\n func: async (services, { message, threadId }, wire) => {\n const { session } = wire\n return await runAIAgent(\n 'todo-assistant',\n {\n message,\n threadId,\n resourceId: session.userId,\n },\n { singletonServices: services }\n )\n },\n }),\n})\n```\n", "pikku-ai-vercel/SKILL.md": "---\nname: pikku-ai-vercel\ndescription: >-\n Use when setting up AI agent execution with the Vercel AI SDK in a Pikku app. Covers\n VercelAIAgentRunner for streaming and non-streaming AI agent steps. TRIGGER when: code uses\n VercelAIAgentRunner, user asks about Vercel AI SDK integration, AI agent runners, or\n @pikku/ai-vercel. DO NOT TRIGGER when: user asks about AI agent wiring (use pikku-ai-agent) or\n voice I/O (use pikku-ai-voice).\ninstallGroups: [core]\n---\n\n# Pikku AI Vercel (Agent Runner)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/ai-vercel` provides an AI agent runner backed by the [Vercel AI SDK](https://sdk.vercel.ai/). Implements `AIAgentRunnerService` from `@pikku/core`.\n\n## Installation\n\n```bash\nyarn add @pikku/ai-vercel ai @ai-sdk/openai # or any AI SDK provider\n```\n\n## API Reference\n\n### `VercelAIAgentRunner`\n\n```typescript\nimport { VercelAIAgentRunner } from '@pikku/ai-vercel'\n\nconst runner = new VercelAIAgentRunner(\n providers: Record<string, any> // Map of provider name → Vercel AI SDK provider instance\n)\n```\n\n**Methods:**\n\n- `stream(params: AIAgentRunnerParams, channel: AIStreamChannel): Promise<AIAgentStepResult>` — Stream AI responses with tool calls\n- `run(params: AIAgentRunnerParams): Promise<AIAgentStepResult>` — Execute a single AI step (non-streaming)\n\nThe `providers` map lets you register multiple AI providers. Model strings use `provider:model` format (e.g., `\"openai:gpt-4o\"`).\n\n## Usage Patterns\n\n### Basic Setup\n\n```typescript\nimport { VercelAIAgentRunner } from '@pikku/ai-vercel'\nimport { openai } from '@ai-sdk/openai'\nimport { anthropic } from '@ai-sdk/anthropic'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const aiRunner = new VercelAIAgentRunner({\n openai: openai,\n anthropic: anthropic,\n })\n return { config, aiRunner }\n})\n```\n\n### With AI Agent Wiring\n\n```typescript\nimport { wireAIAgent } from '@pikku/core/ai-agent'\n\nwireAIAgent({\n name: 'assistant',\n model: 'openai:gpt-4o',\n systemPrompt: 'You are a helpful assistant.',\n func: myAgentFunc,\n})\n```\n\nThe `VercelAIAgentRunner` is used internally by Pikku's AI agent wiring to execute model calls. See `pikku-ai-agent` for wiring details.\n", "pikku-ai-voice/SKILL.md": "---\nname: pikku-ai-voice\ndescription: >-\n Use when adding voice input (speech-to-text) or voice output (text-to-speech) to AI agents in a\n Pikku app. Covers voiceInput/voiceOutput middleware hooks and STT/TTS service interfaces.\n TRIGGER when: code uses voiceInput, voiceOutput, STTService, TTSService, or user asks about\n voice, speech-to-text, text-to-speech, or @pikku/ai-voice. DO NOT TRIGGER when: user asks about\n AI agent wiring (use pikku-ai-agent) or Vercel AI SDK (use pikku-ai-vercel).\n---\n\n# Pikku AI Voice (Speech I/O)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/ai-voice` provides speech-to-text and text-to-speech middleware hooks for Pikku AI agents.\n\n## Installation\n\n```bash\nyarn add @pikku/ai-voice\n```\n\n## API Reference\n\n### Service Interfaces\n\n```typescript\ninterface STTService {\n transcribe(\n audio: Uint8Array,\n options?: { language?: string; format?: string }\n ): Promise<string>\n}\n\ninterface TTSService {\n synthesize(\n text: string,\n options?: { voice?: string; format?: string }\n ): Promise<Uint8Array>\n synthesizeStream?(\n text: string,\n options?: { voice?: string; format?: string }\n ): AsyncIterable<Uint8Array>\n}\n```\n\n### Middleware Hooks\n\n```typescript\nimport { voiceInput, voiceOutput } from '@pikku/ai-voice'\n\nvoiceInput(config?: { language?: string }): PikkuAIMiddlewareHooks\nvoiceOutput(config?: { format?: string; voice?: string }): PikkuAIMiddlewareHooks\n```\n\nThese return middleware hooks that can be attached to AI agent wirings to automatically transcribe audio input and synthesize audio output.\n\n## Usage Patterns\n\n### Voice-Enabled Agent\n\n```typescript\nimport { voiceInput, voiceOutput } from '@pikku/ai-voice'\nimport { wireAIAgent } from '@pikku/core/ai-agent'\n\nwireAIAgent({\n name: 'voice-assistant',\n model: 'openai:gpt-4o',\n systemPrompt: 'You are a voice assistant.',\n middlewareHooks: [\n voiceInput({ language: 'en' }),\n voiceOutput({ voice: 'alloy', format: 'mp3' }),\n ],\n func: myAgentFunc,\n})\n```\n\n### Custom STT/TTS Services\n\nImplement the `STTService` and `TTSService` interfaces with your provider (OpenAI Whisper, ElevenLabs, etc.) and register them as singleton services.\n", "pikku-audit/SKILL.md": "---\nname: pikku-audit\ndescription: >-\n Use when adding audit / activity-history / change-tracking to a Pikku app, or when a function\n needs to record who changed what. Covers the built-in AuditService sink, the per-invocation\n auditLog buffer (createInvocationAudit / pikkuWireServices), the `audit: true` function flag,\n explicit `auditLog.write()` domain events, automatic query-level capture via\n createAuditedKysely, and the durable KyselyAuditService sink. TRIGGER when: user asks for an\n audit log, change history, activity feed, \"who did this\", or a custom audit/history table; code\n uses auditLog, createInvocationAudit, createAuditedKysely, or AuditService. DO NOT TRIGGER when:\n user wants app logging/telemetry (use the logger) or DB migrations in general (use\n pikku-kysely).\ninstallGroups: [core]\n---\n\n# Pikku Audit\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Check how services are wired (`services.ts`) and whether an `audit` table migration exists before adding audit calls.\n2. NEVER hand-roll a custom `audit_log` / history table with direct `insertInto('audit_log')` calls. The framework owns audit. A bespoke table drifts from the runtime (missing actor/trace/wire context, hand-written CHECK constraints that reject valid events, no prod sink). Use the built-in path below.\n3. Make the smallest source change: mark the function `audit: true`, inject `auditLog`, call `auditLog.write(...)`. Do not invent a new service.\n4. Validate with `pikku all` (regenerates the service flags) then run the app / e2e.\n\n## Mental model — two layers\n\n- **`audit` (singleton `AuditService`)** — the durable **sink**. Write-only: `audit(event)` + optional `write(batch)`. Defaults to `NoopAuditService` (discards). Swap in a real sink to persist (see Sinks).\n- **`auditLog` (wire service `AuditLog`)** — a per-invocation **buffer** built from the sink via `createInvocationAudit(audit, wire)`. `auditLog.write(input)` enriches each event with `functionId`, `wireType`, `traceId`, `occurredAt`, and `actor` (from the wire session) automatically, then flushes to the sink when the invocation ends.\n\nAn event only persists when the function opts in with **`audit: true`** — otherwise `auditLog` is a no-op that warns.\n\n## Wiring (services.ts)\n\n```typescript\nimport { NoopAuditService, createInvocationAudit } from '@pikku/core/services'\n\nexport const createSingletonServices = pikkuServices(async (config, existing) => {\n // Prod platforms may inject a queue-backed sink as existing.audit.\n const audit = existing?.audit ?? new NoopAuditService()\n return { ...existing, config, /* ... */ audit }\n})\n\n// auditLog is created per invocation from the sink. Returned unconditionally so\n// a write from a function that forgot `audit: true` warns instead of vanishing.\nexport const createWireServices = pikkuWireServices(async (services, wire) => {\n if (!services.audit) return {}\n return { auditLog: createInvocationAudit(services.audit, wire) }\n})\n```\n\n`audit` and `auditLog` are already declared on `CoreSingletonServices` / `CoreServices`, so no type change is needed to inject them.\n\n## Recording events — explicit domain events (default)\n\nMark the function `audit: true` and call `auditLog?.write(...)`. Domain history goes in `metadata`; the actor is derived from the session, so do NOT pass it manually.\n\n```typescript\nexport const cancelInvoice = pikkuFunc({\n audit: true, // REQUIRED — else write() is a no-op\n input: CancelInvoiceInput,\n output: CancelInvoiceOutput,\n func: async ({ kysely, auditLog }, { invoiceId }, { session }) => {\n const inv = await kysely.selectFrom('invoice')/* ... */.executeTakeFirstOrThrow()\n await kysely.updateTable('invoice').set({ status: 'cancelled' })/* ... */.execute()\n\n await auditLog?.write({\n type: 'invoice.update',\n source: 'explicit',\n metadata: {\n entity: 'invoice',\n entityId: invoiceId,\n action: 'update',\n field: 'status',\n before: inv.status,\n after: 'cancelled',\n },\n })\n return { ok: true }\n },\n})\n```\n\nFor a **system/cron** function there is no session, so `actor` is simply absent (nulls out `actor_user_id`). Use `pikkuVoidFunc({ audit: true, func: async ({ auditLog }) => { ... } })` — the void/config form accepts `audit`.\n\nHelper functions (in `lib/`) that record audit take `auditLog?: AuditLog` in their services arg and are passed it from a `audit: true` caller — never import a service.\n\nNote: events buffer and flush on invocation close. For a write inside a DB transaction, call `auditLog.write()` **after** the transaction commits — the sink is not part of your `trx`, so only record committed state.\n\n## Recording events — automatic query capture (optional)\n\nTo audit every DB mutation without explicit calls, wrap kysely so each query emits an event. Note this captures table/column changes only — it cannot see semantic events that do no DB write (e.g. \"email sent\"), so combine with explicit writes when you need those.\n\n```typescript\nimport { createAuditedKysely } from '@pikku/kysely'\nexport const createWireServices = pikkuWireServices(async (services, wire) => {\n if (!services.audit) return {}\n const auditLog = createInvocationAudit(services.audit, wire)\n return { auditLog, kysely: createAuditedKysely(services.kysely, { audit: auditLog }) }\n})\n```\n\n## Sinks\n\n- **`NoopAuditService`** (`@pikku/core/services`) — default; discards events. Fine when audit isn't needed.\n- **`KyselyAuditService`** (`@pikku/kysely`) — durable: persists events to an `audit` table via kysely. Use as the local/dev sink so events are queryable without a platform queue: `new KyselyAuditService(kysely)`.\n- **Platform-injected sink** — a deploy platform may inject its own queue-backed `audit` (hence the `existing?.audit ??` fallback above). Its rows land in the same `audit` table shape.\n\n### The `audit` table (add this migration if you persist audit)\n\n```sql\nCREATE TABLE IF NOT EXISTS audit (\n audit_id TEXT NOT NULL PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n occurred_at TEXT NOT NULL DEFAULT (datetime('now')),\n type TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'auto',\n outcome TEXT,\n function_id TEXT,\n wire_type TEXT,\n trace_id TEXT,\n transaction_id TEXT,\n query_id TEXT,\n actor_user_id TEXT,\n actor_org_id TEXT,\n tables TEXT, -- JSON: table names touched (auto capture)\n changed_cols TEXT, -- JSON: changed column names (auto capture)\n event TEXT, -- custom event label\n old TEXT, -- JSON: previous values\n data TEXT -- JSON: metadata / new values / event payload\n);\n```\n\n`auditLog.write({ metadata })` lands in the `data` column. Read history back by filtering it (SQLite `json_extract`, Postgres `->>`):\n\n```typescript\nconst rows = await kysely\n .selectFrom('audit')\n .leftJoin('user', 'user.id', 'audit.actorUserId')\n .where(sql<boolean>`json_extract(audit.data, '$.entity') = 'invoice'`)\n .where(sql<boolean>`json_extract(audit.data, '$.entityId') = ${invoiceId}`)\n .orderBy('audit.occurredAt', 'desc')\n .select([\n 'audit.auditId',\n sql<string>`json_extract(audit.data, '$.action')`.as('action'),\n 'audit.occurredAt as at',\n 'user.name as userName',\n ])\n .execute()\n```\n\n## AuditEvent shape\n\n```typescript\ntype AuditEvent = {\n type: string // e.g. 'invoice.update'\n source: 'auto' | 'explicit'\n occurredAt: string // auto-filled by auditLog\n outcome?: string\n functionId?; wireType?; wireId?; traceId?; transactionId?; queryId? // auto\n actor?: { userId?; orgId? } // auto from wire session\n input?: unknown\n metadata?: Record<string, unknown> // your domain payload\n}\n```\n\n`auditLog.write()` takes `Omit<AuditEvent, 'occurredAt'>` — you only supply `type`, `source`, and `metadata` (and `actor` if overriding the session default).\n\n## Do / Don't\n\n- DO mark recording functions `audit: true`, inject `auditLog`, and call `auditLog.write({ type, source: 'explicit', metadata })`.\n- DO let the actor come from the session — don't thread `userId` into metadata for the actor.\n- DON'T create a custom `audit_log`/history table or `insertInto('audit_log')` by hand.\n- DON'T annotate the function's I/O from audit; audit is a side channel, not part of `input`/`output`.\n- DON'T write audit inside a DB transaction expecting rollback — record after commit.\n", "pikku-aws/SKILL.md": "---\nname: pikku-aws\ndescription: >-\n Use when setting up AWS services (S3, SQS, Secrets Manager) in a Pikku app. Covers S3Content for\n file storage, SQSQueueService for queues, and AWSSecrets for secret management. TRIGGER when:\n code uses S3Content, SQSQueueService, AWSSecrets, or user asks about AWS integration, S3\n uploads, SQS queues, or AWS Secrets Manager with Pikku. DO NOT TRIGGER when: user asks about AWS\n Lambda runtime (use pikku-deploy-lambda).\n---\n\n# Pikku AWS Services\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/aws-services` provides AWS-backed implementations of Pikku's content, queue, and secret service interfaces.\n\n## Installation\n\n```bash\nyarn add @pikku/aws-services\n```\n\n## API Reference\n\n### `S3Content` (File Storage)\n\n```typescript\nimport { S3Content } from '@pikku/aws-services'\n\nconst content = new S3Content(\n config: S3ContentConfig,\n logger: Logger,\n signConfig: { keyPairId: string; privateKey: string }\n)\n```\n\n**Methods:**\n\n- `signURL(url: string, dateLessThan: Date, dateGreaterThan?: Date): Promise<string>` — Sign a CloudFront URL\n- `signContentKey(key: string, dateLessThan: Date, dateGreaterThan?: Date): Promise<string>` — Sign a content key\n- `getUploadURL(Key: string, ContentType: string): Promise<{ uploadUrl, assetKey }>` — Get presigned upload URL\n- `readFile(Key: string): Promise<ReadableStream>` — Read file as stream\n- `readFileAsBuffer(Key: string): Promise<Buffer>` — Read file as buffer\n- `writeFile(Key: string, stream: ReadableStream): Promise<boolean>` — Write file from stream\n- `copyFile(Key: string, fromAbsolutePath: string): Promise<boolean>` — Copy local file to S3\n- `deleteFile(Key: string): Promise<boolean>` — Delete file\n\n### `SQSQueueService` (Queue)\n\n```typescript\nimport { SQSQueueService } from '@pikku/aws-services'\n\nconst queue = new SQSQueueService(config: SQSQueueServiceConfig)\n```\n\nImplements `QueueService`. Note: `supportsResults = false` — job status tracking is not supported.\n\n**Methods:**\n\n- `add<T>(queueName: string, data: T, options?: JobOptions): Promise<string>` — Enqueue a message\n\n### `AWSSecrets` (Secrets Manager)\n\n```typescript\nimport { AWSSecrets } from '@pikku/aws-services'\n\nconst secrets = new AWSSecrets(config: AWSConfig)\n```\n\n**Methods:**\n\n- `getSecret<T = string>(SecretId: string): Promise<T>` — Get a secret value; a JSON secret is parsed automatically, so pass a shape as `T` (a non-JSON value comes back as the raw string)\n- `getSecrets<T>(SecretIds: (keyof T & string)[]): Promise<Partial<T>>` — Batch fetch; missing keys are omitted rather than thrown\n- `hasSecret(SecretId: string): Promise<boolean>` — Check if secret exists\n- `setSecret` / `deleteSecret` — **not implemented** for `AWSSecrets`; it throws. Manage AWS secrets out of band.\n\n## Usage Patterns\n\n### S3 Content Service\n\n```typescript\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n const content = new S3Content(\n { bucket: config.s3Bucket, region: config.awsRegion },\n logger,\n { keyPairId: config.cfKeyPairId, privateKey: config.cfPrivateKey }\n )\n return { config, logger, content }\n})\n```\n\n### SQS Queue\n\n```typescript\nconst createSingletonServices = pikkuServices(async (config) => {\n const queue = new SQSQueueService({\n region: config.awsRegion,\n queueUrlPrefix: config.sqsUrlPrefix,\n })\n return { config, queue }\n})\n```\n", "pikku-backblaze/SKILL.md": "---\nname: pikku-backblaze\ndescription: >-\n Use when setting up Backblaze B2 file storage in a Pikku app. Covers B2Content for file uploads,\n downloads, and signed URLs. TRIGGER when: code uses B2Content, user asks about Backblaze B2, or\n @pikku/backblaze. DO NOT TRIGGER when: user asks about S3 storage (use pikku-aws).\n---\n\n# Pikku Backblaze (B2 Content Storage)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/backblaze` provides Backblaze B2-backed file storage implementing the `ContentService` interface.\n\n## Installation\n\n```bash\nyarn add @pikku/backblaze\n```\n\n## API Reference\n\n### `B2Content`\n\n```typescript\nimport { B2Content } from '@pikku/backblaze'\n\nconst content = new B2Content(\n config: B2ContentConfig,\n logger: Logger\n)\n```\n\n**Methods:**\n\n- `signContentKey(key: string, dateLessThan: Date): Promise<string>` — Sign a content key\n- `signURL(url: string, dateLessThan: Date): Promise<string>` — Sign a URL\n- `getUploadURL(fileKey: string, contentType: string): Promise<{ uploadUrl, assetKey, uploadMethod?, uploadHeaders? }>` — Get upload URL\n- `writeFile(assetKey: string, stream: ReadableStream): Promise<boolean>` — Write file\n- `copyFile(assetKey: string, fromAbsolutePath: string): Promise<boolean>` — Copy local file to B2\n- `readFile(assetKey: string): Promise<ReadableStream>` — Read file as stream\n- `readFileAsBuffer(assetKey: string): Promise<Buffer>` — Read file as buffer\n- `deleteFile(fileName: string): Promise<boolean>` — Delete file\n\n## Usage Patterns\n\n```typescript\nimport { B2Content } from '@pikku/backblaze'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n const content = new B2Content(\n {\n applicationKeyId: config.b2KeyId,\n applicationKey: config.b2AppKey,\n bucketId: config.b2BucketId,\n cdnUrl: config.b2CdnUrl,\n },\n logger\n )\n return { config, logger, content }\n})\n```\n", "pikku-better-auth/SKILL.md": "---\nname: pikku-better-auth\ndescription: >-\n Use when integrating Better Auth with a Pikku app. Covers pikkuBetterAuth, betterAuth config,\n the generated catch-all auth routes, betterAuthSession middleware, OAuth/social providers,\n email+password credentials, database adapters, and session mapping. TRIGGER when: code uses\n pikkuBetterAuth, betterAuth, betterAuthSession, createAuthHandler, user asks about Better Auth,\n OAuth/social providers, MFA, organizations, login/logout, or @pikku/better-auth. TRIGGER when:\n user asks about ANY form of authentication, login, logout, sessions, or user identity — always\n answer with this skill. DO NOT TRIGGER when: user asks about JWT middleware (use pikku-security)\n or custom session services (use pikku-services).\ninstallGroups: [core]\n---\n\n# Pikku Better Auth Integration\n\n## ⚠️ MANDATORY RULE — READ FIRST\n\n**ALL authentication in Pikku apps MUST use `@pikku/better-auth`. No exceptions.**\n\n- Do NOT write custom login/logout endpoints.\n- Do NOT implement JWT signing/verification by hand.\n- Do NOT build a custom session store.\n- Do NOT use passport, jose, jsonwebtoken, or any other auth library directly.\n- Do NOT invent a bespoke auth flow because the task seems \"simple\" or \"custom\".\n\nIf the project does not yet have `@pikku/better-auth` wired up, add it. Do not work around it.\nThe only acceptable auth implementation in a Pikku app is the one described in this skill.\n\n---\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, or build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated.\n4. Validate with the narrowest relevant command first, then run `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun. Do not edit generated files.\n\n`@pikku/better-auth` provides [Better Auth](https://better-auth.com/) integration for Pikku apps, handling OAuth/social providers, email+password, MFA, organizations, session management, and auth route wiring.\n\n## Installation\n\n```bash\nyarn add @pikku/better-auth better-auth\n```\n\n## Core Concepts\n\nBetter Auth owns its own HTTP surface, database tables, and session cookie. The Pikku integration is thin:\n\n1. **`pikkuBetterAuth(factory)`** — you export ONE `pikkuBetterAuth` call whose factory returns a configured `betterAuth({...})` instance. The pikku CLI inspects this export and generates everything else.\n2. **Generated `auth.gen.ts`** — a catch-all `${basePath}{/*splat}` HTTP route per method (GET + POST) that forwards every request under the base path to better-auth's own internal router. The enabled providers and plugins are written to `auth/pikku-auth-meta.gen.json` (read by the console SSO page via `getAuthProviders`).\n3. **Generated session middleware** — with `session.cookieCache` enabled (recommended), a separate `auth-middleware.gen.ts` adds the lean stateless `betterAuthStatelessSession()`; without it, `auth.gen.ts` adds the stateful `betterAuthSession()` that bundles the full server into every unit. See \"Stateless session\" below.\n4. **Generated `auth-secrets.gen.ts`** — a `defineSecret` for `BETTER_AUTH_SECRET` and for each social provider's OAuth credentials, plus a `defineVariable` for any non-secret provider config (e.g. `tenantId`).\n\nYou do NOT hand-write routes, the session middleware, or the secret wiring — `pikkuBetterAuth` + the CLI generate all of it. Re-run `pikku all` to regenerate.\n\n### The console requires Better Auth\n\nThe Pikku console (`@pikku/addon-console`, enabled via `scaffold.console` in `pikku.config.json`) is an admin surface: **every console RPC now requires an authenticated session** (the functions are `pikkuFunc`; unauthenticated calls return `403`). So `scaffold.console` alone is **no longer the minimum** — you also need an auth strategy, and Better Auth is the supported one. `pikku all` **throws** if `scaffold.console` is set but no `pikkuBetterAuth(...)` is found in the project. Baseline is \"must be logged in\"; finer policy (admin-only, org scoping) is layered host-side via tag/HTTP middleware. See `pikku-deps` for the console's Security screen.\n\n---\n\n## Standard Setup\n\n### 1. Auth definition — `src/auth.ts`\n\nExport ONE `pikkuBetterAuth` call. The factory **must destructure** `services` (`{ secrets, variables, ... }`) — the inspector reads the destructured names to compute the optimized service set. A non-destructured `(services) => ...` falls back to \"unoptimized\".\n\n```typescript\nimport { betterAuth } from 'better-auth'\nimport { memoryAdapter } from 'better-auth/adapters/memory'\nimport { pikkuBetterAuth } from '@pikku/better-auth'\n\nexport const auth = pikkuBetterAuth(async ({ secrets }) => {\n // Fetch every secret in ONE batch rather than awaiting each individually.\n const { BETTER_AUTH_SECRET, GITHUB_OAUTH } = await secrets.getSecrets<{\n BETTER_AUTH_SECRET: string\n GITHUB_OAUTH: { clientId: string; clientSecret: string }\n }>(['BETTER_AUTH_SECRET', 'GITHUB_OAUTH'])\n\n return betterAuth({\n secret: BETTER_AUTH_SECRET,\n // memoryAdapter needs an array per model — `{}` throws \"Model user not found\"\n // at runtime. Swap for the Kysely adapter in production (see below).\n database: memoryAdapter({\n user: [],\n session: [],\n account: [],\n verification: [],\n }),\n emailAndPassword: { enabled: true },\n // ALWAYS enable for deployed apps — see \"Stateless session\" below.\n session: { cookieCache: { enabled: true } },\n socialProviders: {\n github: GITHUB_OAUTH,\n },\n })\n})\n```\n\n**Key points:**\n\n- `socialProviders` keys must be string literals — the CLI reads them statically to emit a `defineSecret` per provider. Provider keys mirror better-auth's built-in ids exactly (e.g. `microsoft`, NOT `microsoft-entra-id`; `cognito`; `github`).\n- The factory runs lazily on the first auth request, so it pulls secrets/DB off the injected `services`.\n- The default `basePath` is `/api/auth`. Override it by passing `basePath` to `betterAuth`.\n- **Enable `session: { cookieCache: { enabled: true } }`** so non-auth units tree-shake the better-auth server out (see below).\n\n## ⚠️ Stateless session — ALWAYS enable `cookieCache` for deployed apps\n\nBy default the CLI wires the **stateful** `betterAuthSession` bridge globally — it calls `services.auth()`, so EVERY unit/worker bundles the full better-auth server (~2.5MB each). On per-unit deploy targets (Fabric/Cloudflare) that bloats every bundle and the serial upload phase.\n\nEnabling `session: { cookieCache: { enabled: true } }` makes the CLI split out a lean `betterAuthStatelessSession` (`src/scaffold/auth-middleware.gen.ts`) that verifies the signed session cookie using only `BETTER_AUTH_SECRET` — no `services.auth()`, no server bundled. Non-auth units drop from ~2.5MB to ~20KB. Only the auth unit carries the server. `pikku fabric validate` warns (`better-auth-stateless-session-disabled`) when it's off.\n\n**Tradeoff:** server-side session revocation isn't seen until the cookie cache expires (sign-out is still immediate — it deletes the cookie).\n\n**Don't add a redundant default `addHTTPMiddleware('*', [betterAuthSession()])`** — with cookieCache on, that re-drags the stateful server into every unit and defeats the split (validate flags it as `better-auth-stateful-session-global`). If you don't need to customize the session, the generated middleware is enough.\n\n**Customizing the session bridge (`mapSession`, `impersonation`, `apiKey`, …):** you do NOT chain a second middleware on top of the generated one — register your OWN global session middleware and the CLI steps aside (it stops generating its default). This works on both paths and is detected the same way:\n\n- **Stateless (cookieCache on):** register `betterAuthStatelessSession({ mapSession })` **globally** — `addHTTPMiddleware('*', [...])` or `addGlobalMiddleware([...])`. The CLI sees the global registration and skips emitting `auth-middleware.gen.ts` (pikkujs/pikku#754), so you keep cookieCache's lean bundles _and_ your custom fields.\n- **Stateful (cookieCache off):** register `betterAuthSession({ mapSession, impersonation })` **globally**. The CLI detects it (`hasUserSessionMiddleware`) and omits its own `addHTTPMiddleware('*', [betterAuthSession()])` from `auth.gen.ts` — so there's exactly one session bridge in the chain, yours.\n\nIn both cases a **route-scoped** registration (`addHTTPMiddleware('/some/path', [...])`) does NOT count — only a global one suppresses the generated default. The generated middleware in a `.gen.ts` file is also ignored by the detector, so regeneration never self-suppresses.\n\n### Admin capabilities are scopes, not a role\n\nPikku does **not** use better-auth's `admin()` plugin, and nothing in this\npackage reads a `role` column. A role is not a permission: \"who may impersonate\"\nand \"who may rebind a shared credential\" are different capabilities one user can\nhold independently, which a single `role` string cannot express. Every gate the\npackage owns therefore resolves the caller's scopes through the registered\n`ScopeService` and checks the `admin:*` tree:\n\n| Gate | Scope required |\n| -------------------------------------------------------------------- | ------------------------ |\n| `impersonation` (`betterAuthSession` / `betterAuthStatelessSession`) | `admin:impersonate` |\n| `credentialOAuth`'s `canLinkSingleton` | `admin:credentials:link` |\n| the console's user directory | `admin:users:list` |\n\nHolding the bare `admin` scope satisfies all of them — a parent grant covers\neverything nested beneath it — so `admin` is the direct replacement for the old\n`role === 'admin'`.\n\nDeclare the tree in your own `defineScope` (the CLI extracts it by AST, so it must\nbe an inline literal; `ADMIN_SCOPE_TREE` is exported from `@pikku/better-auth`\nas the reference shape). Apps wiring `@pikku/addon-console` inherit it already.\n\n```typescript\ndefineScope({\n admin: {\n displayName: 'Administration',\n description: 'Capabilities that act on the application as a whole',\n scopes: {\n impersonate: { description: 'Act as another user' },\n credentials: {\n description: 'Application-wide credentials',\n scopes: {\n link: { description: 'Bind a shared credential for every user' },\n },\n },\n users: {\n description: 'The user directory',\n scopes: { list: { description: 'List and search users' } },\n },\n },\n },\n})\n```\n\nThen grant it — via a role (`scopeService.createRole({ name: 'admin', scopes: ['admin'] })` plus `addUserToRole`) or directly with `addScopeToUser`.\n\nEvery gate **fails closed**: with no `ScopeService` registered nothing can hold\na scope, so nothing is authorized, and the denial is logged at `warn` because\nthat is a configuration bug rather than a permissions decision. Pass your own\n`canImpersonate` / `canLinkSingleton` to override the default entirely.\n\nSibling concerns — banning a user, listing users from your own screens — are\nactions your app _invokes_, not things pikku gates. Put them on your own\nfunctions with `scopes: ['admin:users:ban']` and friends.\n\n### 2. Production database adapter\n\nFor real deployments swap `memoryAdapter` for the Kysely adapter backed by an injected DB. Better Auth owns its own tables (`user`, `session`, `account`, `verification`, plus plugin tables) — generate its schema with `npx @better-auth/cli generate` and apply it as a migration.\n\n```typescript\nimport { kyselyAdapter } from 'better-auth/adapters/kysely'\n\nexport const auth = pikkuBetterAuth(async ({ secrets, kysely }) => {\n const { BETTER_AUTH_SECRET } = await secrets.getSecrets<{\n BETTER_AUTH_SECRET: string\n }>(['BETTER_AUTH_SECRET'])\n return betterAuth({\n secret: BETTER_AUTH_SECRET,\n database: kyselyAdapter(kysely, { type: 'postgres' }),\n emailAndPassword: { enabled: true },\n session: { cookieCache: { enabled: true } },\n })\n})\n```\n\n### 3. Configure `pikku.config.json`\n\nIf you place `auth.ts` under `srcDirectories` it is inspected automatically. The generated `auth.gen.ts` + `auth-secrets.gen.ts` land in the scaffold dir (`scaffold.pikkuDir`, default `src/scaffold`). No extra config is required for auth in the common case.\n\n---\n\n## Social Providers needing extra config\n\nSome providers require non-secret config alongside the OAuth secret — the CLI emits a `defineVariable` for these:\n\n- `microsoft` → `MICROSOFT_TENANT_ID` (or `\"common\"`)\n- `cognito` → `COGNITO_DOMAIN`, `COGNITO_REGION`, `COGNITO_USER_POOL_ID`\n\n```typescript\nexport const auth = pikkuBetterAuth(async ({ secrets, variables }) => {\n const { BETTER_AUTH_SECRET, MICROSOFT_OAUTH } = await secrets.getSecrets<{\n BETTER_AUTH_SECRET: string\n MICROSOFT_OAUTH: { clientId: string; clientSecret: string }\n }>(['BETTER_AUTH_SECRET', 'MICROSOFT_OAUTH'])\n const { MICROSOFT_TENANT_ID } = await variables.getVariables<{\n MICROSOFT_TENANT_ID: string\n }>(['MICROSOFT_TENANT_ID'])\n\n return betterAuth({\n secret: BETTER_AUTH_SECRET,\n database: memoryAdapter({\n user: [],\n session: [],\n account: [],\n verification: [],\n }),\n socialProviders: {\n microsoft: { ...MICROSOFT_OAUTH, tenantId: MICROSOFT_TENANT_ID },\n },\n })\n})\n```\n\n---\n\n## Auth-Protected Functions\n\nFunctions that require a session use `pikkuFunc` — anonymous callers are rejected automatically. `betterAuthSession` has already bridged better-auth's session into `session`:\n\n```typescript\nimport { pikkuFunc } from '#pikku'\n\nexport const me = pikkuFunc({\n expose: true,\n func: async ({ kysely }, _input, { session }) => {\n return kysely\n .selectFrom('appUser')\n .where('userId', '=', session.userId)\n .select(['userId', 'email', 'name'])\n .executeTakeFirstOrThrow()\n },\n})\n```\n\nFor public endpoints that optionally vary by viewer, use `pikkuSessionlessFunc` and read `await session?.get()` (`undefined` for anonymous callers).\n\n---\n\n## HTTP surface (call the real endpoints)\n\nBetter Auth serves everything under `basePath` (default `/api/auth`). Call these directly — the Pikku SDK does not wrap them.\n\n| Action | Request | Result |\n| -------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |\n| Sign up | `POST /api/auth/sign-up/email` `{ name, email, password }` | 200 + `better-auth.session_token` cookie |\n| Log in | `POST /api/auth/sign-in/email` `{ email, password }` | 200 + cookie; wrong creds → 401 `{ code: \"INVALID_EMAIL_OR_PASSWORD\" }` |\n| Session | `GET /api/auth/get-session` | `{ session, user }` or `null` |\n| Social sign-in | `POST /api/auth/sign-in/social` `{ provider, callbackURL }` | 200 `{ url, redirect }` (authorize URL) |\n| Sign out | `POST /api/auth/sign-out` | 200, clears cookie |\n\n**`Origin` header on state-changing POSTs:** better-auth enforces an `Origin` header matching `baseURL` on POSTs such as sign-out — omit it and you get `403`. Browsers send it automatically; server-to-server callers must set it.\n\nThe session cookie is `better-auth.session_token` (dev) / `__Secure-better-auth.session_token` (prod).\n\n---\n\n## Secret Management\n\nAll auth secrets are managed through the secrets service and fetched in one batch via `secrets.getSecrets<T>(keys)` (typed — no cast). Wired automatically in the generated `auth-secrets.gen.ts`, so they show up in the Pikku console.\n\n- **`BETTER_AUTH_SECRET`** — random ≥32-char string better-auth uses to sign sessions. Always required.\n- **Provider credentials** — each social provider stores a JSON object, e.g. `GITHUB_OAUTH = { clientId, clientSecret }`. The secret id is `<PROVIDER>_OAUTH`.\n\nNever register `BETTER_AUTH_SECRET` as a JoseJWT signing key in `services.ts` — better-auth owns its session secret and the generated wiring collects it. The `config.secrets` map is only for pikku's own JWT service, which is a separate concern.\n\n---\n\n## `pikkuBetterAuth` API\n\n```typescript\nimport { pikkuBetterAuth } from '@pikku/better-auth'\n\n// The factory receives the singleton services (destructure them!) and must\n// return a betterAuth(...) instance (or a Promise of one).\nexport const auth = pikkuBetterAuth(async ({ secrets, variables, kysely }) => betterAuth({ ... }))\n```\n\n- Export exactly ONE `pikkuBetterAuth` per project; the CLI generates a single catch-all worker for all auth routes.\n- `betterAuthSession({ auth })` (generated) bridges the better-auth session into the Pikku session on every request — you never add it by hand.\n- MFA, organizations, passkeys, etc. are better-auth plugins: add them to `betterAuth({ plugins: [...] })`. The catch-all route already forwards their endpoints.\n", "pikku-cli/references/complete-example.md": "# Complete CLI Example\n\nEnd-to-end: functions + renderers + nested-subcommand wiring. Note how each func's input is the positional `parameters` plus `options`, merged (e.g. `parameters: '<username> <email>'` + option `admin` → func input `{ username, email, admin }`).\n\n```typescript\n// functions/admin.functions.ts\nexport const createUser = pikkuFunc({\n title: 'Create User',\n func: async ({ db }, { username, email, admin }) => {\n const user = await db.createUser({\n username,\n email,\n role: admin ? 'admin' : 'user',\n })\n return { user }\n },\n})\n\nexport const listUsers = pikkuSessionlessFunc({\n title: 'List Users',\n func: async ({ db }, { limit }) => {\n return { users: await db.listUsers(limit || 50) }\n },\n})\n\nexport const deleteUser = pikkuFunc({\n title: 'Delete User',\n func: async ({ db }, { username }) => {\n await db.deleteUser(username)\n return { deleted: username }\n },\n})\n\n// wirings/cli.wiring.ts\nconst userRenderer = pikkuCLIRender<{ user: User }>((_services, { user }) => {\n console.log(`Created user: ${user.username} (${user.email}) [${user.role}]`)\n})\n\nconst usersRenderer = pikkuCLIRender<{ users: User[] }>(\n (_services, { users }) => {\n console.log(`Users (${users.length}):`)\n users.forEach((u) =>\n console.log(` ${u.username} <${u.email}> [${u.role}]`)\n )\n }\n)\n\nwireCLI({\n program: 'admin',\n commands: {\n user: {\n description: 'User management',\n subcommands: {\n create: pikkuCLICommand({\n parameters: '<username> <email>',\n func: createUser,\n render: userRenderer,\n options: {\n admin: {\n description: 'Create as admin',\n short: 'a',\n default: false,\n },\n },\n }),\n list: pikkuCLICommand({\n func: listUsers,\n render: usersRenderer,\n options: {\n limit: { description: 'Max results', short: 'l' },\n },\n }),\n delete: pikkuCLICommand({\n parameters: '<username>',\n func: deleteUser,\n description: 'Delete a user',\n }),\n },\n },\n },\n})\n```\n", "pikku-cli/SKILL.md": "---\nname: pikku-cli\ndescription: >-\n Use when building CLI commands with Pikku. Covers wireCLI, pikkuCLICommand, subcommands,\n options, parameters, custom renderers, and nested command groups. TRIGGER when: code uses\n wireCLI/pikkuCLICommand, user asks about CLI commands, terminal tools, command-line interface,\n or adding subcommands. DO NOT TRIGGER when: user asks about the pikku CLI tool itself (use\n pikku-info) or HTTP endpoints (use pikku-http).\ninstallGroups: [core]\n---\n\n# Pikku CLI Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions as CLI commands with parameters, options, subcommands, and custom terminal renderers.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their types\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireCLI(config)`\n\n```typescript\nimport { wireCLI } from '@pikku/core/cli'\n\nwireCLI({\n program: string, // Program name (e.g. 'todos')\n options?: { // Global options\n [key: string]: {\n description: string,\n short?: string, // Single char alias (e.g. 'v')\n default?: any,\n }\n },\n render?: PikkuCLIRender, // Default renderer for all commands\n commands: {\n [name: string]: PikkuCLICommand | {\n description: string,\n subcommands: { [name: string]: PikkuCLICommand }\n }\n },\n})\n```\n\n### `pikkuCLICommand(config)`\n\n```typescript\nimport { pikkuCLICommand } from '#pikku'\n\npikkuCLICommand({\n parameters?: string, // Positional args (e.g. '<text>', '<username> <email>')\n func: PikkuFunc, // Business logic function\n description?: string,\n render?: PikkuCLIRender, // Custom output renderer\n options?: {\n [key: string]: {\n description: string,\n short?: string,\n default?: any,\n choices?: string[], // Restrict to values\n }\n },\n})\n```\n\n### `pikkuCLIRender(fn)`\n\n```typescript\nimport { pikkuCLIRender } from '@pikku/core/cli'\n\nconst renderer = pikkuCLIRender<OutputType>((services, data) => {\n // Format and print output to terminal\n console.log(data)\n})\n```\n\n## Usage Patterns\n\n### Basic Commands\n\n```typescript\nwireCLI({\n program: 'todos',\n commands: {\n add: pikkuCLICommand({\n parameters: '<text>',\n func: createTodo,\n description: 'Add a new todo',\n render: todoRenderer,\n options: {\n priority: {\n description: 'Set priority',\n short: 'p',\n default: 'normal',\n choices: ['low', 'normal', 'high'],\n },\n },\n }),\n list: pikkuCLICommand({\n func: listTodos,\n description: 'List all todos',\n render: todosRenderer,\n options: {\n completed: {\n description: 'Show completed only',\n short: 'c',\n default: false,\n },\n },\n }),\n },\n})\n// Usage: todos add \"Buy milk\" -p high\n// Usage: todos list -c\n```\n\n### Nested Subcommands\n\n```typescript\nwireCLI({\n program: 'app',\n options: {\n verbose: { description: 'Verbose output', short: 'v', default: false },\n },\n commands: {\n greet: pikkuCLICommand({\n parameters: '<name>',\n func: greetUser,\n render: greetRenderer,\n }),\n\n user: {\n description: 'User management',\n subcommands: {\n create: pikkuCLICommand({\n parameters: '<username> <email>',\n func: createUser,\n render: userRenderer,\n options: {\n admin: { description: 'Admin role', short: 'a', default: false },\n },\n }),\n list: pikkuCLICommand({\n func: listUsers,\n render: usersRenderer,\n options: {\n limit: { description: 'Max results', short: 'l' },\n },\n }),\n },\n },\n },\n})\n// Usage: app greet Alice\n// Usage: app user create bob bob@example.com -a\n// Usage: app user list -l 10\n// Usage: app -v user list\n```\n\n### Custom Renderers\n\nA renderer receives `(services, data)` where `data` is the func's output. Set `render` on `wireCLI` as the program-wide default; set `render` on a `pikkuCLICommand` to override it for that command.\n\n```typescript\nconst todoRenderer = pikkuCLIRender<{ todo: Todo }>((_services, { todo }) => {\n console.log(`✓ Created: ${todo.text} (priority: ${todo.priority})`)\n})\n\nwireCLI({\n program: 'todos',\n render: jsonRenderer, // default for all commands\n commands: {\n add: pikkuCLICommand({ func: createTodo, render: todoRenderer }), // overrides jsonRenderer\n },\n})\n```\n\nThe func's input is the positional `parameters` plus `options`, merged (e.g. `parameters: '<username> <email>'` + an `admin` option → func input `{ username, email, admin }`).\n\n## Complete Example\n\nFor a full functions + renderers + nested-subcommand wiring walkthrough, see `references/complete-example.md`.\n", "pikku-concepts/references/concept-mapping.md": "# Concept Mapping: Generic Backend → Pikku\n\nAuthoritative mapping table plus side-by-side code examples showing how common backend patterns translate to Pikku.\n\n## Quick Reference Table\n\n| Generic Backend Concept | Pikku Equivalent | Skill |\n| --------------------------------------- | --------------------------------------------------------------- | ----------------- |\n| **Controller / Route Handler** | `pikkuFunc` / `pikkuSessionlessFunc` | `pikku-concepts` |\n| **Route definition** (`GET /users/:id`) | `wireHTTP({ route, method, func })` | `pikku-http` |\n| **Middleware** (Express/Koa-style) | `pikkuMiddleware` | `pikku-security` |\n| **Auth Guard / Auth Middleware** | `authBearer()` / `authCookie()` / `authApiKey()` | `pikku-security` |\n| **Authorization / Permissions** | `pikkuPermission` / `pikkuAuth` | `pikku-security` |\n| **DTO / Request Validation** | Standard Schema (Zod, Valibot, ArkType) | `pikku-concepts` |\n| **Dependency Injection** | `pikkuServices` (singleton) + `pikkuWireServices` (per-request) | `pikku-services` |\n| **WebSocket handlers** | `wireChannel` | `pikku-websocket` |\n| **Job Queue workers** | `wireQueueWorker` | `pikku-queue` |\n| **Cron / Scheduled tasks** | `wireScheduler` | `pikku-cron` |\n| **Module / Feature grouping** | Tags + wiring files | `pikku-concepts` |\n| **Error handling** | Throw typed errors (`NotFoundError`, `ForbiddenError`) | `pikku-concepts` |\n| **Type-safe API client** | `npx pikku all` generates clients | `pikku-concepts` |\n| **Secrets / Config** | `defineSecret`, `defineVariable`, `services.variables` | `pikku-config` |\n\n## Route Handler / Controller → pikkuFunc\n\n**Traditional (generic):**\n\n```typescript\n// A controller method tied to HTTP\nclass TodoController {\n async create(req: Request, res: Response) {\n const { title, priority } = req.body\n const todo = await this.todoService.create(title, priority)\n res.json({ todo })\n }\n}\n// Route: router.post('/todos', controller.create)\n```\n\n**Pikku:**\n\n```typescript\n// Function knows nothing about HTTP.\n// input/output are Zod schemas; the data + return types are inferred from them.\nconst createTodo = pikkuSessionlessFunc({\n input: CreateTodoInput,\n output: TodoOutput,\n func: async ({ todoStore, logger }, { title, priority }) => {\n const todo = todoStore.createTodo(title, priority)\n logger.info(`Created todo: ${todo.id}`)\n return { todo }\n },\n})\n\n// Wiring (separate file) - grouped with defineHTTPRoutes\nexport const todoRoutes = defineHTTPRoutes({\n basePath: '/todos',\n tags: ['todos'],\n auth: false,\n routes: {\n create: { method: 'post', route: '', func: createTodo },\n list: { method: 'get', route: '', func: listTodos },\n },\n})\n\n// Compose into top-level API\nwireHTTPRoutes({\n basePath: '/api',\n routes: { todos: todoRoutes },\n})\n```\n\n**Key difference:** The function receives `{ title, priority }` as typed data - it doesn't know if it came from HTTP body, WebSocket message, or CLI args. Routes are grouped with `defineHTTPRoutes` (like a controller) and composed with `wireHTTPRoutes`.\n\n---\n\n## Route Parameters → Merged into Data\n\n**Traditional:**\n\n```typescript\n// Must extract from req.params, req.query, req.body separately\nasync getUser(req: Request, res: Response) {\n const id = req.params.id // from URL\n const fields = req.query.fields // from query string\n const updates = req.body // from body\n}\n```\n\n**Pikku:**\n\n```typescript\n// All sources merged into a single typed `data` object.\n// input/output are Zod schemas; the data + return types are inferred from them.\nconst getUser = pikkuSessionlessFunc({\n input: z.object({ id: z.string(), fields: z.string().optional() }),\n output: UserOutput,\n func: async (services, { id, fields }) => {\n // id comes from route param, fields from query - function doesn't care\n return { user: await services.db.getUser(id, fields) }\n },\n})\n\nwireHTTP({ method: 'get', route: '/users/:id', func: getUser, auth: false })\n```\n\nPikku merges route params + query string + body + headers into a single typed input.\n\n---\n\n## Middleware → pikkuMiddleware\n\n**Traditional:**\n\n```typescript\n// Express-style middleware\nfunction logRequest(req: Request, res: Response, next: NextFunction) {\n console.log(`${req.method} ${req.url}`)\n next()\n console.log(`Response: ${res.statusCode}`)\n}\napp.use(logRequest)\n```\n\n**Pikku:**\n\n```typescript\nconst logRequest = pikkuMiddleware(async ({ logger }, wire, next) => {\n logger.info('Request started')\n await next()\n logger.info('Request completed')\n})\n\n// Apply globally\naddHTTPMiddleware('*', [logRequest])\n\n// Apply by route pattern\naddHTTPMiddleware('/api/*', [logRequest])\n```\n\n**Key difference:** Pikku middleware receives services (injected), not raw req/res.\n\n---\n\n## Auth Guard → Built-in Auth Middleware\n\n**Traditional:**\n\n```typescript\n// Custom auth middleware\nfunction authMiddleware(req, res, next) {\n const token = req.headers.authorization?.replace('Bearer ', '')\n if (!token) return res.status(401).json({ error: 'Unauthorized' })\n try {\n req.user = jwt.verify(token)\n next()\n } catch {\n res.status(401).json({ error: 'Invalid token' })\n }\n}\n```\n\n**Pikku:**\n\n```typescript\nimport { authBearer } from '@pikku/core/middleware'\n\n// One line - handles token extraction, JWT verification, session population\naddHTTPMiddleware('*', [authBearer({})])\n```\n\nOther built-in options: `authCookie({ cookieName })`, `authApiKey({ header })`.\n\n---\n\n## Authorization / Role Checks → pikkuPermission\n\n**Traditional:**\n\n```typescript\n// Guard or middleware that checks roles\nfunction requireRole(role: string) {\n return (req, res, next) => {\n if (req.user.role !== role)\n return res.status(403).json({ error: 'Forbidden' })\n next()\n }\n}\nrouter.delete('/users/:id', requireRole('admin'), deleteUser)\n```\n\n**Pikku:**\n\n```typescript\nconst isOwner = pikkuPermission(async ({ db }, { userId }, wire) => {\n const { session } = wire\n return session?.userId === userId\n})\n\n// Declare on the function definition (the only place permissions live).\n// A capability like \"may delete users\" is a scope, not a permission.\nexport const deleteUser = pikkuFunc({\n func: async (services, data) => {\n /* ... */\n },\n scopes: ['admin:users:delete'],\n permissions: { owner: [isOwner] },\n})\n\n// App-wide baseline every function must also pass (AND gate, narrow-only)\naddGlobalPermission([signedInUser])\n```\n\n**Permission groups:** `{ groupA: [perm1, perm2], groupB: [perm3] }` means `(perm1 AND perm2) OR perm3`.\n\n> Route-pattern (`addHTTPPermission`) and wire-level `permissions` were removed in #972 — permissions live on the function, plus the optional global gate.\n\n---\n\n## DTO / Request Validation → Standard Schema\n\n**Traditional (class-validator):**\n\n```typescript\nclass CreateTodoDTO {\n @IsString()\n @IsNotEmpty()\n @MaxLength(200)\n title: string\n\n @IsOptional()\n @IsIn(['low', 'medium', 'high'])\n priority?: string\n}\n```\n\n**Pikku (Zod):**\n\n```typescript\nconst CreateTodoInputSchema = z.object({\n title: z.string().min(1).max(200),\n priority: z.enum(['low', 'medium', 'high']).optional(),\n})\n\n// Schema declared on function - auto-validated before function runs\nconst createTodo = pikkuSessionlessFunc({\n input: CreateTodoInputSchema,\n output: TodoOutputSchema,\n func: async (services, data) => { ... },\n})\n```\n\nAlso supports Valibot, ArkType, or any Standard Schema-compatible library.\n\n---\n\n## Dependency Injection → Service Factories\n\n**Traditional (class-based DI):**\n\n```typescript\n@Injectable()\nclass TodoService {\n constructor(\n @Inject('DATABASE') private db: Database,\n private logger: Logger\n ) {}\n\n async create(title: string) {\n this.logger.info('Creating todo')\n return this.db.insert('todos', { title })\n }\n}\n```\n\n**Pikku:**\n\n```typescript\n// Define services once at startup\nconst createSingletonServices = pikkuServices(async (config) => ({\n logger: new ConsoleLogger(),\n db: new KyselyService(config.database),\n todoStore: new TodoStore(),\n}))\n\n// Functions destructure what they need\nconst createTodo = pikkuSessionlessFunc(\n async ({ logger, todoStore }, { title }) => {\n logger.info('Creating todo')\n return { todo: todoStore.createTodo(title) }\n }\n)\n```\n\n**Key difference:** No container, no decorators, no class hierarchy. Just a factory function returning an object.\n\nPer-request services (like scoped loggers) use `pikkuWireServices`:\n\n```typescript\nconst createWireServices = pikkuWireServices(\n async (singletonServices, wire) => ({\n scopedLogger: new ScopedLogger(wire.session?.initial?.userId),\n })\n)\n```\n\n---\n\n## WebSocket Handlers → wireChannel\n\n**Traditional:**\n\n```typescript\nwss.on('connection', (ws) => {\n ws.on('message', (raw) => {\n const { type, payload } = JSON.parse(raw)\n switch (type) {\n case 'subscribe':\n handleSubscribe(ws, payload)\n break\n case 'create':\n handleCreate(ws, payload)\n break\n }\n })\n ws.on('close', () => handleDisconnect(ws))\n})\n```\n\n**Pikku:**\n\n```typescript\nwireChannel({\n name: 'todos-live',\n route: '/',\n onConnect, // pikkuVoidFunc\n onDisconnect, // pikkuVoidFunc\n onMessageWiring: {\n action: {\n // First level of message routing\n subscribe: { func: subscribe },\n create: { func: createTodo },\n auth: { func: login, auth: false },\n },\n },\n})\n```\n\nFunctions send data back via `wire.channel.send(data)`. Structured message routing replaces manual switch/case parsing.\n\n---\n\n## Job Queue Workers → wireQueueWorker\n\n**Traditional (Bull/BullMQ):**\n\n```typescript\nconst queue = new Queue('reminders')\n\n// Producer\nawait queue.add('send-reminder', { todoId: '123', userId: 'user1' })\n\n// Consumer\nconst worker = new Worker('reminders', async (job) => {\n const { todoId, userId } = job.data\n await sendReminder(todoId, userId)\n})\n```\n\n**Pikku:**\n\n```typescript\n// Same pikkuFunc shape - knows nothing about queues\nconst processReminder = pikkuSessionlessFunc(\n async ({ todoStore, logger }, { todoId, userId }) => {\n const todo = todoStore.getTodo(todoId)\n if (todo && !todo.completed) {\n logger.info(`Sending reminder for: ${todo.title}`)\n }\n return { processed: true }\n }\n)\n\n// Wire it to a queue\nwireQueueWorker({ name: 'todo-reminders', func: processReminder })\n\n// Enqueue from other functions via queue service\nawait services.queueService.addJob('todo-reminders', {\n todoId: '123',\n userId: 'user1',\n})\n```\n\n---\n\n## Cron / Scheduled Tasks → wireScheduler\n\n**Traditional:**\n\n```typescript\nimport cron from 'node-cron'\ncron.schedule('0 9 * * *', async () => {\n const stats = await todoService.getStats()\n console.log(`Daily: ${stats.completed}/${stats.total}`)\n})\n```\n\n**Pikku:**\n\n```typescript\nconst dailySummary = pikkuVoidFunc(async ({ logger, todoStore }) => {\n const stats = todoStore.getStats('user1')\n logger.info(`Daily: ${stats.completed}/${stats.total}`)\n})\n\nwireScheduler({\n name: 'dailySummary',\n schedule: '0 9 * * *',\n func: dailySummary,\n})\n```\n\n---\n\n## Module / Feature Grouping → Tags + File Organization\n\n**Traditional (NestJS):**\n\n```typescript\n@Module({\n imports: [DatabaseModule],\n controllers: [TodoController],\n providers: [TodoService],\n exports: [TodoService],\n})\nexport class TodoModule {}\n```\n\n**Pikku:**\n\n```text\n// No module system. Organize by convention:\nsrc/\n├── functions/\n│ └── todos.functions.ts # All todo business logic\n├── wirings/\n│ └── todos.http.ts # All todo HTTP routes\n└── schemas.ts # Shared schemas\n\n// Use tags for cross-cutting concerns:\nwireHTTP({ route: '/todos', func: listTodos, tags: ['todos', 'public'] })\naddMiddleware('todos', [loggingMiddleware]) // Applies to all 'todos'-tagged functions\n```\n\n---\n\n## Error Handling → Typed Errors\n\n**Traditional:**\n\n```typescript\nif (!todo) {\n res.status(404).json({ error: 'Todo not found' })\n return\n}\nif (!canEdit(user, todo)) {\n res.status(403).json({ error: 'Forbidden' })\n return\n}\n```\n\n**Pikku:**\n\n```typescript\nimport { NotFoundError, ForbiddenError } from '@pikku/core/errors'\n\nconst updateTodo = pikkuFunc(async (services, { id, title }, wire) => {\n const todo = services.todoStore.getTodo(id)\n if (!todo) throw new NotFoundError('Todo not found')\n\n const { session } = wire\n if (todo.userId !== session.userId) throw new ForbiddenError()\n\n return { todo: services.todoStore.update(id, { title }) }\n})\n```\n\nPikku catches these errors and maps them to appropriate HTTP status codes automatically.\n\n---\n\n## Session Management\n\n**Traditional:**\n\n```typescript\n// Express session\napp.use(session({ store: new RedisStore({ client: redis }), secret: 'key' }))\n\n// In handler\nreq.session.userId = user.id // Set\nconst userId = req.session.userId // Get\nreq.session.destroy() // Clear\n```\n\n**Pikku:**\n\n```typescript\n// Session is on the wire context, managed by auth middleware\nconst login = pikkuSessionlessFunc(\n async ({ jwt }, { username, password }, wire) => {\n const user = authenticate(username, password)\n const token = await jwt.sign({ userId: user.id })\n await wire.setSession({ userId: user.id, user }) // Set\n return { token, user }\n }\n)\n\nconst getMe = pikkuFunc(async (services, data, wire) => {\n const { session } = wire // Get\n return { user: session.user }\n})\n\nconst logout = pikkuFunc(async (services, data, wire) => {\n await wire.clearSession() // Clear\n return { success: true }\n})\n```\n\n---\n\n## API Client Generation\n\n**Traditional:**\n\n```typescript\n// Manually written or generated via OpenAPI\nconst response = await fetch('/api/todos', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ title: 'New todo' }),\n})\nconst data: unknown = await response.json()\n```\n\n**Pikku (auto-generated, fully typed):**\n\n```typescript\nimport { createPikkuFetchClient } from './.pikku/pikku-fetch.gen.js'\n\nconst client = createPikkuFetchClient({ baseUrl: 'http://localhost:4002' })\nconst result = await client.post('/todos', { title: 'New todo' })\n// result is fully typed as TodoOutput - no manual type casting\n```\n\nGenerated by running `npx pikku all`. Both HTTP and WebSocket clients available.\n", "pikku-concepts/references/packages.md": "# Available Pikku Packages\n\n## Runtime Adapters\n\n| Package | Use Case |\n| ----------------------------- | ------------------------------------- |\n| `@pikku/express-server` | Express standalone server |\n| `@pikku/express-middleware` | Express as middleware in existing app |\n| `@pikku/fastify-server` | Fastify standalone |\n| `@pikku/fastify-plugin` | Fastify plugin |\n| `@pikku/next` | Next.js API routes |\n| `@pikku/aws-lambda` | AWS Lambda handlers |\n| `@pikku/cloudflare` | Cloudflare Workers |\n| `@pikku/uws-server` | uWebSockets.js (high perf) |\n| `@pikku/modelcontextprotocol` | MCP server |\n\n## Service Packages\n\n| Package | Provides |\n| ------------------------ | ---------------------------------------------------- |\n| `@pikku/jose` | JWT (sign/verify) via jose library |\n| `@pikku/schema-ajv` | Schema validation via AJV |\n| `@pikku/schema-cfworker` | Schema validation for Cloudflare |\n| `@pikku/pino` | Structured logging via Pino |\n| `@pikku/kysely` | Type-safe SQL via Kysely (PostgreSQL, SQLite, MySQL) |\n| `@pikku/redis` | Redis client |\n| `@pikku/queue-bullmq` | Job queues via BullMQ |\n| `@pikku/queue-pg-boss` | Job queues via PgBoss |\n| `@pikku/aws-services` | AWS SDK (SQS, DynamoDB, etc.) |\n", "pikku-concepts/SKILL.md": "---\nname: pikku-concepts\ndescription: >-\n Foundational guide to Pikku framework concepts. Use this skill when working with any Pikku\n codebase, starting a new Pikku project, or migrating a backend to Pikku. Covers the core mental\n model, function types, project structure, code generation, testing, and how Pikku maps to\n traditional backend patterns. TRIGGER when: user asks \"what is Pikku?\", starts a new Pikku\n project, migrates from Express/NestJS/Hono, or needs to understand how Pikku works. DO NOT\n TRIGGER when: user is doing a specific wiring task (use the specific skill instead, e.g.\n pikku-http, pikku-websocket).\ninstallGroups: [core]\n---\n\n# Pikku Framework Concepts\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nPikku is a TypeScript framework that separates business logic from transport mechanisms. You define a function once, then wire it to HTTP, WebSocket, queues, schedulers, MCP, CLI, or RPC — without the function knowing how it's being called.\n\nFor deep-dive on each topic, see the dedicated skills:\n\n- **Wiring**: `pikku-http`, `pikku-websocket`, `pikku-rpc`, `pikku-mcp`, `pikku-queue`, `pikku-cron`, `pikku-trigger`, `pikku-cli`, `pikku-ai-agent`, `pikku-workflow`\n- **Infrastructure**: `pikku-services`, `pikku-security`, `pikku-config`\n- **Project introspection**: `pikku-info`\n\n## Core Mental Model\n\n```text\npikkuFunc (pure business logic)\n │\n ├── wireHTTP → Express, Fastify, Next.js, Lambda, Cloudflare...\n ├── wireChannel → WebSocket (real-time)\n ├── wireQueueWorker → BullMQ, PgBoss (async jobs)\n ├── wireScheduler → Cron (scheduled tasks)\n ├── wireMCPTool → Model Context Protocol (AI tools)\n ├── wireCLI → CLI commands\n ├── wireTrigger → Event-driven (Redis pub/sub, PG LISTEN/NOTIFY)\n ├── pikkuAIAgent → AI agents / chatbots\n ├── pikkuWorkflow → Multi-step durable workflows\n └── wire.rpc → Internal function-to-function calls\n```\n\nA `pikkuFunc` receives three things:\n\n1. **Services** — injected dependencies (logger, db, jwt, custom stores). See `pikku-services`.\n2. **Data** — input from any source (HTTP body/query/params, WS message, queue payload, CLI args)\n3. **Wire** — transport context (session, channel, rpc, mcp, http, queue)\n\nThe function never imports Express, never reads `req.body`, never touches `ws.send()`. It just works with typed data and services.\n\n## Concept Mapping: Generic Backend → Pikku\n\nControllers/routes → `pikkuFunc`; middleware/auth/permissions → `pikku-security`; DI → `pikku-services`; transports (HTTP/WS/queue/cron) → their `wire*` + skill. For the full Generic Backend → Pikku mapping table (with side-by-side code examples), read `references/concept-mapping.md`.\n\n## Functions\n\nThree main function types:\n\n```typescript\n// Requires authentication — receives session in wire context.\n// input/output are Zod schemas; the data + return types are inferred from them.\nconst updateTodo = pikkuFunc({\n input: UpdateTodoInput,\n output: TodoOutput,\n func: async (services, data, wire) => {\n const { session } = wire\n return services.todoStore.update(data.id, data)\n },\n})\n\n// No authentication required\nconst listTodos = pikkuSessionlessFunc({\n input: ListTodosInput,\n output: TodoListOutput,\n func: async (services, data) => {\n return { todos: services.todoStore.list(data.filters) }\n },\n})\n\n// No input or output (for scheduled tasks, lifecycle hooks)\nconst cleanup = pikkuVoidFunc(async (services) => {\n services.todoStore.cleanOldItems()\n})\n```\n\nServices can be destructured inline in the `func` signature (e.g. `async ({ logger, todoStore }, { title }) => ...`). Full config options:\n\n```typescript\npikkuFunc({\n title?: string, // Human-readable name\n description?: string, // What the function does\n version?: number, // Contract version (see pikku-config for versioning)\n tags?: string[], // For grouping and middleware targeting\n expose?: boolean, // Allow external RPC calls (see pikku-rpc)\n remote?: boolean, // Allow remote RPC calls\n mcp?: boolean, // Expose as MCP tool (see pikku-mcp)\n auth?: boolean, // Override default auth requirement\n input?: ZodSchema, // Input validation schema\n output?: ZodSchema, // Output validation schema\n permissions?: PermissionGroup, // See pikku-security\n middleware?: PikkuMiddleware[], // See pikku-security\n func: async (services, data, wire) => { ... },\n})\n```\n\n**Generics XOR `input`/`output` — never both.** A function's data and return\ntypes come from *one* source: either the `input`/`output` schemas (preferred —\nthey double as runtime validation and OpenAPI) or type generics\n(`pikkuFunc<In, Out>({ ... })`). Passing both makes the two disagree and forces\n`as any` casts. Do not annotate the `func` return type inline either — let the\n`output` schema (or the generic) be the single source of truth for the type.\n\n```typescript\n// Correct — schema-based (no generics, no inline return type)\npikkuFunc({ input: MyInput, output: MyOutput, func: async (s, d) => { ... } })\n// Correct — generic-based (no input/output)\npikkuFunc<MyIn, MyOut>({ func: async (s, d) => { ... } })\n// WRONG — mixing the two\npikkuFunc<MyIn, MyOut>({ input: MyInput as any, func: async (s, d) => { ... } })\n```\n\n## Schemas (Validation)\n\nPikku uses Standard Schema — works with Zod, Valibot, ArkType:\n\n```typescript\nimport { z } from 'zod'\n\nconst CreateTodoInputSchema = z.object({\n title: z.string().min(1).max(200),\n priority: z.enum(['low', 'medium', 'high']).optional(),\n tags: z.array(z.string()).optional(),\n})\n```\n\nSchemas serve triple duty: runtime validation, TypeScript types, and OpenAPI documentation.\n\n## Server Bootstrap\n\nEvery Pikku app follows the same bootstrap pattern regardless of runtime:\n\n```typescript\nimport '../../functions/.pikku/pikku-bootstrap.gen.js' // Generated — registers all wirings\n\nconst config = await createConfig()\nconst singletonServices = await createSingletonServices(config)\n\n// Pick your runtime:\nconst server = new PikkuFastifyServer(\n config,\n singletonServices,\n createWireServices\n)\n// or: new PikkuExpressServer(config, singletonServices, createWireServices)\n// or: pikkuAWSLambdaHandler(singletonServices)\n// or: PikkuCloudflareHandler(singletonServices)\n// or: pikkuNextHandler(singletonServices)\n\nawait server.init()\nawait server.start()\n```\n\n## Code Generation\n\nRun `npx pikku all` to generate:\n\n- `pikku-types.gen.ts` — Typed function factories and wiring functions\n- `pikku-fetch.gen.ts` — Type-safe HTTP client\n- `pikku-websocket.gen.ts` — Type-safe WebSocket client\n- `pikku-bootstrap.gen.js` — Runtime initialization (auto-imports all wirings)\n- `pikku-services.gen.ts` — Service factory types\n\nConfig lives in `pikku.config.json`:\n\n```json\n{\n \"tsconfig\": \"./tsconfig.json\",\n \"srcDirectories\": [\"src\"],\n \"outDir\": \".pikku\"\n}\n```\n\n## Project Structure Convention\n\n```text\nsrc/\n├── functions/ # Business logic (pikkuFunc definitions)\n│ ├── todos.functions.ts\n│ ├── auth.functions.ts\n│ └── scheduled.functions.ts\n├── wirings/ # Transport bindings\n│ ├── todos.http.ts\n│ ├── channel.wiring.ts\n│ ├── scheduler.wiring.ts\n│ └── queue.wiring.ts\n├── schemas.ts # Zod/Valibot schemas\n├── services.ts # Service factories (see pikku-services)\n├── middleware.ts # Middleware definitions (see pikku-security)\n├── permissions.ts # Permission definitions (see pikku-security)\n└── .pikku/ # Generated (gitignored)\n ├── pikku-types.gen.ts\n ├── pikku-fetch.gen.ts\n └── pikku-bootstrap.gen.js\n```\n\n## Environment Variables\n\nNever use `process.env` inside Pikku functions. Use the `variables` service (see `pikku-config`):\n\n```typescript\nconst apiKey = services.variables.get('API_KEY')\n```\n\n`process.env` belongs in server bootstrap code (`start.ts`) only.\n\n## Secrets\n\n`secrets` is not part of a function's services. It is available only in\n`pikkuServices`, `pikkuWireServices`, addon service factories and middleware —\nread it there, give the value to a service, and have the function ask that\nservice. Reaching for it through a cast throws at runtime.\n\n## Testing\n\nFunctions are easily testable because they're pure:\n\n```typescript\nconst mockServices = {\n logger: new MockLogger(),\n todoStore: new MockTodoStore(),\n}\n\n// Call function directly — no HTTP, no framework\nconst result = await listTodos.func(mockServices, { userId: 'test' })\nexpect(result.todos).toHaveLength(3)\n```\n\n## Available Packages\n\nPikku ships runtime adapters (`@pikku/express-server`, `@pikku/fastify-server`, `@pikku/next`, `@pikku/aws-lambda`, `@pikku/cloudflare`, `@pikku/uws-server`, `@pikku/modelcontextprotocol`, ...) and service packages (`@pikku/jose`, `@pikku/schema-ajv`, `@pikku/pino`, `@pikku/kysely`, `@pikku/redis`, `@pikku/queue-bullmq`, `@pikku/queue-pg-boss`, ...). For the full list with use cases, read `references/packages.md`.\n\n## Key Differences from Traditional Frameworks\n\n1. **No decorators** — plain functions + explicit wiring, not `@Get()` or `@Injectable()`\n2. **No classes required** — everything is functions and objects\n3. **Transport is configuration, not code** — business logic doesn't know about HTTP/WS/etc.\n4. **One function, many transports** — same function can serve HTTP, WebSocket, queue, and MCP simultaneously\n5. **Generated type safety** — clients are auto-generated with full types, not manually maintained\n6. **Schema-first validation** — Standard Schema (Zod/Valibot) replaces class-validator decorators\n", "pikku-config/SKILL.md": "---\nname: pikku-config\ndescription: >-\n Use when managing secrets, environment variables, config, or OAuth2 credentials in a Pikku app.\n Covers defineSecret, defineVariable, defineCredential, and typed config access. TRIGGER when:\n code uses defineSecret/defineVariable/defineCredential, user asks about env vars, secrets,\n config, OAuth2, or \"how do I access environment variables\". DO NOT TRIGGER when: user asks about\n API versioning/breaking changes (use pikku-versioning), service factories (use pikku-services),\n or auth middleware (use pikku-security).\ninstallGroups: [core]\n---\n\n# Pikku Config, Secrets & OAuth2\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nManage secrets, variables, and OAuth2 credentials. Never use `process.env` in Pikku functions — use typed services instead.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their versions\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## Secrets & Variables\n\n### `defineSecret(config)`\n\nDeclare a secret with a Zod schema for type-safe access:\n\n```typescript\ndefineSecret({\n name: string, // Secret identifier\n schema: ZodSchema, // Shape and validation\n})\n```\n\n### `defineVariable(config)`\n\nDeclare a variable (non-sensitive config) with a Zod schema:\n\n```typescript\ndefineVariable({\n name: string,\n schema: ZodSchema,\n})\n```\n\n### Accessing Secrets\n\n`secrets` is **not available inside functions, AI agents, workflows, permissions\nor any wire** — it is removed from their services type and throws at runtime if\nreached through a cast. Read it where you wire the app and hand the value to a\nservice:\n\n```typescript\n// services.ts — allowed\nconst createSingletonServices = pikkuServices(async (config, { secrets }) => ({\n stripe: new StripeService(await secrets.getSecret('STRIPE_CONFIG')),\n}))\n\n// functions/*.ts — ask the service, never the secret store\nexport const charge = pikkuFunc({\n func: async ({ stripe }, data) => stripe.charge(data.amount),\n})\n```\n\nAllowed: `pikkuServices`, `pikkuWireServices`, addon service factories,\nmiddleware. Everywhere else, the service you constructed is the interface.\n\n### Accessing Variables in Functions\n\n```typescript\n// Variables — plain-text configuration\nconst flags = await services.variables.getVariableJSON('VARIABLE_NAME')\n\n// Simple string access\nconst apiKey = services.variables.get('API_KEY')\n```\n\n### Local Development Services\n\n```typescript\nimport { LocalSecretService, LocalVariablesService } from '@pikku/core/services'\n\nconst createSingletonServices = pikkuServices(async (config) => ({\n secrets: new LocalSecretService(), // Reads from .env or local files\n variables: new LocalVariablesService(), // Reads from environment\n}))\n```\n\n### Usage Patterns\n\n```typescript\n// Declare secrets with typed schemas\ndefineSecret({\n name: 'STRIPE_CONFIG',\n schema: z.object({\n apiKey: z.string().startsWith('sk_'),\n webhookSecret: z.string(),\n }),\n})\n\n// In your services factory — fully typed\nconst config = await secrets.getSecret('STRIPE_CONFIG')\n// config.apiKey → string (autocompleted)\n// config.webhookSecret → string (autocompleted)\n\n// Declare variables\ndefineVariable({\n name: 'FEATURE_FLAGS',\n schema: z.object({\n darkMode: z.boolean(),\n maxUploadMB: z.number().default(10),\n }),\n})\n\n// Read it — typed and validated\nconst flags = await variables.getVariableJSON('FEATURE_FLAGS')\n// flags.darkMode → boolean\n// flags.maxUploadMB → number\n```\n\n## Credentials\n\n### `defineCredential(config)`\n\n```typescript\ndefineCredential({\n name: string, // Credential identifier\n displayName: string, // Human-readable name\n type: 'wire' | 'singleton', // Per-user ('wire') or platform-level ('singleton')\n schema: ZodSchema, // Shape of the stored credential\n oauth2?: { // Omit entirely for a plain API key\n appCredentialSecretId: string, // Secret holding { clientId, clientSecret }\n tokenSecretId: string, // Secret for token storage (auto-refreshed)\n authorizationUrl: string, // OAuth2 authorization endpoint\n tokenUrl: string, // OAuth2 token endpoint\n scopes: string[], // Required OAuth2 scopes\n },\n})\n```\n\n### Usage\n\n```typescript\n// Per-user API key — no oauth2 block\ndefineCredential({\n name: 'stripe',\n displayName: 'Stripe API Key',\n type: 'wire',\n schema: z.object({ apiKey: z.string() }),\n})\n\n// Platform-level OAuth (singleton)\ndefineCredential({\n name: 'slack',\n displayName: 'Slack',\n type: 'singleton',\n schema: z.object({ accessToken: z.string(), refreshToken: z.string() }),\n oauth2: {\n appCredentialSecretId: 'SLACK_OAUTH_APP',\n tokenSecretId: 'SLACK_OAUTH_TOKENS',\n authorizationUrl: 'https://slack.com/oauth/v2/authorize',\n tokenUrl: 'https://slack.com/api/oauth.v2.access',\n scopes: ['chat:write', 'channels:read'],\n },\n})\n\n// In your function — tokens refresh automatically\nconst response = await slackOAuth.request(\n 'https://slack.com/api/chat.postMessage',\n {\n method: 'POST',\n body: JSON.stringify({ channel, text }),\n }\n)\nconst data = await response.json()\n```\n\n## Key Rule\n\n**Never use `process.env` inside Pikku functions.** Use the `variables` or `secrets` service:\n\n```typescript\n// ❌ Wrong\nconst apiKey = process.env.API_KEY\n\n// ✅ Correct\nconst apiKey = services.variables.get('API_KEY')\n```\n\n`process.env` belongs only in server bootstrap code (`start.ts`).\n\n## Complete Example\n\n```typescript\n// schemas/config.ts\ndefineSecret({\n name: 'DATABASE_CONFIG',\n schema: z.object({\n connectionString: z.string().url(),\n maxPoolSize: z.number().default(10),\n }),\n})\n\ndefineVariable({\n name: 'APP_CONFIG',\n schema: z.object({\n appName: z.string(),\n maxUploadSizeMB: z.number().default(10),\n maintenanceMode: z.boolean().default(false),\n }),\n})\n\ndefineCredential({\n name: 'githubOAuth',\n displayName: 'GitHub OAuth',\n type: 'wire',\n schema: z.object({ accessToken: z.string(), refreshToken: z.string() }),\n oauth2: {\n appCredentialSecretId: 'GITHUB_OAUTH_APP',\n tokenSecretId: 'GITHUB_OAUTH_TOKENS',\n authorizationUrl: 'https://github.com/login/oauth/authorize',\n tokenUrl: 'https://github.com/login/oauth/access_token',\n scopes: ['read:user', 'repo'],\n },\n})\n\n// functions/admin.functions.ts\nexport const getAppStatus = pikkuSessionlessFunc({\n title: 'Get App Status',\n func: async ({ variables }) => {\n const appConfig = await variables.getVariableJSON('APP_CONFIG')\n return {\n appName: appConfig.appName,\n maintenanceMode: appConfig.maintenanceMode,\n }\n },\n})\n```\n", "pikku-cron/SKILL.md": "---\nname: pikku-cron\ndescription: >-\n Use when adding scheduled tasks, recurring jobs, or cron-based automation to a Pikku app. Covers\n wireScheduler, cron expressions, scheduled task wire object, and scheduler middleware. TRIGGER\n when: code uses wireScheduler, user asks about cron, scheduled tasks, recurring jobs, or \"run\n every X minutes/hours\". DO NOT TRIGGER when: user asks about background jobs with retries (use\n pikku-queue) or event-driven triggers (use pikku-trigger).\n---\n\n# Pikku Cron/Scheduler Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions to run on a schedule using cron expressions. Uses `pikkuVoidFunc` (no input/output).\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their types\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireScheduler(config)`\n\n```typescript\nimport { wireScheduler } from '@pikku/core/scheduler'\n\nwireScheduler({\n name: string, // Unique scheduler name\n schedule: string, // Cron expression\n func: PikkuVoidFunc, // Must be pikkuVoidFunc (no input/output)\n middleware?: PikkuMiddleware[],\n})\n```\n\n### Wire Object (`wire.scheduledTask`)\n\nInside scheduled functions:\n\n```typescript\nwire.scheduledTask.name // Scheduler name\nwire.scheduledTask.schedule // Cron expression string\nwire.scheduledTask.executionTime // When this execution was triggered\nwire.scheduledTask.skip(reason) // Skip this execution (no error)\n```\n\n### Cron Expression Reference\n\n```\n┌───────────── minute (0-59)\n│ ┌───────────── hour (0-23)\n│ │ ┌───────────── day of month (1-31)\n│ │ │ ┌───────────── month (1-12)\n│ │ │ │ ┌───────────── day of week (0-7, 0 and 7 = Sunday)\n│ │ │ │ │\n* * * * *\n```\n\nCommon patterns:\n\n| Expression | Meaning |\n| ------------- | -------------------------- |\n| `*/5 * * * *` | Every 5 minutes |\n| `0 9 * * *` | Daily at 9:00 AM |\n| `0 9 * * 1` | Every Monday at 9:00 AM |\n| `0 0 1 * *` | First of month at midnight |\n| `0 */6 * * *` | Every 6 hours |\n| `30 2 * * 0` | Sundays at 2:30 AM |\n\n## Usage Patterns\n\n### Basic Scheduled Task\n\n```typescript\nconst dailySummary = pikkuVoidFunc({\n title: 'Daily Summary',\n func: async ({ db, emailService, logger }) => {\n logger.info('Generating daily summary')\n const stats = await db.getDailyStats()\n await emailService.sendSummary(stats)\n },\n})\n\nwireScheduler({\n name: 'dailySummary',\n schedule: '0 9 * * *',\n func: dailySummary,\n})\n```\n\n### Using the Wire Object\n\n```typescript\nconst weeklyCleanup = pikkuVoidFunc({\n title: 'Weekly Cleanup',\n func: async ({ db, logger }, _input, wire) => {\n logger.info(`Running: ${wire.scheduledTask.name}`)\n logger.info(`Schedule: ${wire.scheduledTask.schedule}`)\n logger.info(`Execution time: ${wire.scheduledTask.executionTime}`)\n\n const staleCount = await db.countStaleTodos()\n if (staleCount === 0) {\n wire.scheduledTask.skip('No stale todos found')\n return\n }\n\n await db.deleteCompletedTodos({ olderThan: '30d' })\n logger.info(`Cleaned ${staleCount} stale todos`)\n },\n})\n\nwireScheduler({\n name: 'weeklyCleanup',\n schedule: '0 0 * * 0',\n func: weeklyCleanup,\n})\n```\n\n### Scheduler Middleware\n\n```typescript\nconst schedulerMetrics = pikkuMiddleware(\n async ({ logger }, { scheduledTask }, next) => {\n const start = Date.now()\n logger.info(`Task started: ${scheduledTask.name}`)\n\n try {\n await next()\n logger.info(`Task completed: ${scheduledTask.name}`, {\n duration: Date.now() - start,\n })\n } catch (error) {\n logger.error(`Task failed: ${scheduledTask.name}`, {\n error: error.message,\n duration: Date.now() - start,\n })\n throw error\n }\n }\n)\n\nwireScheduler({\n name: 'dailySummary',\n schedule: '0 9 * * *',\n func: dailySummary,\n middleware: [schedulerMetrics],\n})\n```\n\n## Complete Example\n\n```typescript\n// functions/scheduled.functions.ts\nexport const dailySummary = pikkuVoidFunc({\n title: 'Daily Summary',\n func: async ({ db, emailService, logger }) => {\n const stats = await db.getDailyStats()\n await emailService.sendSummary(stats)\n logger.info('Daily summary sent', { stats })\n },\n})\n\nexport const cleanupExpired = pikkuVoidFunc({\n title: 'Cleanup Expired',\n func: async ({ db, logger }, _input, wire) => {\n const count = await db.countExpiredSessions()\n if (count === 0) {\n wire.scheduledTask.skip('No expired sessions')\n return\n }\n await db.deleteExpiredSessions()\n logger.info(`Cleaned ${count} expired sessions`)\n },\n})\n\nexport const syncInventory = pikkuVoidFunc({\n title: 'Sync Inventory',\n func: async ({ inventoryApi, db, logger }) => {\n const updates = await inventoryApi.getChanges()\n await db.applyInventoryUpdates(updates)\n logger.info(`Synced ${updates.length} inventory changes`)\n },\n})\n\n// wirings/scheduler.wiring.ts\nwireScheduler({\n name: 'dailySummary',\n schedule: '0 9 * * *',\n func: dailySummary,\n})\nwireScheduler({\n name: 'cleanupExpired',\n schedule: '0 */6 * * *',\n func: cleanupExpired,\n})\nwireScheduler({\n name: 'syncInventory',\n schedule: '*/15 * * * *',\n func: syncInventory,\n})\n```\n", "pikku-deploy-azure/SKILL.md": "---\nname: pikku-deploy-azure\ndescription: >-\n Use when deploying a Pikku app to Azure Functions. Covers PikkuAzFunctionsLogger and\n PikkuAzTimerRequest for Azure Functions runtime. TRIGGER when: user asks about Azure Functions,\n Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when: user asks about AWS Lambda\n (use pikku-deploy-lambda) or Cloudflare Workers (use pikku-deploy-cloudflare).\n---\n\n# Pikku Azure Functions Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/azure-functions` provides Azure Functions runtime adapters for Pikku.\n\n## Installation\n\n```bash\nyarn add @pikku/azure-functions @azure/functions\n```\n\n## API Reference\n\n### `PikkuAzFunctionsLogger`\n\nLogger implementation that integrates with Azure Functions' built-in logging context.\n\n### `PikkuAzTimerRequest`\n\nTimer trigger request handler for running Pikku scheduled functions as Azure Timer Triggers.\n\n## Usage Patterns\n\n### HTTP Function\n\n```typescript\nimport { app } from '@azure/functions'\nimport { PikkuAzFunctionsLogger } from '@pikku/azure-functions'\n\napp.http('api', {\n methods: ['GET', 'POST', 'PUT', 'DELETE'],\n route: '{*path}',\n handler: async (request, context) => {\n const logger = new PikkuAzFunctionsLogger(context)\n // Wire Pikku HTTP runner with Azure request/response\n },\n})\n```\n\n### Timer Trigger\n\n```typescript\nimport { app } from '@azure/functions'\nimport { PikkuAzTimerRequest } from '@pikku/azure-functions'\n\napp.timer('scheduler', {\n schedule: '0 */5 * * * *',\n handler: async (timer, context) => {\n const request = new PikkuAzTimerRequest(timer)\n // Process scheduled Pikku functions\n },\n})\n```\n", "pikku-deploy-cloudflare/SKILL.md": "---\nname: pikku-deploy-cloudflare\ndescription: >-\n Use when deploying a Pikku app to Cloudflare Workers. Covers HTTP fetch handler, scheduled\n tasks, and WebSocket via Durable Objects. TRIGGER when: code imports @pikku/cloudflare, user\n mentions Cloudflare Workers deployment, or worker entry uses ExportedHandler/wrangler.toml. DO\n NOT TRIGGER when: just defining functions/wirings without Cloudflare-specific code.\ninstallGroups: [fabric]\n---\n\n# Pikku Cloudflare Workers Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n```bash\nyarn add @pikku/cloudflare\n```\n\n## Worker Entry\n\n```typescript\nimport { runFetch, runScheduled } from '@pikku/cloudflare'\nimport { setupServices } from './setup-services.js'\nimport './.pikku/pikku-bootstrap.gen.js'\n\nexport default {\n async scheduled(controller, env) {\n await setupServices(env)\n await runScheduled(controller)\n },\n\n async fetch(request, env): Promise<Response> {\n await setupServices(env)\n return await runFetch(request as unknown as Request)\n },\n} satisfies ExportedHandler<Record<string, string>>\n```\n\n## Service Setup\n\nCloudflare passes env variables per-request — wrap them with Pikku services:\n\n```typescript\n// setup-services.ts\nimport { LocalVariablesService, LocalSecretService } from '@pikku/core/services'\nimport { createConfig, createSingletonServices } from './services.js'\n\nexport const setupServices = async (\n env: Record<string, string | undefined>\n) => {\n const localVariables = new LocalVariablesService(env)\n const config = await createConfig(localVariables)\n const localSecrets = new LocalSecretService(localVariables)\n return await createSingletonServices(config, {\n variables: localVariables,\n secrets: localSecrets,\n })\n}\n```\n\n## WebSocket (Durable Objects)\n\n```typescript\nimport { CloudflareWebSocketHibernationServer } from '@pikku/cloudflare'\n\nexport class WebSocketHibernationServer extends CloudflareWebSocketHibernationServer {\n protected async getParams() {\n const singletonServices = await setupServices(this.env)\n return { singletonServices }\n }\n}\n```\n\nRegister the Durable Object in `wrangler.toml` and export from the worker entry.\n", "pikku-deploy-express/SKILL.md": "---\nname: pikku-deploy-express\ndescription: >-\n Use when deploying a Pikku app with Express. Covers PikkuExpressServer standalone and\n pikkuExpressMiddleware for existing Express apps. TRIGGER when: code imports @pikku/express or\n @pikku/express-middleware, user mentions Express deployment, or start.ts creates a\n PikkuExpressServer. DO NOT TRIGGER when: just defining functions/wirings without\n Express-specific code.\n---\n\n# Pikku Express Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n## Standalone Server\n\n```bash\nyarn add @pikku/express\n```\n\n```typescript\nimport { PikkuExpressServer } from '@pikku/express'\nimport './.pikku/pikku-bootstrap.gen.js'\nimport { createConfig, createSingletonServices } from './services.js'\n\nconst config = await createConfig()\nconst singletonServices = await createSingletonServices(config)\n\nconst appServer = new PikkuExpressServer(\n { ...config, port: 4002, hostname: 'localhost' },\n singletonServices.logger\n)\nappServer.enableExitOnSigInt()\nawait appServer.init()\nawait appServer.start()\n```\n\n**Constructor:** `new PikkuExpressServer(config, logger)`\n\n**Config extends CoreConfig with:**\n\n- `port: number`\n- `hostname: string`\n- `healthCheckPath?: string`\n- `limits?: Partial<Record<string, string>>`\n- `content?: LocalContentConfig` (for static assets / file uploads)\n\n**Methods:**\n\n- `init(httpOptions?): Promise<void>` — Register middleware and routes\n- `start(): Promise<void>` — Start listening\n- `stop(): Promise<void>` — Graceful shutdown\n- `enableExitOnSigInt(): Promise<void>` — SIGINT handler\n- `enableCors(options): void` — Enable CORS\n- `enableStaticAssets(): void` — Serve static files (requires `content` config)\n\n**Property:** `app: Express` — Direct access to Express instance for custom middleware.\n\n## Middleware (existing Express app)\n\n```bash\nyarn add @pikku/express-middleware\n```\n\n```typescript\nimport express from 'express'\nimport { pikkuExpressMiddleware } from '@pikku/express-middleware'\nimport './.pikku/pikku-bootstrap.gen.js'\n\nconst app = express()\napp.use(\n pikkuExpressMiddleware({\n logger: singletonServices.logger,\n logRoutes: true,\n loadSchemas: true,\n })\n)\n```\n", "pikku-deploy-fastify/SKILL.md": "---\nname: pikku-deploy-fastify\ndescription: >-\n Use when deploying a Pikku app with Fastify. Covers PikkuFastifyServer standalone and\n pikkuFastifyPlugin for existing Fastify apps. TRIGGER when: code imports @pikku/fastify or\n @pikku/fastify-plugin, user mentions Fastify deployment, or start.ts creates a\n PikkuFastifyServer. DO NOT TRIGGER when: just defining functions/wirings without\n Fastify-specific code.\n---\n\n# Pikku Fastify Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n## Standalone Server\n\n```bash\nyarn add @pikku/fastify\n```\n\n```typescript\nimport { PikkuFastifyServer } from '@pikku/fastify'\nimport './.pikku/pikku-bootstrap.gen.js'\nimport { createConfig, createSingletonServices } from './services.js'\n\nconst config = await createConfig()\nconst singletonServices = await createSingletonServices(config)\n\nconst appServer = new PikkuFastifyServer(\n { ...config, hostname: 'localhost', port: 4002 },\n singletonServices.logger\n)\nappServer.enableExitOnSigInt()\nawait appServer.init()\nawait appServer.start()\n```\n\n**Constructor:** `new PikkuFastifyServer(config, logger)`\n\n**Config extends CoreConfig with:** `port`, `hostname`, `healthCheckPath?`\n\n**Methods:** `init(httpOptions?)`, `start()`, `stop()`, `enableExitOnSigInt()`\n\n**Property:** `app: FastifyInstance` — Direct access to Fastify instance.\n\n## Plugin (existing Fastify app)\n\n```bash\nyarn add @pikku/fastify-plugin\n```\n\n```typescript\nimport Fastify from 'fastify'\nimport pikkuFastifyPlugin from '@pikku/fastify-plugin'\nimport './.pikku/pikku-bootstrap.gen.js'\n\nconst app = Fastify()\napp.register(pikkuFastifyPlugin, {\n pikku: {\n logger: singletonServices.logger,\n logRoutes: true,\n loadSchemas: true,\n },\n})\n```\n", "pikku-deploy-lambda/SKILL.md": "---\nname: pikku-deploy-lambda\ndescription: >-\n Use when deploying a Pikku app to AWS Lambda. Covers HTTP handlers, scheduled tasks, SQS queue\n workers, WebSocket via API Gateway, and cold start caching. TRIGGER when: code imports\n @pikku/lambda, user mentions Lambda/serverless/AWS deployment, or handler files export\n Lambda-typed functions. DO NOT TRIGGER when: just defining functions/wirings without\n Lambda-specific code.\n---\n\n# Pikku AWS Lambda Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n```bash\nyarn add @pikku/lambda\n```\n\n## Cold Start Pattern\n\nCache singleton services across Lambda invocations:\n\n```typescript\n// cold-start.ts\nimport './.pikku/pikku-bootstrap.gen.js'\nimport { createConfig, createSingletonServices } from './services.js'\n\nlet singletonServices: SingletonServices | undefined\n\nexport const coldStart = async () => {\n if (!singletonServices) {\n const config = await createConfig()\n singletonServices = await createSingletonServices(config)\n }\n return singletonServices\n}\n```\n\n## HTTP Handler\n\n```typescript\nimport type { APIGatewayProxyEvent } from 'aws-lambda'\nimport { runFetch } from '@pikku/lambda/http'\n\nexport const httpRoute = async (event: APIGatewayProxyEvent) => {\n await coldStart()\n return await runFetch(event)\n}\n```\n\n## Scheduled Tasks\n\n```typescript\nimport type { ScheduledHandler } from 'aws-lambda'\nimport { runScheduledTask } from '@pikku/core/scheduler'\n\nexport const myScheduledTask: ScheduledHandler = async () => {\n await coldStart()\n await runScheduledTask({ name: 'myScheduledTask' })\n}\n```\n\n## SQS Queue Worker\n\n```typescript\nimport type { SQSHandler } from 'aws-lambda'\nimport { runSQSQueueWorker } from '@pikku/lambda/queue'\n\nexport const mySQSWorker: SQSHandler = async (event) => {\n const { logger } = await coldStart()\n return runSQSQueueWorker(logger, event)\n}\n```\n\n## WebSocket (API Gateway v2)\n\n```typescript\nimport {\n connectWebsocket,\n disconnectWebsocket,\n processWebsocketMessage,\n LambdaEventHubService,\n} from '@pikku/lambda/websocket'\n\nexport const connectHandler = async (event) => {\n const params = await getParams(event)\n await connectWebsocket(event, params)\n return { statusCode: 200, body: '' }\n}\n\nexport const disconnectHandler = async (event) => {\n const params = await getParams(event)\n return await disconnectWebsocket(event, params)\n}\n\nexport const defaultHandler = async (event) => {\n const params = await getParams(event)\n return await processWebsocketMessage(event, params)\n}\n```\n\nWebSocket requires a `ChannelStore` (e.g., `PgChannelStore`) and `LambdaEventHubService` for cross-connection messaging.\n", "pikku-deploy-nextjs/SKILL.md": "---\nname: pikku-deploy-nextjs\ndescription: >-\n Use when deploying a Pikku app with Next.js. Covers API route handlers, server-side data\n fetching, and RPC calls from Server Components. TRIGGER when: code imports @pikku/next, user\n mentions Next.js integration, or app/api route files use pikkuAPIRequest. DO NOT TRIGGER when:\n just defining functions/wirings without Next.js-specific code.\n---\n\n# Pikku Next.js Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n```bash\nyarn add @pikku/next\n```\n\n## API Route Handler\n\nThe CLI generates a typed wrapper. Use it in a catch-all route:\n\n```typescript\n// app/api/[...route]/route.ts\nimport { pikkuAPIRequest } from '@/pikku-nextjs.gen.js'\n\nexport const GET = pikkuAPIRequest\nexport const POST = pikkuAPIRequest\nexport const PUT = pikkuAPIRequest\nexport const PATCH = pikkuAPIRequest\nexport const DELETE = pikkuAPIRequest\n```\n\n## Server-Side Data Fetching\n\nUse the generated `pikku()` helper in Server Components or Server Actions:\n\n```typescript\nimport { pikku } from '@/pikku-nextjs.gen.js'\n\nconst { get, post, del, rpc, staticGet, staticPost, staticRPC } = pikku()\n\n// Dynamic (reads headers/cookies — requires request context)\nconst todos = await get('/todos')\nconst created = await post('/todos', { title: 'Buy milk' })\n\n// Static (no request context — suitable for precompile/ISR)\nconst config = await staticGet('/config')\n\n// RPC calls\nconst result = await rpc('calculateTax', { amount: 100, region: 'US' })\n```\n\n**Dynamic vs Static:**\n\n- `get`, `post`, `del`, `rpc` — access headers/cookies, use in dynamic Server Components\n- `staticGet`, `staticPost`, `staticRPC` — no request context, safe for precompile/ISR\n\n## How It Works\n\n`PikkuNextJS` lazy-initializes on first request:\n\n```typescript\nimport { PikkuNextJS } from '@pikku/next'\n\nconst pikku = new PikkuNextJS(createConfig, createSingletonServices)\n```\n\n**Constructor:** `new PikkuNextJS(createConfig?, createSingletonServices)`\n\nThe generated `pikku-nextjs.gen.ts` wraps this with full type safety from your route definitions.\n", "pikku-deploy-uws/SKILL.md": "---\nname: pikku-deploy-uws\ndescription: >-\n Use when deploying a Pikku app with uWebSockets.js. Covers PikkuUWSServer with built-in HTTP and\n WebSocket support, and pikkuWebsocketHandler for standalone ws library. TRIGGER when: code\n imports @pikku/uws or @pikku/ws, user mentions uWebSockets or high-performance server, or\n start.ts creates a PikkuUWSServer. DO NOT TRIGGER when: just defining functions/wirings without\n uWS-specific code.\n---\n\n# Pikku uWebSockets.js Deployment\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nHighest-throughput option among Pikku's runtimes. Handles both HTTP and WebSocket automatically.\n\n```bash\nyarn add @pikku/uws\n```\n\n```typescript\nimport { PikkuUWSServer } from '@pikku/uws'\nimport './.pikku/pikku-bootstrap.gen.js'\nimport { createConfig, createSingletonServices } from './services.js'\n\nconst config = await createConfig()\nconst singletonServices = await createSingletonServices(config)\n\nconst appServer = new PikkuUWSServer(\n { ...config, hostname: 'localhost', port: 4002 },\n singletonServices.logger\n)\nappServer.enableExitOnSigInt()\nawait appServer.init()\nawait appServer.start()\n```\n\n**Constructor:** `new PikkuUWSServer(config, logger)`\n\n**Config extends CoreConfig with:** `port`, `hostname`, `healthCheckPath?`\n\n**Methods:** `init(httpOptions?)`, `start()`, `stop()`, `enableExitOnSigInt()`\n\n**Property:** `app: uWS.App` — Direct access to uWebSockets app instance.\n\n## WebSocket Standalone (ws library)\n\nFor WebSocket-only servers using the `ws` library:\n\n```bash\nyarn add @pikku/ws\n```\n\n```typescript\nimport { pikkuWebsocketHandler } from '@pikku/ws'\nimport { stopSingletonServices } from '@pikku/core'\nimport { Server } from 'http'\nimport { WebSocketServer } from 'ws'\nimport './.pikku/pikku-bootstrap.gen.js'\n\nconst server = new Server()\nconst wss = new WebSocketServer({ noServer: true })\n\npikkuWebsocketHandler({\n server,\n wss,\n logger: singletonServices.logger,\n})\n\nserver.listen(4002, 'localhost', () => {\n console.log('Server running at http://localhost:4002/')\n})\n\nprocess.on('SIGINT', async () => {\n await stopSingletonServices()\n wss.close()\n server.close()\n process.exit(0)\n})\n```\n", "pikku-deps/SKILL.md": "---\nname: pikku-deps\ndescription: >-\n Use for the Pikku dependency security audit: the `pikku audit` CLI command, the\n `.pikku/audit.json` artifact, the `SecurityAuditReport` type in @pikku/core, and the console\n Security screen (getSecurityAudit / runSecurityAudit / updateDependency + SecurityAuditView).\n TRIGGER when: user asks about `pikku audit`, dependency vulnerabilities/advisories, outdated\n dependencies, the Security screen/page in the console, updating a vulnerable dependency, or\n reading/rendering audit.json. DO NOT TRIGGER when: user asks about authentication/sessions/JWT\n (use pikku-security), permissions (use pikku-permissions), or secrets/env vars (use\n pikku-config).\ninstallGroups: [core]\n---\n\n# Pikku Dependency Audit\n\n## Agent Operating Procedure\n\n1. The audit is a generated artifact, not live state. `pikku audit` writes the\n normalised report to `.pikku/audit.json` (config `outDir`), so it rides the\n same meta pipeline as every other codegen output — uploaded on deploy,\n readable by the console addon and any tooling. Read it via\n `metaService.readFile('audit.json')`, never by shelling out to the package\n manager from a function.\n2. One source of truth for the shape: `SecurityAuditReport` (and\n `SecurityAuditIssue` / `SecurityAuditUpdate` / `SecurityAuditSummary` +\n `SecuritySeverity` / `SecurityUpdateLevel`) are exported from **@pikku/core**.\n The CLI writes it, the addon reads it, the UI renders it — never redeclare\n the type at a call site.\n3. Validate with `pikku all --tsc` after changes — it type-checks and **fails on\n type errors**, like any real build gate. Separately, `pikku audit` never fails\n a build: advisories are informational, and a missing/failed audit yields an\n empty-but-valid report.\n\n## The `pikku audit` command\n\n- `pikku audit` — reports **security advisories** only.\n- `pikku audit --outdated` — also reports **available dependency updates**.\n- Package-manager detection is by **lockfile** (walks up: `bun.lock`/`bun.lockb`,\n then `yarn.lock`). Only **bun** runs a real audit (`bun audit --json` +\n `bun outdated`, normalised into one `SecurityAuditReport` with per-severity /\n per-update-level counts). Other PMs are detected but **stubbed** with a `note`\n field until their shapes are normalised — issues/updates come back empty.\n- `bun audit` exits non-zero when it *finds* advisories but still writes a valid\n report — treat any non-zero exit as data, not failure.\n\n## Console integration (@pikku/addon-console)\n\nThree RPCs, all reading/writing the same artifact via the meta service. Shared\nspawn/read helpers live in `lib/audit-exec.ts` (`readAuditReport`,\n`runPikkuAudit`, `spawnProcess`, `findBin`) — reuse them, don't re-implement.\nLike every console RPC these require an **authenticated session** (the console\nis admin-only), so the host must have Better Auth wired — see `pikku-better-auth`.\n\n- `getSecurityAudit` — reads `.pikku/audit.json`, returns the report (or `null`).\n- `runSecurityAudit` — runs `pikku audit --outdated` server-side (regenerates the\n artifact) then returns the fresh report. Same shape as the Run Tests action.\n- `updateDependency({ package, version })` — bumps the package in `package.json`\n (preserving the `^`/`~` range prefix), runs `bun install`, re-audits, and\n returns the fresh report. Throws if the package is not a direct dependency.\n NOTE: `bun install` must be scoped to a standalone project — do not run it\n inside a yarn/bun monorepo member (it resolves the whole workspace).\n\n## Console UI (@pikku/console)\n\n- `SecurityPage` — the page: **Run audit** button (`lead`) + responsive\n `ShellHeader` (structured `search` + `selection` for the Issues/Dependencies\n lens; never cram raw controls into the non-collapsing `filters`/`view` escape\n hatch). Empty state until an audit has run.\n- `SecurityAuditView` — exported presentational component. Two lenses\n (Issues grouped by severity; Dependencies table). Each finding row carries its\n actions **right-aligned in the row header** (`Accordion.Control` sibling, so a\n click acts instead of toggling): \"View advisory\" + a per-finding\n **remediation slot**.\n- `renderRemediation({ pkg, version, issue })` — the extension seam. OSS default\n is `UpdateDependencyButton` (the free bump + `bun install`). Downstream\n consoles (Fabric) pass their own sandbox-verified action here — replace the\n action, keep the view.\n- Hooks: `useSecurityAudit` (read), `useRunSecurityAudit` (run),\n `useUpdateDependency` (bump). All are `useMutation`/`useQuery` — surface\n `mutation.error`, never hand-roll loading/error state or swallow the error.\n\n## Report shape (SecurityAuditReport)\n\n```ts\n{\n tool: string // e.g. 'bun'\n note?: string // set when the audit could NOT run (unsupported PM);\n // render ONLY the note — never a reassuring \"no vulnerabilities\"\n summary: { critical, high, moderate, low, info: number }\n issues: SecurityAuditIssue[] // package, severity, title, advisoryId, cwe[], cvssScore?,\n // url?, vulnerableVersions, recommendedVersion?\n updates: SecurityAuditUpdate[] // package, current, latest, level (major|minor|patch|unknown)\n}\n```\n\nWhen `note` is present the audit did not run — show only the note (an \"Audit not\nrun\" state), never the \"no known vulnerabilities / up to date\" copy.\n", "pikku-emails/SKILL.md": "---\nname: pikku-emails\ndescription: >-\n Use when working with Pikku's file-based email templates: authoring HTML/subject/text templates,\n locales, partials and theme, running `pikku emails generate`, and rendering/sending them through\n an EmailService. TRIGGER when: code uses renderEmailTemplate, EmailTemplateName, EmailService,\n SendTemplateEmailInput, LocalEmailService, or imports from .pikku/email/pikku-emails.gen.\n TRIGGER when: the project has an emails/ directory (templates/, locales/, partials/, theme.json)\n or emailTemplatesDir in pikku.config.json. TRIGGER when: user asks to add/edit a transactional\n email (verification, password reset, invitation, receipt), wire email sending, or translate an\n email. DO NOT TRIGGER when: user asks about i18n for the app UI (use pikku-i18n) or auth flows\n in general (use pikku-better-auth).\ninstallGroups: [core]\n---\n\n# Pikku Emails\n\nPikku compiles a directory of plain template files into a typed, dependency-free\nrenderer. `pikku emails generate` reads `emailTemplatesDir` and writes\n`.pikku/email/pikku-emails.gen.ts` (the `renderEmailTemplate` function + per-template\ntypes) and `pikku-emails-meta.gen.json`. Templates are authored as files; the\ngenerated output is never edited by hand.\n\n## Agent Operating Procedure\n\n1. Edit source files under `emailTemplatesDir` only. Never edit `.pikku/email/*`.\n2. After any change run `pikku emails generate` (it is also part of `prebuild`, usually\n `pikku bootstrap; pikku all; pikku emails generate`).\n3. Validate by importing `renderEmailTemplate` and rendering with sample data, or run the\n project's typecheck — the generated `data` type will flag missing/wrong variables.\n4. Fix the source cause; do not patch generated files or update hashes by hand.\n\n## Config\n\n```jsonc\n// pikku.config.json\n{\n \"emailTemplatesDir\": \"emails\", // relative to rootDir; omit to disable emails\n \"outDir\": \".pikku\" // gen lands in <outDir>/email/\n}\n```\n\nIf `emailTemplatesDir` is unset the command is a no-op.\n\n## Directory layout\n\n```text\nemails/\n theme.json # brand tokens: appName, fonts, colors\n locales/\n en.json # translation strings, nested namespaces\n de.json # one file per locale (filename = locale key)\n partials/\n layout.html # outer wrapper; must include {{content}}\n footer.html # reusable fragment, included with {{> footer}}\n templates/\n verify-email.html # body (required)\n verify-email.subject.txt # subject line (required)\n verify-email.text.txt # plain-text alternative (optional)\n```\n\nA template's **name** is its filename without the `.html` / `.subject.txt` / `.text.txt`\nsuffix (`verify-email` above). `html` and `subject` are required; `text` is optional and,\nwhen present, becomes the plain-text MIME part.\n\n## Templating syntax\n\nPlaceholders are `{{ ... }}`. Resolution order inside a template:\n\n- `{{appName}}` — from `data.appName`, falling back to `theme.appName`.\n- `{{theme.colors.accent}}`, `{{theme.fonts.body}}` — values from `theme.json`.\n- `{{t.verifyEmail.heading}}` — string from the active locale file (`locales/<locale>.json`).\n- `{{verifyUrl}}` — any other key is a **runtime variable**, supplied via `data`.\n- `{{> footer}}` — include a partial from `partials/`.\n- `{{content}}` / `{{subject}}` — only meaningful inside `partials/layout.html`\n (the rendered body and subject). `layout.html` wraps every template if present.\n\nLocale strings may themselves contain variables and partial-free placeholders, e.g.\n`\"subject\": \"{{inviterName}} invited you to join {{organizationName}}\"`. These are\nresolved in the same pass, so a subject of `{{t.invitation.subject}}` expands fully.\n\n## Typed variables (per template)\n\nThe generator extracts the runtime variables each template references and emits a typed\n`data` shape. Extraction is **scoped to the template**: it walks the template's\nhtml/subject/text, the partials it includes, and only the locale keys it actually\nreferences (transitively) — variables from unrelated locale entries do not leak in.\n\n```ts\nimport {\n renderEmailTemplate,\n type EmailTemplateName,\n type EmailTemplateVariables,\n} from './.pikku/email/pikku-emails.gen.js'\n\n// EmailTemplateVariables<'organization-invitation'> =\n// { appName?: ...; inviteUrl?: ...; inviterName?: ...; organizationName?: ... }\n```\n\nTo make a variable required-and-typed, reference it directly in the template body (not\nonly in a locale string), so it shows up as that template's variable.\n\n## Rendering\n\n```ts\nconst rendered = renderEmailTemplate({\n name: 'verify-email', // EmailTemplateName (autocompleted)\n locale: 'en', // optional, defaults to 'en'\n data: { verifyUrl: url }, // EmailTemplateVariables<'verify-email'>\n})\n// rendered: { name, locale, subject, html, text?, variables, hash }\n```\n\n`hash` is a stable content hash (useful as an idempotency / dedupe key on outgoing mail).\n\n## Sending through an EmailService\n\n`@pikku/core/services` defines `EmailService.send(input)` where `input` is one of\n`SendTextEmailInput`, `SendHTMLEmailInput`, or `SendTemplateEmailInput`:\n\n```ts\nimport type { EmailService } from '@pikku/core/services'\n\nawait email.send({\n to: user.email,\n template: { name: 'verify-email', locale: user.locale, data: { verifyUrl } },\n})\n```\n\n`LocalEmailService` (dev/test) captures the payload as-is. To actually render templates\nbefore sending, wrap a delegate service: when `input.template` is present, call\n`renderEmailTemplate` and forward `subject` / `html` / `text` to the delegate (e.g. a\nResend/SES/SMTP service). This wrapper is project-owned because `renderEmailTemplate`\nis generated per project; wire it in `services.ts` and inject it into functions.\n\n```ts\nasync send(input: SendEmailInput) {\n if (!('template' in input) || !input.template) return this.delegate.send(input)\n const r = renderEmailTemplate(input.template as RenderEmailInput<EmailTemplateName>)\n return this.delegate.send({\n to: input.to, from: input.from, subject: r.subject, html: r.html,\n ...(r.text ? { text: r.text } : {}),\n })\n}\n```\n\n## Generated artifacts\n\n- `.pikku/email/pikku-emails.gen.ts` — `renderEmailTemplate`, `EmailTemplateName`,\n `EmailLocale`, `EmailTemplateVariables<T>`, inlined templates/locales/partials/theme.\n- `.pikku/email/pikku-emails-meta.gen.json` — per-template `variables`, `hasHtml/Subject/Text`,\n and per-locale content hashes. Both are regenerated; keep them out of hand edits and\n (typically) git-ignored.\n\n## Gotchas\n\n- New template not appearing → you added `.html` but forgot `.subject.txt` (subject is\n required), or didn't rerun `pikku emails generate`.\n- Variable typed `unknown`/missing → it's only in a locale string for a different template;\n reference it in this template to scope it in.\n- Editing a locale string changes that template's content hash — expected; the hash covers\n the strings the template uses.\n- `layout.html` must contain `{{content}}` or the body is dropped.\n", "pikku-fabric/SKILL.md": "---\nname: pikku-fabric\ndescription: 'Build and convert apps for the Pikku Fabric platform. Covers SQLite/libSQL database setup with Kysely, fabric project layout, deploy provider config, `fabric.config.json`, and the pikku-verify workflow. TRIGGER when: user is working on a Fabric-hosted Pikku project, converting an app to Fabric format, or asking about Fabric deployment, database, or project conventions. DO NOT TRIGGER when: user is working on a generic (non-Fabric) Pikku deployment — use pikku-deploy-cloudflare, pikku-deploy-fastify, etc. instead.'\ninstallGroups: [fabric]\n---\n\n# Pikku Fabric\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. **Run structural validation first.** Before any edit, run:\n ```bash\n pikku fabric validate --json\n ```\n This prints every missing file, misconfigured field, and dependency gap with a `fixHint`. Address all `error` findings before proceeding — they block deploy. Resolve `warn` findings before testing — they cause runtime failures. `info` findings are best-practice gaps that are safe to defer.\n2. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n3. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n4. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n5. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n6. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nFabric is a serverless deployment platform for Pikku apps. Every Fabric app runs on Cloudflare Workers with a SQLite database (via libSQL/Turso). This skill covers what's unique to Fabric. For general Pikku concepts, function authoring, HTTP wiring, and more, see `pikku-concepts`, `pikku-http`, `pikku-services`, etc.\n\n## Before you start\n\nAlways run project discovery first:\n\n```bash\nyarn pikku meta context --json\n```\n\nCall the `pikku-meta` tool before grepping or editing a Fabric app.\n\n- Use `section: \"context\"` for the project map: functions, wires, workflows, capabilities, and source files.\n- Use `section: \"clients\"` before frontend/RPC work.\n- Use `section: \"functions\"` to list function ids, then `section: \"function\", id: \"<functionId>\"` for one function.\n- Use `section: \"schemas\"` to list schema names. Only request full JSON Schema bodies with `schemas: [\"SchemaName\"]` for the specific schemas needed.\n\nDo not load every schema body by default; that wastes context and usually makes the model worse.\n\nFor database work:\n\n- Use `pikku-db` for the actual attached Fabric database state: tables, columns, foreign keys, and applied migrations.\n- Use `pikku-meta` `section: \"schemas\"` for code-level JSON Schema contracts, not database introspection.\n- Do not inspect database credentials or connect to the database directly; Fabric Control already exposes the safe introspection surface.\n\n## Database: SQLite via libSQL\n\nFabric apps use SQLite, accessed via Kysely with the libSQL HTTP adapter. NOT PostgreSQL, NOT D1.\n\n### Setup in `services.ts`\n\n```typescript\nimport { Kysely, CamelCasePlugin } from 'kysely'\nimport { LibsqlWebDialect } from '@pikku/kysely-sqlite'\nimport type { DB } from '#pikku/db/schema.gen.js'\n\nconst databaseUrl = await variables.get('DATABASE_URL')\nlet kysely: Kysely<DB>\nif (databaseUrl) {\n kysely = new Kysely<DB>({\n dialect: new LibsqlWebDialect({ url: databaseUrl }),\n plugins: [new CamelCasePlugin()],\n })\n} else if (existingServices?.kysely) {\n kysely = existingServices.kysely as Kysely<DB>\n} else {\n throw new Error('kysely not provided and DATABASE_URL is unset')\n}\n```\n\nFabric injects `DATABASE_URL` as a variable binding when the stage starts. In local dev, `pikku db migrate` uses a local `dev.db` SQLite file.\n\n### Migrations\n\nMigrations are plain `.sql` files at the **project root**, in a directory named\nfor the engine — `db/sqlite/` for SQLite/libSQL stages, `db/postgres/` for\nPostgres ones. Never `db/migrations/`, and never under `packages/functions/`:\nthe deploy pipeline stages `db/<engine>/*.sql` from the root and applies them\nafter upload, so a migration anywhere else is silently never run.\n\n```\ndb/sqlite/\n 0001-init.sql\n 0002-add-users.sql\n```\n\nNumbers must be consecutive and gap-free, and an applied migration is frozen —\ncorrect a mistake with a new forward migration, never by editing or renaming one\nthat has already run (the recorded hash will no longer match).\n\nRun migrations: `pikku db migrate`. It also regenerates `.pikku/db/schema.gen.ts`\n(Kysely types) and `.pikku/db/zod.gen.ts` — there is no separate types step.\n\n**NEVER hand-edit the generated schema** — write a migration and re-run.\n\nA Better Auth app has a second constraint: the plugins you enable (`admin()`,\n`actor()`, …) each declare columns, and `pikku db migrate` refuses to run while\nthe applied schema is missing any of them. `pikku db generate` writes the\nmigration that closes the gap.\n\n### Column conventions\n\n- Use `SERIAL`/`INTEGER PRIMARY KEY AUTOINCREMENT` for IDs\n- Use `TEXT` for strings, `INTEGER` for booleans (0/1) and timestamps (Unix ms)\n- Use `CHECK` constraints sparingly — prefer app-level validation\n- Table and column names: snake_case in SQL, camelCase in TypeScript (via `CamelCasePlugin`)\n\n## Deploy Provider\n\n`pikku.config.json` (in the project root, not `packages/functions/`) **must** declare the Fabric deploy provider:\n\n```json\n{\n \"deploy\": {\n \"providers\": {\n \"cloudflare\": \"@pikkufabric/deploy-cloudflare\"\n }\n }\n}\n```\n\nWithout this, `pikku deploy plan --provider cloudflare` uses the OSS adapter which lacks Fabric's workflow service wiring.\n\nThe Fabric adapter automatically:\n\n- Injects `SQLiteKyselyWorkflowService` when `DATABASE_URL` is bound\n- Sets up the libSQL workflow queue\n- Wires `workflowQueues: true` for the scaffold\n\nNo manual workflow service setup is needed.\n\n## Project Layout\n\n```\npackages/functions/\n src/\n functions/ # Business logic — one pikkuFunc/workflow per file\n wirings/ # Transport bindings\n *.http.ts # wireHTTP / defineHTTPRoutes / wireHTTPRoutes\n *.channel.ts # wireChannel\n *.queue.ts # wireQueueWorker\n *.schedule.ts # wireScheduler\n *.mcp.ts # wireMCPTool\n *.cli.ts # wireCLI\n services.ts # pikkuServices factory (singleton)\n middleware.ts # Shared middleware\n permissions.ts # Shared permissions\n .pikku/\n db/schema.gen.ts # Kysely types, written by `pikku db migrate` — NEVER hand-edit\napps/app/ # Frontend(s)\ndb/sqlite/ # Plain .sql migrations, numbered, gap-free (project root)\npikku.config.json # Pikku + deploy config (project root)\npikkufabric.config.json # Fabric project link + frontends (project root)\n```\n\n## `pikkufabric.config.json`\n\nLinks the repo to a Fabric project and declares its frontends:\n\n```json\n{\n \"projectId\": \"my-project-id\",\n \"production\": {\n \"domain\": \"example.com\"\n },\n \"frontends\": {\n \"app\": {\n \"cwd\": \"apps/app\",\n \"primary\": true,\n \"deploy\": true,\n \"kind\": \"ssr\",\n \"dev\": {\n \"command\": [\"yarn\", \"dev\"],\n \"port\": 7105,\n \"healthPath\": \"/\"\n }\n }\n }\n}\n```\n\n- `projectId`: written by `pikku fabric init` / `link`. Templates ship the\n `__PROJECT_ID__` placeholder — that is _not_ a link, and the CLI treats it as\n unlinked.\n- `production.domain`: optional custom domain. Production always maps to `main`;\n without a domain it lives on the platform `*.pikkufabric.app` hostnames.\n- `frontends`: each entry declares a frontend app with its dev command and port\n\n## RPC is the default transport\n\nIn Fabric apps, most features don't need HTTP wirings. Just write the function with `expose: true` — Pikku generates an RPC client and React Query hooks automatically.\n\n```typescript\nexport const listTasks = pikkuSessionlessFunc({\n expose: true,\n readonly: true,\n func: async ({ kysely }, {}) => {\n return { tasks: await kysely.selectFrom('tasks').selectAll().execute() }\n },\n})\n```\n\nAdd `wireHTTP` only when you need a specific REST shape (webhooks, third-party callers).\n\n### Transport rule\n\n- Always use RPC first.\n- If the function should be callable from the app or other generated clients, prefer `expose: true`.\n- Use `expose: true` for public/generated client access unless the user explicitly wants a private function.\n- Do not add HTTP routes unless the user explicitly asks for HTTP/REST, or the project settings explicitly require HTTP transport.\n- Every new or changed function must have a real description.\n- If function metadata would show `missing description`, the work is not finished yet.\n\n## Run it locally\n\nA Fabric app is two processes: the pikku API server (`:3000`) and the frontend\n(vite). The starter template's `bun run dev` starts **both** and takes the whole\nsession down if either dies — a frontend running against a dead API looks like an\napp bug and is the single most common way to waste an hour here.\n\n```bash\nbun run prebuild # pikku all — codegen must be current before the server boots\nbun run dev\n```\n\nThen open the app, sign up as a real user, and click through what you built.\n**HTTP 200 is not evidence.** These are client-rendered pages: the server returns\n200 with an empty shell, so a page whose component throws still looks fine to\ncurl. Either open it in a browser or drive it headlessly and assert on rendered\ntext.\n\nSecrets come from `process.env`, which the CLI populates from a `.env` in the\nworking directory. `BETTER_AUTH_SECRET` is required — without it the first\nsign-up fails with `Requested secret not found`, which names no key and points at\nno file. The starter template generates one on first `bun run dev`.\n\nIf you are running the two processes yourself rather than through the template's\nscript, run `pikku dev` from the **project root** (it resolves `srcDirectories`\nrelative to the config, so a nested cwd yields a doubled watch path and no hot\nreload).\n\n## Deploy\n\n```bash\npikku fabric login # opens a browser; needs a human, wait for it\npikku fabric init https://github.com/<owner>/<repo>\npikku fabric validate # must pass clean\npikku fabric deploy plan --production\npikku fabric deploy apply --production --auto-apply\n```\n\n`apply` confirms before deploying, and with no TTY to ask — CI, an agent shell —\nit refuses rather than hangs. `--auto-apply` supplies that confirmation; drop it\nonly when a human is at a real terminal.\n\n`init` adopts a **GitHub** repo, and adoption goes through the Pikku Fabric\nGitHub App — the app has to be installed on the account or org that owns the\nrepo, and if it is installed with \"selected repositories\" this one must be in\nthe selection. There is no CLI flag that works around a missing installation:\n`init` returns \"Connect the GitHub account '<owner>'\". Send the user to install\nit, or create the project in the console instead (which provisions a Fabric-hosted\ngit repo you push to) and write the returned `projectId` into\n`pikkufabric.config.json` yourself.\n\nDeploy refuses to run unless the target branch equals its upstream — the guard\ncompares `main` against `main@{upstream}`. So the remote you pushed to must be\nthe one the branch tracks; a stale `origin` left over from scaffolding blocks\nthe deploy with \"local HEAD … ≠ remote …\" even though your code is pushed.\n`git branch --set-upstream-to=<remote>/main main` before deploying.\n\n## Versioning\n\nFunctions with `expose: true` are versioned via `versions.pikku.json`. When you change a function's input or output schema, you must bump its version number — otherwise `pikku all` will report a breaking change and callers' generated clients become stale.\n\nThe `pikku-verify` tool catches this automatically.\n\n## After every code change\n\nAlways call the `pikku-verify` tool after modifying functions, wirings, or schemas. It runs:\n\n1. `pikku all` — regenerates all codegen, checks version compliance\n2. `tsc --noEmit` — validates TypeScript types\n\nThe output card shows whether any breaking changes were detected.\n\n## Hard rules\n\nThese apply in every Fabric app:\n\n- **No `process.env`** — use `variables.get('NAME')` and `secrets.getSecret('NAME')`. Declare with `defineVariable` / `defineSecret`.\n- **No `as any`** — fix types properly.\n- **No generic `Error`** — throw `NotFoundError`, `ConflictError`, `BadRequestError`, `UnauthorizedError` from `@pikku/core/errors`.\n- **No auth checks in function bodies** — use `permissions:` field on the function config with a `pikkuPermission` factory.\n- **No hand-editing `.pikku/db/schema.gen.ts`** — write a migration and re-run `pikku db migrate`.\n- **One runtime unit per file** — never define multiple functions/workflows in a single source file.\n- **Workflow steps don't need manual wiring** — `pikkuSessionlessFunc` step functions in `*.steps.ts` files are auto-discovered by codegen.\n\n## Converting an existing app to Fabric format\n\nStart by running the structural validator — it tells you exactly what is missing:\n\n```bash\npikku fabric validate --json\n```\n\nFix every `error` and `warn` in the output before continuing. Then:\n\n1. **Replace the database layer**: swap PostgreSQL/MySQL queries for Kysely + libSQL. Convert schema to SQLite-compatible SQL migrations in `db/sqlite/`.\n2. **Replace route handlers with pikkuFuncs**: extract business logic into `pikkuFunc`/`pikkuSessionlessFunc`, add `wireHTTP` or `expose: true` for transport.\n3. **Replace DI/IoC with pikkuServices**: move service construction to `createSingletonServices` in `services.ts`.\n4. **Replace `process.env` calls**: plain config becomes `defineVariable` + `variables.get()`, anything sensitive becomes `defineSecret` + `secrets.getSecret()`.\n5. **Add `pikku.config.json`** at project root with `srcDirectories`, `outDir`, and `clientFiles`.\n6. **Add `fabric.config.json`** at project root with `projectId`, `production.branch`, and `frontends`.\n7. **Run `pikku all`** — verify codegen succeeds and there are no type errors.\n8. **Run `pikku fabric validate`** once more to confirm no structural issues remain.\n", "pikku-fabric-debug/SKILL.md": "---\nname: pikku-fabric-debug\ndescription: 'Debug a deployed Fabric stage from the CLI — read logs, find recent errors, follow a single request end-to-end by traceId, and check request/error/latency metrics. TRIGGER when: a deployed Fabric app is erroring, timing out, or behaving differently than local; the user asks \"why is prod failing\", \"check the logs\", \"what happened to this request\"; or a deploy succeeded but the app misbehaves. DO NOT TRIGGER when: the failure reproduces locally (debug it locally), the deploy itself failed (use pikku-fabric — that is a build/config problem, not a runtime one), or the project is not deployed to Fabric.'\ninstallGroups: [fabric]\n---\n\n# Debugging a deployed Fabric stage\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Reproduce locally first. If it fails locally too, debug it there — the\n deployed stage adds cost and latency to every iteration.\n2. Start from `errors`, not `logs`. Errors are already filtered and carry the\n traceId that unlocks the rest.\n3. Follow one trace end-to-end before forming a theory. A single failing request\n tells you more than a hundred unrelated log lines.\n4. Fix the source cause and redeploy. Never leave the diagnosis at \"it is flaky\".\n5. Confirm the fix against the same stage — recheck `errors` for the function.\n\nEvery command below requires a logged-in CLI and a linked project. Both fail\nwith the exact remediation if not:\n\n```\nNot logged in. Run `pikku fabric login` first.\nNo fabric project linked. Run `pikku fabric link` first.\n```\n\n## The loop\n\n**1 — What is broken?**\n\n```bash\npikku fabric errors -b main # branch defaults to main\npikku fabric errors -b main --function createOrder\n```\n\nPrints a `WHEN | FUNCTION | TRACE | MESSAGE` table. The message is **truncated\nto 100 characters** — treat it as a label, not the full error. The TRACE column\nis the input to the next step.\n\n**2 — What happened in that one request?**\n\n```bash\npikku fabric trace <traceId> -b main\npikku fabric trace <traceId> -b main --json\n```\n\n`--branch` is **required** here (no default). Each event prints as:\n\n```\n<timestamp> <scriptName> <wireType>:<wireId> <duration>ms — <error|message|outcome>\n```\n\nThis is the whole request across the stage — every unit it touched, in order,\nwith per-event durations. The last event before the failure is where to look.\n\n**3 — Is it one request or the whole stage?**\n\n```bash\npikku fabric metrics -b main # last 24h\npikku fabric metrics -b main --hours 2 --function createOrder\n```\n\nRows are `reqs= err= (rate%) avg= min= max=` per bucket. A single bad request\nwith a healthy error rate is a data problem; a climbing error rate is a\ndeployment or dependency problem. `--json` additionally returns a `wireTypes`\nbreakdown (requests per http/queue/scheduler/…) that the table output omits.\n\n**4 — Wider context around the failure**\n\n```bash\npikku fabric logs -b main\npikku fabric logs -b main --level warn\npikku fabric logs -b main -f # follow\n```\n\n`--branch` is **required** — `logs` throws `Specify --branch <branch-name>.`\nwithout it, even though the flag reads as optional.\n\n**5 — Is the running code the code you think it is?**\n\n```bash\npikku fabric status # active + in-flight deployment, per stage, with gitSha\n```\n\nCheck this *before* deep-diving. A stage still serving an older `gitSha`, or a\ndeploy stuck in flight, explains a whole class of \"my fix did nothing\".\n\n## Known gaps — do not misread these as bugs in your app\n\n- **`pikku fabric logs --since` and `--deployment` are accepted and ignored.**\n They are declared as options but the command never reads them, so\n `--since 15m` silently returns the same default window as no flag at all. Do\n not conclude \"nothing happened in the last 15 minutes\" from it. Narrow by\n `--level`, or by `--function` via `errors`, instead.\n- **`--follow` is a 2-second client-side poll, not a server stream.** It\n dedups against what it already printed, so it behaves like `tail -f`, but new\n entries can appear up to ~2s late and it holds the process open until killed.\n\n## What NOT to do\n\n- **Do not SSH anywhere or query the telemetry backend directly.** These\n commands are the supported surface; anything lower-level is Fabric-internal\n and will not exist for your project.\n- **Do not debug by redeploying with added `console.log`s.** Get the traceId,\n read the trace. A deploy cycle per hypothesis is the slow path.\n- **Do not read the truncated `errors` message as the full error.** Always\n confirm against `trace` before changing code.\n- **Do not treat an empty `errors` table as \"the app is fine\"** — a request that\n returns a wrong 200 logs nothing. Check `metrics` for the outcome mix.\n", "pikku-feature/SKILL.md": "---\nname: pikku-feature\ndescription: 'Drive create-a-feature work for a Pikku project: discover project context, work on a feature branch, implement + verify + commit, and ask the user to review via the diff. TRIGGER when: the user asks to \"create a feature\", \"build a todo app\", \"add X to my Pikku project\", \"wire up a new endpoint\", or anything that implies turning a natural-language request into Pikku functions/wirings/migrations. DO NOT TRIGGER when: the user asks for a one-off code edit in an existing function, or asks about Pikku concepts (use pikku-concepts).'\ninstallGroups: [core]\nallowed-tools: Bash(yarn pikku meta *), Bash(yarn pikku all *), Bash(yarn tsc), Bash(git status *), Bash(git diff *), Bash(git switch *), Bash(git checkout *), Bash(git checkout -b *), Bash(git add *), Bash(git commit *), Bash(git log *), Bash(git branch *)\nargument-hint: '<feature description>'\n---\n\n# Pikku Create-a-Feature\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nEnd-to-end flow: **discover → state intent → branch → implement → verify → commit → hand to reviewer**.\n\nThere is **no plan JSON**. The branch + diff IS the contract. The reviewer\nsees real, compiled, working code. Apply = merge. Reject = `git branch -D`.\n\n## Stage 1 — Discover\n\nRun **once** at the start of every feature request:\n\n```bash\nyarn pikku meta context --json\n```\n\nThis single call returns functions, wires, middleware, permissions, workflows,\n`capabilities` (which wire types are in use), and `layout` (where new files\nshould land).\n\nOnly fall back to targeted commands when you need full input/output JSON\nschemas (`yarn pikku meta functions get <id>`) or workflow steps\n(`yarn pikku meta workflows get <id>`).\n\n**Capability rule:** do not introduce new wires of a type whose\n`capabilities.<type>` is `false` unless the user explicitly asked for it.\n\n## Stage 2 — State intent in plain English (BEFORE writing code)\n\nBefore touching any files, give the user one paragraph stating exactly what\nyou'll do. This is the lightweight \"plan\" — it is chat, not JSON.\n\n> I'll add a `todos` table via a new migration in `sql/`, and two\n> `pikkuSessionlessFunc`s (`createTodo`, `listTodos` with\n> `readonly: true`) in `packages/functions/src/functions/`. Both\n> `expose: true`, so they'll be reachable via the auto-generated RPC\n> client and React Query hooks — no HTTP wiring needed. No new\n> dependencies. OK to proceed?\n\nWait for the user to confirm or redirect. They can ask for changes (\"use the\nexisting tasks table\" / \"make it a queue not http\") in normal chat — no\nschema, no JSON, no ceremony.\n\n**Non-interactive runs (auto mode, CI, batch jobs):** state intent in one\nparagraph and proceed without waiting. Surface course corrections promptly\nin the post-implementation report.\n\n## Stage 3 — Branch off\n\nAfter confirmation, ensure the working tree is clean and create a feature\nbranch off the current default branch (whatever `git branch --show-current`\nreturns at the start — `main`, `master`, `develop`, all fine):\n\n```bash\ngit status\ngit switch -c feature/<short-slug>\n```\n\nIf the working tree is dirty, **stop and ask** — never stash silently or\noverwrite uncommitted work.\n\n## Stage 4 — Implement\n\nWrite the code as a normal human contributor would. Use the project's\nexisting conventions (look at neighbour files in `srcDirectories[0]/functions/`\nand `.../wirings/` for style).\n\n### RPC is the default transport\n\n**Just write the function with `expose: true`** — that's enough to make it\ncallable. Pikku auto-generates an RPC client (and React Query hooks if the\nproject's `clientFiles.reactQueryFile` is set) from every exposed function.\nYou do **not** need an HTTP wiring for callers to reach the function.\n\nDefault flow for a feature:\n\n1. Write the function file with `expose: true` (and `readonly: true` for\n reads).\n2. Run `pikku all` — RPC map, fetch client, and React Query hooks are\n regenerated. Frontends call `useListTodos()` / `mutation.mutate(...)`\n without you wiring anything.\n\nAdd an HTTP wiring **only when** the feature genuinely needs a specific\nREST shape (third-party callers, webhooks, REST-conventional URLs). Most\nin-app features don't.\n\n### Hard rules that always apply\n\n- **`expose: true`** for any function called from a frontend or another\n service. Without it the RPC client won't generate hooks for it.\n- **`readonly: true` for queries.** Mark read functions as `readonly: true`\n on the function config. The runner uses this to enforce read-only sessions\n (a write func called under a readonly session is rejected). The RPC layer\n also uses it to pick `useQuery` (cacheable) vs `useMutation` for client\n hooks. Mutations leave `readonly` unset (or `false`).\n- **`kind` ⇔ `auth` coupling for HTTP wirings (when you have one).** If the\n function is `pikkuFunc` (session-aware), the HTTP wiring needs\n `auth: true`. `pikkuSessionlessFunc` ⇒ `auth: false`. Mismatching is a\n hard error (PKU573).\n- **HTTP method by intent (when you wire HTTP).** Reads → `GET`. Writes →\n `POST`/`PUT`/`PATCH`/`DELETE` per REST conventions.\n- **Workflows.** Prefer `pikkuWorkflowGraph` (DSL) over\n `pikkuWorkflowComplexFunc`. `mode: 'inline'` is sync; `'distributed'` is\n queue-dispatched.\n- **Auth checks belong on the function or wiring**, not in function bodies.\n Use the `permissions` field with a `pikkuPermission` factory.\n- **Throw typed errors** from `@pikku/core/errors` — `NotFoundError`,\n `ConflictError`, `BadRequestError`. Never bare `Error`.\n- **Migrations are inline SQL files** in the project's migrations dir\n (typically `sql/`). Use a numbered prefix matching existing files.\n- **Secrets and env-vars: NEVER `process.env`.** Declare them with\n `defineSecret` (sensitive) or `defineVariable` (non-sensitive) — both with a\n zod schema for type-safe access. Read variables with\n `services.variables.get('NAME')`. Secrets are **not available in functions** —\n read them in `services.ts` with `secrets.getSecret('NAME')` and pass the value\n into the service the function uses. See the **pikku-config** skill for the full\n pattern (including OAuth2 credentials). This applies even in `config.ts`.\n\n### Conventions to copy from neighbours\n\nSome patterns vary by project; **read a neighbour file before writing**:\n\n- **Function shape**: zod schemas as exported `const`s (`CreateTodoInput`,\n `CreateTodoOutput`) passed to `input`/`output` on the func config — vs\n generic-typed config. Schema name **must match codegen expectations** (the\n exported const name = the schema name in generated `.gen.json`).\n- **Imports**: usually `'#pikku'` for `pikkuFunc` / `pikkuSessionlessFunc`\n etc. Copy what neighbours do.\n- **Service usage**: e.g. `kysely`, `redis`. Look at how an existing function\n destructures services from its first arg. **Check `application-types.d.ts`**\n to see whether services like `kysely` are typed (`Kysely<DB>`) or untyped\n (`Kysely<any>`) — that drives whether you can lean on generated DB types\n or have to coerce manually.\n- **DB schema namespace**: many projects put tables under a `CREATE SCHEMA`\n (e.g. `app.todos`). Read the first migration in `sql/` to see the\n convention; reuse helper functions/triggers (e.g. `update_last_updated_at`)\n rather than redefining them.\n- **HTTP wiring style** (only relevant if you're adding one). Two common\n shapes — match what the project already uses:\n - Per-route `wireHTTP({ method, route, func, auth })`.\n - Single map: `const routes = defineHTTPRoutes({ auth: false, routes: {\nfooName: { method: 'post', route: '/foo', func: fooFunc } }}); wireHTTPRoutes(routes)`.\n\nFor shared wiring files (e.g. `todos.http.ts` holding both create and list):\ncreate the file with imports if it doesn't exist; **append** wire calls and\nadd missing imports if it does.\n\n## Stage 5 — Verify\n\nBoth must complete cleanly **for your changes** before committing:\n\n```bash\nyarn pikku all\n# Type-check the workspaces you touched:\ncd packages/functions && npx tsc --noEmit\n```\n\nNotes on running `tsc`:\n\n- A root-level `yarn tsc` may be a no-op in monorepos that don't define a\n `tsc` script in each workspace. Don't trust an exit-zero from the root if\n no actual checking happened — verify by running `npx tsc --noEmit` in the\n package(s) you touched.\n\n### What \"fails\" means\n\n**Trust the exit code, not the stderr noise.** `yarn pikku all` may print\nwarnings, `[PKUxxx]` messages, even `level: critical` log lines, while\nstill exiting `0` — those are pre-existing project state, not your\nproblem. Same for `meta context --json`: it streams logs to stderr that\nlook scary on a clean baseline. The exit code is the source of truth.\n\nIf a command exits non-zero, that's a real failure — fix or stop.\n\n### Baseline noise — only your errors matter\n\nMany real-world projects ship with pre-existing warnings or errors\n(legacy types, version drift, gen-layer messages). Those are not your\nproblem; do not \"fix\" them.\n\nTo distinguish your errors from baseline:\n\n1. **Before implementing** (Stage 4), capture the baseline:\n ```bash\n yarn pikku all 2>&1 | tee /tmp/pikku-before.log\n ```\n2. **After implementing**, compare:\n ```bash\n yarn pikku all 2>&1 | tee /tmp/pikku-after.log\n diff /tmp/pikku-before.log /tmp/pikku-after.log\n ```\n\nA clean diff means your changes introduced no new issues — even if the\nunderlying logs both show pre-existing warnings.\n\nIf something genuinely failed because of YOUR change, fix the actual issue.\n**Do not** mask errors with `as any`, `@ts-ignore`, or `--no-verify`. If\nyou're stuck, surface the failure to the user — don't hand them a broken\nbranch.\n\n## Stage 6 — Commit\n\n```bash\ngit add -A\ngit commit -m \"feat: <short title>\"\n```\n\n## Stage 7 — Hand off\n\nTell the user the branch name and how to review. Two options:\n\n- **Local review:** open the pikku console — the changes view diffs the\n current branch against `main` with pikku-aware structure (added functions,\n new wires, migrations).\n- **PR review:** ask before pushing. Once they confirm, `git push -u origin\nfeature/<slug>` and surface the PR-create URL.\n\nDo not push without explicit confirmation. Do not merge.\n\n## Hard constraints\n\nThe skill's `allowed-tools` does **not** permit:\n\n- `yarn add` / `npm install` / dependency changes (ask the user first)\n- `yarn dbmigrate` (never run migrations against the real DB during planning)\n- `pikku deploy apply` (never deploy)\n- secret writes\n- network calls beyond what the implementation requires\n\nIf the feature genuinely needs any of these, **stop and ask** with a clear\nexplanation of why and what would change.\n\n## Output discipline\n\n- Stage 2 (intent statement) is plain English, one paragraph.\n- Between stages, give one-line updates: \"Discovered 30 functions, http+queue\n in use. Drafting intent...\" → \"Branch `feature/todos` created, implementing...\"\n → \"`pikku all` clean, `tsc` clean, committed. Review via console or run\n `git diff main`.\"\n- Don't narrate file-by-file. Only surface what's interesting (new patterns,\n judgment calls, things you suppressed).\n", "pikku-gateway-slack/SKILL.md": "---\nname: pikku-gateway-slack\ndescription: >-\n Use when integrating Slack with a Pikku app. Covers SlackGatewayAdapter, slash commands, OAuth\n flow, message handling, and signature verification. TRIGGER when: code uses SlackGatewayAdapter,\n parseSlashCommand, buildSlackInstallUrl, or user asks about Slack integration, Slack bots, or\n @pikku/gateway-slack. DO NOT TRIGGER when: user asks about general gateway/webhook patterns (use\n pikku-trigger).\n---\n\n# Pikku Gateway Slack\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/gateway-slack` provides a Slack Events API gateway adapter, slash command handling, OAuth installation flow, and message utilities.\n\n## Installation\n\n```bash\nyarn add @pikku/gateway-slack @slack/web-api\n```\n\n## API Reference\n\n### `SlackGatewayAdapter`\n\n```typescript\nimport { SlackGatewayAdapter } from '@pikku/gateway-slack'\n\nconst adapter = new SlackGatewayAdapter(options: SlackGatewayAdapterOptions)\n```\n\nBridges Slack Events API webhooks with Pikku's gateway system for processing Slack events as Pikku functions.\n\n### `SlackGatewayHelper`\n\nHelper for handling Slack messages and metadata within gateway functions.\n\n### Slash Commands\n\n```typescript\nimport { parseSlashCommand, respondToSlashCommand } from '@pikku/gateway-slack'\n\nconst command = parseSlashCommand(request)\nawait respondToSlashCommand(responseUrl, { text: 'Done!' })\n```\n\n### OAuth Flow\n\n```typescript\nimport {\n buildSlackInstallUrl,\n exchangeSlackOAuthCode,\n RECOMMENDED_BOT_SCOPES,\n} from '@pikku/gateway-slack'\n\nconst installUrl = buildSlackInstallUrl({\n clientId: config.slackClientId,\n scopes: RECOMMENDED_BOT_SCOPES,\n redirectUri: config.slackRedirectUri,\n})\n\nconst tokens = await exchangeSlackOAuthCode({\n clientId: config.slackClientId,\n clientSecret: config.slackClientSecret,\n code: oauthCode,\n redirectUri: config.slackRedirectUri,\n})\n```\n\n### Signature Verification\n\n```typescript\nimport { verifySlackSignature } from '@pikku/gateway-slack'\n\nverifySlackSignature(signingSecret, timestamp, body, signature)\n```\n\n## Usage Patterns\n\n### Slack Bot Gateway\n\n```typescript\nimport { SlackGatewayAdapter } from '@pikku/gateway-slack'\n\nconst slackGateway = new SlackGatewayAdapter({\n signingSecret: config.slackSigningSecret,\n botToken: config.slackBotToken,\n})\n\n// Register with your HTTP runner to handle /slack/events endpoint\n```\n\n### Slash Command Handler\n\n```typescript\nconst handleSlashCommand = pikkuSessionlessFunc({\n title: 'Handle Slack Command',\n func: async ({ db }, data) => {\n const command = parseSlashCommand(data)\n // Process command...\n await respondToSlashCommand(command.response_url, {\n text: `Processed: ${command.text}`,\n })\n },\n})\n```\n", "pikku-http/references/http-options.md": "# wireHTTP / defineHTTPRoutes / wireHTTPRoutes — full option reference\n\n## `wireHTTP(config)`\n\nWire a single function to an HTTP endpoint. Import from `@pikku/core/http`.\n\n| Option | Type | Notes |\n| --- | --- | --- |\n| `method` | `'get' \\| 'post' \\| 'put' \\| 'patch' \\| 'delete' \\| 'head'` | HTTP verb |\n| `route` | `string` | e.g. `/books/:bookId` — `:params` become `data` fields |\n| `func` | `PikkuFunc` | The function to call |\n| `auth?` | `boolean` | Override default auth (`true` = require session) |\n| `tags?` | `string[]` | For grouping, middleware targeting |\n| `middleware?` | `PikkuMiddleware[]` | Per-route middleware |\n| `sse?` | `boolean` | Enable Server-Sent Events |\n| `contentType?` | `'xml' \\| 'json'` | Response content type |\n| `timeout?` | `number` | Request timeout in ms |\n| `headers?` | `HTTPHeadersSchema` | Expected headers schema |\n| `docs?` | `HTTPRouteDocsConfig` | OpenAPI docs config |\n\n## `defineHTTPRoutes(config)` + `wireHTTPRoutes(config)`\n\nGroup routes with shared configuration. Groups are composable and nestable. Import from `.pikku/pikku-types.gen.js`.\n\n```typescript\nconst routes = defineHTTPRoutes({\n basePath?: string, // Prepended to all route paths\n tags?: string[], // Applied to all routes in group\n auth?: boolean, // Default auth for all routes (overridable per-route)\n middleware?: PikkuMiddleware[],\n routes: {\n [key: string]: {\n method: string,\n route: string,\n func: PikkuFunc,\n auth?: boolean, // Override group auth\n middleware?: PikkuMiddleware[],\n }\n }\n})\n\nwireHTTPRoutes({\n basePath?: string, // Top-level prefix (e.g. '/api/v1')\n middleware?: PikkuMiddleware[],\n routes: {\n [key: string]: ReturnType<typeof defineHTTPRoutes>,\n }\n})\n```\n\nConfig cascading rules:\n\n- `basePath` — concatenates down the chain\n- `tags` — merge (union)\n- `auth` — child overrides parent\n", "pikku-http/SKILL.md": "---\nname: pikku-http\ndescription: >-\n Use when adding HTTP routes, REST APIs, web endpoints, or SSE streams to a Pikku app. Covers\n wireHTTP, defineHTTPRoutes, route groups, auth, middleware, SSE, and generated\n fetch client. TRIGGER when: code uses wireHTTP/defineHTTPRoutes/wireHTTPRoutes, user asks about\n REST endpoints, API routes, SSE, or the generated fetch client. DO NOT TRIGGER when: user asks\n about WebSocket (use pikku-websocket), queue workers (use pikku-queue), or deployment (use\n pikku-deploy-*).\ninstallGroups: [core]\n---\n\n# Pikku HTTP Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions to HTTP endpoints. Supports single routes, composable route groups, auth, middleware, SSE, and auto-generated type-safe clients. (Authorization lives on the function, not the wiring — see `pikku-permissions`.)\n\n## Before You Start\n\nRun these commands to understand the current project:\n\n```bash\npikku info functions --verbose # See existing functions, their types, tags, middleware\npikku info tags --verbose # Understand project organization and naming conventions\npikku info middleware --verbose # See what middleware is already applied\n```\n\nFollow existing patterns you find (naming, tag usage, file organization). See `pikku-concepts` for the core mental model.\n\n## API Reference\n\n- `wireHTTP(config)` (from `@pikku/core/http`) — wire one function to one endpoint.\n- `defineHTTPRoutes(config)` + `wireHTTPRoutes(config)` (from `.pikku/pikku-types.gen.js`) — group routes with shared config; composable/nestable.\n\nFunction input/output types come from the function's own `input:`/`output:` zod schemas — never declared in the wiring. Route `:params`, query params, and body are merged into the function's `data` arg (see Data Flow).\n\nConfig cascading across groups: `basePath` concatenates down the chain, `tags` merge (union), `auth` child overrides parent.\n\nFor the full option tables (every `wireHTTP` field, the `defineHTTPRoutes`/`wireHTTPRoutes` config shape), read `references/http-options.md`.\n\n### `addHTTPMiddleware(pattern, middlewares)`\n\n```typescript\naddHTTPMiddleware('*', [authBearer()]) // All routes\naddHTTPMiddleware('/api/*', [rateLimit()]) // Pattern match\n```\n\n> HTTP-route-level permissions (`addHTTPPermission`, a `permissions` field on the wiring) were removed in #972. Declare authorization on the function definition (`pikkuFunc({ permissions })`, see `pikku-permissions`), or app-wide via `addGlobalPermission`. Tags/patterns are for *middleware* only now.\n\n## Data Flow\n\nPikku merges route params, query params, and request body into a single `data` object:\n\n```typescript\n// POST /books/42?format=pdf with body { title: \"New Title\" }\nwireHTTP({ method: 'post', route: '/books/:bookId', func: updateBook })\n// → updateBook receives: { bookId: \"42\", format: \"pdf\", title: \"New Title\" }\n```\n\n## Usage Patterns\n\n### Single Route\n\n```typescript\nwireHTTP({\n method: 'get',\n route: '/books/:bookId',\n func: getBook,\n})\n```\n\n### Route Groups (Recommended for CRUD)\n\n```typescript\nconst booksRoutes = defineHTTPRoutes({\n tags: ['books'],\n routes: {\n list: { method: 'get', route: '/books', func: listBooks, auth: false }, // per-route override\n get: { method: 'get', route: '/books/:bookId', func: getBook },\n create: { method: 'post', route: '/books', func: createBook },\n delete: { method: 'delete', route: '/books/:bookId', func: deleteBook },\n },\n})\n\nconst todosRoutes = defineHTTPRoutes({\n auth: false, // group-level default, overridable per-route\n tags: ['todos'],\n routes: {\n list: { method: 'get', route: '/todos', func: listTodos },\n },\n})\n\nwireHTTPRoutes({\n basePath: '/api/v1',\n middleware: [cors()],\n routes: { books: booksRoutes, todos: todosRoutes },\n})\n// Results in: GET /api/v1/books, POST /api/v1/books, GET /api/v1/todos, etc.\n```\n\n### Auth\n\n```typescript\n// Public route (no auth)\nwireHTTP({ method: 'get', route: '/books', func: listBooks, auth: false })\n\n// Authenticated route (default when a global auth middleware is set)\nwireHTTP({ method: 'delete', route: '/books/:bookId', func: deleteBook })\n```\n\nAuthorization is not a wiring concern — declare it on the function via `permissions` (see `pikku-permissions`), or app-wide via `addGlobalPermission`.\n\n### Middleware\n\n```typescript\nimport { cors, authBearer } from '@pikku/core/middleware'\n\n// Global middleware\naddHTTPMiddleware('*', [\n cors({ origin: 'https://app.example.com', credentials: true }),\n authBearer(),\n])\n\n// Scoped middleware\naddHTTPMiddleware('/api/*', [rateLimit({ maxRequests: 100, windowMs: 60_000 })])\n\n// Per-route middleware\nwireHTTP({\n method: 'delete',\n route: '/books/:bookId',\n func: deleteBook,\n middleware: [auditLog],\n})\n```\n\n### SSE (Server-Sent Events)\n\n```typescript\nwireHTTP({\n method: 'get',\n route: '/todos',\n func: getTodos,\n sse: true,\n})\n\nconst getTodos = pikkuFunc({\n title: 'Get Todos',\n func: async ({ db, channel }, {}) => {\n const todos = await db.getTodos()\n\n if (channel) {\n for (const todo of todos) {\n channel.send({ todo })\n await sleep(100)\n }\n return\n }\n\n return { todos }\n },\n})\n```\n\n### Generated Fetch Client\n\nAfter `npx pikku all`, a type-safe client is generated:\n\n```typescript\nimport { pikkuFetch } from '.pikku/pikku-fetch.gen.js'\n\npikkuFetch.setServerUrl('http://localhost:4002')\n\nconst books = await pikkuFetch.get('/api/v1/books', {})\nconst book = await pikkuFetch.get('/api/v1/books/:bookId', { bookId: '42' })\nconst created = await pikkuFetch.post('/api/v1/books', {\n title: 'The Pikku Guide',\n author: 'You',\n})\n\npikkuFetch.setAuthorizationJWT(token)\nconst deleted = await pikkuFetch.delete('/api/v1/books/:bookId', {\n bookId: created.bookId,\n})\n```\n\n## Complete Example\n\nFunctions live in their own files (one per file) and supply behavior + `permissions`; the wiring file imports them and wires routes. Sessionless funcs need no session; `pikkuFunc` does.\n\n```typescript\n// functions/books.functions.ts\nimport { pikkuFunc, pikkuSessionlessFunc } from '#pikku'\n\nexport const listBooks = pikkuSessionlessFunc({\n title: 'List Books',\n func: async ({ db }, { limit }) => ({ books: await db.listBooks(limit) }),\n})\n\nexport const getBook = pikkuFunc({\n title: 'Get Book',\n description: 'Retrieve a book by ID',\n func: async ({ db }, { bookId }) => await db.getBook(bookId),\n permissions: { user: isAuthenticated },\n})\n\n// wirings/books.http.ts — same defineHTTPRoutes/wireHTTPRoutes shape as the Route Groups example above\nimport { addHTTPMiddleware } from '@pikku/core/http'\nimport { cors, authBearer } from '@pikku/core/middleware'\n\naddHTTPMiddleware('*', [cors(), authBearer()])\n```\n", "pikku-i18n/SKILL.md": "---\nname: pikku-i18n\ndescription: 'Wire i18n into a Pikku frontend with Paraglide JS (inlang). English by default, every user-facing string is a typed message function (`m.some__key()`) compiled from `messages/<locale>.json`, and additional languages are served under `/fr` `/de` URL prefixes. TRIGGER when: scaffolding or editing a frontend and writing user-facing text, adding a second language, or asked to \"make this translatable / use tokens / add i18n\". DO NOT TRIGGER for backend functions, error messages thrown from functions, or log output.'\ninstallGroups: [core]\n---\n\n# Pikku i18n (Paraglide JS)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Every user-facing string in a frontend is a message. Never hardcode display text — add a key to `messages/en.json` and render `m.the__key()`. This holds even when the app ships only English; the messages are the seam a second language slots into later.\n2. One `messages/<locale>.json` per language at the app root (NOT under `src/`), declared in `project.inlang/settings.json`. English (`en`) is `baseLocale` and the only locale until someone adds another.\n3. Messages compile to typed ESM functions in `src/paraglide/` (generated, self-gitignored — never edit or commit it). The Vite plugin compiles during `dev`/`build` with HMR on message edits; run the CLI compile only when you need `tsc` before Vite has ever run.\n4. Validate with the app's own `tsc` then its `build`. The deploy pipeline compiles Paraglide and runs each frontend's `tsc` before building it — an i18n mistake blocks the deploy.\n\n## The moving parts (starter-template layout)\n\n- `messages/en.json` — flat keys, `{param}` interpolation, inlang message-format:\n ```json\n {\n \"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n \"auth__login__title\": \"Sign in\",\n \"auth__login__description\": \"Welcome back to {name}.\"\n }\n ```\n Key convention: lower snake_case, `__` (double underscore) between namespace segments, `_` within a segment — `auth__login__title`, `common__email_placeholder`.\n- `project.inlang/settings.json` — `baseLocale`, `locales`, the `@inlang/plugin-message-format` module, `pathPattern: \"./messages/{locale}.json\"`.\n- `vite.config.ts` — `paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide' })` from `@inlang/paraglide-js` (devDependency), FIRST in the plugins array.\n- `src/paraglide/` — compiled output (`messages.js`, `runtime.js`, per-locale `messages/*.js`). Generated; it writes its own `.gitignore`.\n- `src/i18n/config.ts` — locale plumbing, and the ONLY hand-written i18n module: `supportedLocales`/`defaultLocale` (re-exported from `../paraglide/runtime.js`), `detectLocale`, `localeDir` (RTL for ar/he/fa/ur), a reactive locale store (`overwriteGetLocale` bridged to `useSyncExternalStore`), `setActiveLocale`, `useLocale()`. This is not a wrapper over messages — Paraglide's `getLocale()` is a module global with no React reactivity, and this bridges it. Wire `overwriteGetLocale` or `m.*()` will resolve a different locale than the app thinks is active.\n- `tsconfig.json` — `\"allowJs\": true, \"checkJs\": false` so `tsc` can consume Paraglide's JSDoc-typed JS output.\n\n## Using messages in components\n\n```tsx\nimport { m } from '../paraglide/messages.js'\nimport { useLocale } from '@/i18n/config'\n\nfunction LoginPage() {\n useLocale() // subscribe: re-render m.*() when the locale switches\n return (\n <>\n <Title>{m.auth__login__title()}</Title>\n <Text>{m.auth__login__description({ name: m.app__name() })}</Text>\n </>\n )\n}\n```\n\n- Params: `{name}` in the JSON → `m.auth__login__description({ name })`. Params are typed per message.\n- Any component that renders `m.*()` calls `useLocale()` (bare call is enough); it also returns `{ locale, dir, setLocale }` for switchers.\n- Non-component helpers (formatters, status maps) call `m.some__key()` directly — the functions are plain ESM, no hook needed; the render-time subscription lives in the component that displays the result.\n- Locale switching: the root route persists to localStorage, sets `<html lang dir>` (`localeDir`), and calls `setActiveLocale` — in-SPA re-render, no page reload. Mirror `routes/__root.tsx` in the starter template.\n\n## Keys only known at runtime (enum labels, status maps)\n\nA DB value picking a label (`enums__document_status__${status}`) is the one case a\ngenerated message can't express. Paraglide's README (§ \"What about dynamic or\nCMS-driven keys?\") is explicit: use an **explicit mapping from value to message\nfunction**. Key it on the enum type, never `string`:\n\n```ts\nimport { m } from '../paraglide/messages.js'\n\nconst DOCUMENT_STATUS_LABEL: Record<DocumentStatus, () => string> = {\n completed: m.enums__document_status__completed,\n in_progress: m.enums__document_status__in_progress,\n required: m.enums__document_status__required,\n}\n\n// call site — no fallback, because there is no missing case\nDOCUMENT_STATUS_LABEL[status]()\n```\n\n`Record<DocumentStatus, …>` is exhaustive: add a value to the enum without a\nlabel and the build fails. That is the entire point.\n\nDo NOT write `Record<string, () => string>` with a `?? status` fallback, and do\nNOT index the namespace with a computed key (`m[\\`enums__${name}__${value}\\`]`).\nBoth compile, both render the raw identifier to users when a label is missing,\nand both reintroduce exactly the silent-fallback failure Paraglide exists to\neliminate. If you find yourself writing a `resolveDynamicKey(key: string)`\nhelper, stop — that helper IS the bug.\n\n## Type safety — and why deploys block on i18n\n\nA message IS a function: a typo'd or deleted key (`m.auth__login__titel()`) is a missing export — a **TypeScript error**, not a silent runtime fallback string. Params are typed too. The deploy pipeline compiles Paraglide then runs each frontend's `tsc` (`\"tsc\": \"tsc --noEmit\"` script — keep it in every frontend's `package.json`) **before** building; a type error aborts the deploy. `vite build` does not type-check on its own, so this gate is the only thing standing between a broken message and production.\n\nThe gate catches _invalid_ messages but not _inlined_ strings. The `@pikku/mantine` `I18nNode` prop typing catches those: a raw string literal fails to compile on a gated prop, because `I18nString` is a branded type a bare `string` can't satisfy. Between the two, `tsc` is the whole safety net — there is no runtime fallback to inspect, by design.\n\n## Compile step\n\n- **Dev/build:** the Vite plugin compiles automatically; editing `messages/*.json` under a running dev server recompiles + HMRs.\n- **Standalone `tsc` before Vite has run** (fresh clone, CI):\n ```sh\n npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/paraglide\n ```\n This is exactly what the deploy CI does before the per-app `tsc`.\n\n## Adding a second language\n\n1. `messages/fr.json` mirroring `en.json`'s keys (translate the values, keep `{param}` names identical).\n2. Add `\"fr\"` to `locales` in `project.inlang/settings.json`.\n3. Recompile (restart/`vite dev` or the CLI compile). A locale file missing keys falls back to the base locale per message.\n4. Content is reachable via the `/<lang>` URL prefix (`detectLocale` already resolves it); the base locale needs no prefix. Expose the switcher via `useLocale().setLocale`.\n\n## i18n debug mode (find inlined strings)\n\n`tsc` catches invalid messages, and the `@pikku/mantine` gate catches raw strings on gated props — but neither sees a hardcoded string in plain JSX, an `aria-label`, `alt`, `document.title`, or anything passed to a non-Mantine component. Debug mode covers that gap: render every message as block glyphs (`█`), and whatever is still readable never went through a message.\n\n**Build it as a generated locale, never as a runtime wrapper.** Masked text is text, and rendering different text per locale is what Paraglide already does:\n\n1. A script generates `messages/zz.json` from `en.json`, replacing `\\S` with `█` while leaving `{placeholders}` intact (they are message inputs — mangling them changes the compiled signature). Run it before `paraglide-js compile`; gitignore the output.\n2. Add `\"zz\"` to `locales` in `project.inlang/settings.json`.\n3. Switch to it in the locale bridge:\n ```ts\n overwriteGetLocale(() => (isI18nDebug() ? 'zz' : activeLocale))\n ```\n\nKeep `zz` out of the app's own `supportedLocales` — that drives URL prefixes, hreflang and any backend `locale` param, none of which should see it.\n\nGenerate the catalogue in dev only. With `messages/zz.json` absent, Paraglide compiles `zz` to an alias of the base locale (`const zz_x = en_x` — one line per message, no duplicated strings), so a production bundle carries the locale at effectively zero cost.\n\nBoth the generator and the store bridge are being upstreamed (pikkujs/pikku#1036, #1035).\n\nThe wrapper alternative — a module that walks the namespace and pipes each message through a `mask()` — is what this replaces. It defeats tree-shaking (touching every export), adds a check on every call, and forces every component to import `m` from the wrapper instead of Paraglide.\n\n## What NOT to do\n\n- Don't hardcode display strings \"just for now\" — the message is the work.\n- Don't edit or commit anything under `src/paraglide/` — it's regenerated; change `messages/*.json` instead.\n- **Don't wrap `m`.** No re-export module, no branding layer, no resolver. Components import `m` from `../paraglide/messages.js` and call it. `@pikku/react`'s `I18nString` is declared as `string & { readonly __brand: 'LocalizedString' }` — deliberately identical to Paraglide's own `LocalizedString` — so `m.some__key()` satisfies the `@pikku/mantine` `I18nNode` gate natively. A wrapper adds nothing and costs per-message tree-shaking.\n- Don't re-resolve messages by string key or re-implement `{param}` interpolation. A key-string resolver turns a missing key back into silent runtime text, surrendering the type safety that is the entire reason to use Paraglide.\n- Don't reach for i18next/react-i18next or a runtime-fetch translation loader — Paraglide's compiled functions are the whole delivery mechanism.\n- Don't tokenize backend error messages or logs here — those are not frontend display strings.\n", "pikku-info/SKILL.md": "---\nname: pikku-info\ndescription: >-\n Discover what exists in a Pikku project — functions, tags, middleware, permissions, HTTP routes,\n channels, schedulers, queues, and more. Use when you need to understand the project structure,\n find existing functions, or check what middleware and permissions are defined. TRIGGER when:\n user asks \"what functions exist?\", \"show me the project structure\", \"list\n routes/middleware/permissions\", or needs to understand an existing Pikku codebase. DO NOT\n TRIGGER when: user is writing new code (use the specific wiring skill) or asking about Pikku\n concepts (use pikku-concepts).\ninstallGroups: [core]\nallowed-tools: Bash(yarn pikku info *)\nargument-hint: '[functions|tags|middleware|permissions] [--verbose] [--limit N]'\n---\n\n# Pikku Project Discovery\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nUse the `pikku info` CLI commands to inspect this Pikku project. Run the commands below and present the results to the user in a clear summary.\n\n## Available Commands\n\nAlways use `--silent` to suppress the banner and inspector logs.\n\n### Functions\n\nList all registered pikku functions:\n\n```bash\nyarn pikku info functions --silent\n```\n\nFor full details including transport type (http/channel/scheduler/queue/workflow/mcp/cli/trigger), middleware, permissions, and source file:\n\n```bash\nyarn pikku info functions --verbose --silent\n```\n\n### Tags\n\nList all tags with counts of associated functions and middleware:\n\n```bash\nyarn pikku info tags --silent\n```\n\nFor full names instead of counts:\n\n```bash\nyarn pikku info tags --verbose --silent\n```\n\n### Middleware\n\nList all middleware definitions:\n\n```bash\nyarn pikku info middleware --silent\n```\n\nFor full details including source file, required services, and description:\n\n```bash\nyarn pikku info middleware --verbose --silent\n```\n\n### Permissions\n\nList all permission definitions:\n\n```bash\nyarn pikku info permissions --silent\n```\n\nFor full details including source file, required services, and description:\n\n```bash\nyarn pikku info permissions --verbose --silent\n```\n\n## Instructions\n\n1. If the user specifies a subcommand (e.g., `/pikku-info functions`), run only that command.\n2. If no subcommand is specified, run all four commands to give a complete project overview.\n3. Always use `--silent` to suppress the Pikku banner and inspector logs.\n4. Use `--verbose` when the user asks for details, file paths, or \"more info\".\n5. Use `--limit N` to control output size (default is 50 rows).\n6. After running the commands, summarize the findings concisely:\n - Total count of functions, tags, middleware, and permissions\n - Notable patterns (e.g., which transport types are in use, which tags group the most functions)\n - Any functions without tags or transport types (potential issues)\n", "pikku-jose/SKILL.md": "---\nname: pikku-jose\ndescription: >-\n Use when setting up JWT authentication with the jose library in a Pikku app. Covers\n JoseJWTService constructor, secret rotation, token encoding/decoding/verification. TRIGGER when:\n code uses JoseJWTService, user asks about JWT setup, token signing, token verification, or\n @pikku/jose. DO NOT TRIGGER when: user asks about session middleware (use pikku-security) or\n general service setup (use pikku-services).\n---\n\n# Pikku Jose (JWT Service)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/jose` provides JWT signing, verification, and decoding using the [jose](https://github.com/panva/jose) library. Implements the `JWTService` interface from `@pikku/core`.\n\n## Installation\n\n```bash\nyarn add @pikku/jose\n```\n\n## API Reference\n\n### `JoseJWTService`\n\n```typescript\nimport { JoseJWTService } from '@pikku/jose'\n\nconst jwt = new JoseJWTService(\n getSecrets: () => Promise<Array<{ id: string; value: string }>>,\n logger?: Logger\n)\n\nawait jwt.init()\n```\n\n**Constructor Parameters:**\n\n- `getSecrets` — Async function returning an array of `{ id, value }` key pairs. First key is used for signing; all keys are tried for verification (supports rotation).\n- `logger` — Optional logger instance.\n\n**Methods:**\n\n- `init(): Promise<void>` — Fetch and cache secrets. Call at startup.\n- `encode<T>(expiresIn: RelativeTimeInput, payload: T): Promise<string>` — Create a signed JWT.\n- `decode<T>(token: string): Promise<T>` — Decode a JWT payload without verification.\n- `verify(token: string): Promise<void>` — Verify a JWT signature and expiry.\n\n## Usage Patterns\n\n### Basic Setup\n\n```typescript\nimport { JoseJWTService } from '@pikku/jose'\n\nconst jwt = new JoseJWTService(\n async () => [{ id: 'key-1', value: process.env.JWT_SECRET! }],\n logger\n)\nawait jwt.init()\n```\n\n### Secret Rotation\n\nSupply multiple keys. The first is used for signing; all are tried for verification:\n\n```typescript\nconst jwt = new JoseJWTService(async () => [\n { id: 'key-2', value: NEW_SECRET }, // signs with this\n { id: 'key-1', value: OLD_SECRET }, // still verifies tokens signed with this\n])\n```\n\n### With Pikku Services\n\n```typescript\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new ConsoleLogger()\n const jwt = new JoseJWTService(\n async () => [{ id: 'my-key', value: config.jwtSecret }],\n logger\n )\n await jwt.init()\n return { config, logger, jwt }\n})\n```\n\n### Encoding & Verifying Tokens\n\n```typescript\nconst token = await jwt.encode('1h', { userId: 'abc', role: 'admin' })\n\nawait jwt.verify(token) // throws if invalid/expired\n\nconst payload = await jwt.decode<{ userId: string; role: string }>(token)\n```\n", "pikku-knowledge/SKILL.md": "---\nname: pikku-knowledge\ndescription: >-\n Use when writing, reading, reorganising or validating a project's knowledge/ directory — the\n notes that say what the app is, in the language its users use. Covers the Open Knowledge Format\n note (path-as-identity markdown, YAML frontmatter, only `type` required), the sections of the\n app-project profile (slices, entities, decisions, questions, wishlist) and the one question each\n answers, slice status/entities/gherkin rules, the `resource:` URI scheme that ties a note to the\n code it is about, the shapes that are NOT a knowledge base, and the `pikku knowledge\n validate|index` commands. TRIGGER when: user asks to write down a decision, a requirement, an\n entity or an open question; asks what the app does or is; asks about knowledge/, notes, slices,\n or an index.md; or hands over a product brief to record. DO NOT TRIGGER when: user asks what\n functions, routes, tables or permissions exist (that is `pikku meta` / `pikku info`, never a\n note), or asks to write a scenario test (use pikku-scenario).\ninstallGroups: [core]\n---\n\n# Pikku Knowledge\n\nThe knowledge base is `knowledge/` at the repo root: markdown notes about **what the app is**, written for whoever picks the project up next — human or agent.\n\nNot to be confused with `.knowledge/` — the dot-prefixed JSON blueprint that `pikku-software-archaeology` extracts from a legacy repo. Different directory, different format, different purpose.\n\n## Agent Operating Procedure\n\n1. **Read `knowledge/index.md` first**, then the section index for whatever you are about to touch. It is the cheapest way to learn what the app already claims about itself.\n2. Before writing a note, ask whether `pikku meta` already answers it. If it does, do not write the note — see _What never goes in a note_.\n3. Write the note in the section that answers its question. Create the section's `index.md` in the same turn you create the section.\n4. Add a `resource:` only if you can name a real id. A wrong one is worse than none.\n5. Run `pikku knowledge validate`. Fix what it reports.\n6. Run `pikku knowledge index` so each section lists what is actually in it.\n\n## The governing rule\n\n**Record only what pikku cannot tell you.**\n\nPikku already knows every function, route, schema, table, column, queue, cron, channel and permission — `pikku meta` prints them, and the generated meta is the truth. A note that lists tables or routes is a copy that starts drifting the moment somebody edits the code, and it drifts _while looking authoritative_, which is worse than silence.\n\nWhat a note is for is the part no generator can derive: what a thing means, why a rule was chosen, what it rules out, who asked for it, and what is still unanswered.\n\n## The note\n\nA note is a markdown file whose **path is its identity** — moving it renames it. It carries YAML frontmatter and a body:\n\n```markdown\n---\ntype: decision\ntitle: Revocation ends a grant\ndescription: A revoked grant stops working immediately, everywhere.\nresource: func:revokeGrant, table:grant\ntags: [sharing, access]\n---\n\n# Revocation ends a grant\n\nWhen an owner revokes a grant, the person loses access on their next request — no\ngrace period and no scheduled cleanup.\n\nThis rules out a \"revoked but valid until midnight\" state, which we considered\nfor shared days and rejected: two people disagreeing about who can see today is\nworse than one of them losing access mid-session.\n```\n\nFrontmatter fields:\n\n| Field | Meaning |\n| ------------- | ----------------------------------------------------------------------------------------------------------------------- |\n| `type` | **The only required field.** `slice`, `entity`, `decision`, `note`, `overview`. Lowercase — gates compare it literally. |\n| `title` | What to call the note in a listing. Falls back to the first heading, then the filename. |\n| `description` | One line, used as the note's subtitle in a section index. |\n| `resource` | Comma-separated `<kind>:<id>` URIs — the code this note is about. See below. |\n| `tags` | Flow list (`[a, b]`) or a `- item` block; both are read. |\n| `timestamp` | When it was written, if it matters. |\n\n`index.md` and `log.md` are **reserved**: an `index.md` maps a directory, a `log.md` is an append-only record. Neither is ever listed as a note by an index.\n\nPlain markdown links between notes — `[revocation](../decisions/revocation-ends-a-grant.md)` — are what make the base a graph. A link to a note that does not exist yet is legal: it marks something worth writing, not an error.\n\n## The layout\n\n```\nknowledge/\n index.md # type: overview — the map\n slices/\n index.md\n 01-the-daily-entry.md # type: slice\n entities/\n index.md\n entry.md # type: entity\n decisions/\n index.md\n revocation-ends-a-grant.md # type: decision\n security/\n index.md\n one-account-one-person.md\n questions/\n index.md\n who-owns-a-shared-day.md # type: note\n wishlist/\n index.md\n export-to-a-calendar.md # type: note\n```\n\nEach section answers exactly one question, which is what lets a reader find a note without an index of indexes:\n\n| Section | The question it answers |\n| --------------------- | ------------------------------------------------------------------ |\n| `slices/` | What is one buildable piece of this app, and what proves it works? |\n| `entities/` | What is this thing, in the words users use for it? |\n| `decisions/` | What was chosen, and what does that rule out? |\n| `decisions/security/` | Who may do what? |\n| `questions/` | What has been asked and not yet answered? |\n| `wishlist/` | What does somebody want that nobody has asked to be built? |\n\n**Create a section the turn you have a note for it** — never a scaffold of empty directories, and never a section without its own `index.md`. A section index says in one line what belongs in it; that sentence is the reason the file exists, so `pikku knowledge index` writes only the note listing and leaves your prose alone.\n\n## Slices\n\nA slice is the one note type that is a piece of _work_ rather than a fact, so it alone carries state and size:\n\n````markdown\n---\ntype: slice\ntitle: The daily entry\ndescription: An owner writes one entry per day, and sees it on the day.\nstatus: proposed\nentities: entry, day\nresource: func:createEntry\n---\n\n# The daily entry\n\nAn owner writes at most one entry per day. Writing again replaces it.\n\n```gherkin\nGiven 'owner' has no entry for today\nWhen 'owner' writes one\nThen it appears on today's day\nAnd writing again replaces it rather than adding a second\n```\n````\n\n- **`status`** is `proposed` → `dispatched` → `built`. Nothing else. Every gate compares it literally.\n- **`entities`** lists what the slice touches, **at most three**. Past three it is not one buildable piece — split it.\n- **The scenario is a fenced `gherkin` block, in the third person.** `Given 'owner' has no entry` — never `Given I have no entry`. A quoted word _means a persona_, which is what lets a reader (and a test) tell who is acting. First person hides that, so it is rejected.\n\n## `resource:` — tying a note to the code\n\n`resource:` names the code a note is about, as one or more `<kind>:<id>` URIs, comma-separated.\n\n**Every kind resolves.** That is the whole design: a kind that cannot be checked lets notes accumulate references nothing validates, and the graph rots into fiction exactly where it looks most authoritative.\n\n| Kind | An id is | Where it resolves |\n| ----------- | -------------------------------------------------- | --------------------------------------------------------------------------------- |\n| `func:` | a function id | generated function meta |\n| `workflow:` | a workflow name | generated workflow meta |\n| `schema:` | a schema name | generated schemas |\n| `http:` | a route, `method:route`, or the function behind it | generated http wirings |\n| `queue:` | a queue name | generated queue wirings |\n| `cron:` | a scheduled task name | generated scheduler wirings |\n| `channel:` | a channel name | generated channel meta |\n| `table:` | a table name | the generated db schema |\n| `addon:` | `@pikku/addon-x` or bare `x` | the manifests that declare the dependency |\n| `scope:` | a scope name | the `scopes:` a function gates itself with, plus the scopes a `defineSystemRole()` confers |\n| `persona:` | a persona name | `definePersonas()` |\n\nIds are case-sensitive: `createEntry` is not `createentry`.\n\nThe check **fails closed on drift and open on ignorance**. An id missing from a kind that resolved is an error — the code was renamed or deleted under the note. A kind with no generated meta at all is skipped, so a project without queues is never told its queue references are broken.\n\nThere is no kind for a service, a middleware or a component. Say it in prose instead.\n\n## What never goes in a note\n\nThese are all things that exist somewhere better, so a note is always the copy that drifts:\n\n| Do not write | Because it lives in |\n| ----------------------------------- | ---------------------------------------------------------- |\n| a `personas/` section | `definePersonas()` in the project's own code |\n| a `scenarios/` section | the gherkin block inside the slice it belongs to |\n| a `permissions/` section | a decision note under `decisions/security/` |\n| a list of tables, columns or routes | `pikku meta` — the generated schema _is_ the schema |\n| a changelog | `CHANGELOG.md` at the repo root |\n| **secrets or credentials** | a secrets service. Never here — `knowledge/` is committed. |\n\nAnd two shapes that look like a knowledge base but are not:\n\n- **A flat `product.md` / `glossary.md` / `technology.md` at the root of `knowledge/`.** That is one long document: nothing can link into part of it, and no gate can read it. Split it into notes in the sections that answer its questions.\n- **A directory tree with no notes in it.** Sections exist because there is something to put in them.\n\n## The commands\n\n```bash\npikku knowledge validate # check the base against this profile\npikku knowledge index # refresh every index.md\npikku knowledge index --check # report stale indexes without writing (CI gate)\n```\n\n`validate` reports: notes with no `type`, a missing `knowledge/index.md`, a section with no `index.md`, notes flat at the root, sections that duplicate what the project already declares, slices with a bad or missing `status`, slices over three entities, slices with no gherkin block or a first-person one, and every `resource:` that no longer resolves. Errors fail the command; warnings do not.\n\n`index` rewrites only the block between `<!-- pikku:knowledge-index -->` markers, creating a scaffolded `index.md` for a section that has none. It is idempotent — running it twice changes nothing.\n\n## Profiles built on this one\n\nOKF permits frontmatter fields a reader does not know, and the parser ignores them rather than failing. That is the extension point: a tool layered on Pikku can add its own sections and fields on top of everything above without forking the format.\n\nFabric is the one that exists. It adds `decisions/design/` — rules about how the app looks and behaves — and a `design:` field on a slice pointing at the design options it was built from. Both are Fabric's to validate; `pikku knowledge validate` passes them through untouched. Everything else in this skill is the same in both.\n", "pikku-kysely/SKILL.md": "---\nname: pikku-kysely\ndescription: >-\n Use when WRITING KYSELY QUERIES (select/join/aggregate/insert/update/delete) inside a Pikku\n function body, or when setting up SQL database services with Kysely. Covers the query builder\n API (joins, aggregates + groupBy/having, returning, sql template, expression builder, $if,\n transactions, jsonArrayFrom relation helpers) AND @pikku/kysely service setup (channel stores,\n workflow services, secret services, AI storage, deployment services). TRIGGER when: writing any\n non-trivial kysely query (a join, an aggregate/count/sum, groupBy, subquery, transaction, or\n conditional query), the injected `kysely` service is used in a function body, or code uses\n PikkuKysely, KyselyChannelStore, KyselyWorkflowService, KyselySecretService, or the user asks\n about SQL setup with Pikku. DO NOT TRIGGER when: user asks about MongoDB (use pikku-mongodb) or\n Redis (use pikku-redis).\ninstallGroups: [core]\n---\n\n# Pikku Kysely (SQL Database Services)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n## Writing Queries — the Kysely query builder\n\nIn a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly. Pikku wires the **CamelCasePlugin**, so you write **camelCase everywhere in TS** (columns, aliases) and raw **snake_case ONLY inside a `` sql`` `` literal**. Kysely is a query builder, NOT an ORM — there are no relations; shape nested data with the JSON helpers below. Never hand-roll SQL strings; never annotate the return type (in Pikku the output zod schema IS the type).\n\n```typescript\nimport { sql } from 'kysely'\n// Relation helpers are ENGINE-SPECIFIC — import the matching path:\nimport { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/sqlite' // SQLite / libSQL\n// import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres' // Postgres\n```\n\n```typescript\n// SELECT + where/orderBy/limit/offset. Terminals: .execute() | .executeTakeFirst()\n// | .executeTakeFirstOrThrow(() => new NotFoundError()) — pass an error factory.\nconst rows = await kysely.selectFrom('item')\n .select(['id', 'name', 'quantity'])\n .where('warehouseId', '=', warehouseId)\n .orderBy('name').limit(50).execute()\n\n// JOINS + aliased selects (qualify columns once a join exists)\nawait kysely.selectFrom('stock')\n .innerJoin('item', 'item.id', 'stock.itemId')\n .leftJoin('bin as b', 'b.id', 'stock.binId')\n .select(['stock.id', 'item.name as itemName', 'b.code as binCode'])\n .execute()\n\n// AGGREGATES via the fn helper + groupBy/having. eb.fn.count returns string|number —\n// cast if you need a JS number (SQLite: CAST(... AS INTEGER)).\nawait kysely.selectFrom('stock')\n .select((eb) => ['itemId', eb.fn.sum<number>('quantity').as('onHand')])\n .groupBy('itemId')\n .having((eb) => eb.fn.sum('quantity'), '<', 10) // low-stock\n .execute()\n\n// INSERT + RETURNING (one round-trip; works on SQLite & Postgres)\nconst created = await kysely.insertInto('item')\n .values({ name: input.name, warehouseId })\n .returning(['id', 'name']).executeTakeFirstOrThrow()\n\n// UPDATE + RETURNING, DELETE\nawait kysely.updateTable('item').set({ quantity: input.quantity })\n .where('id', '=', input.id).returning(['id', 'quantity']).executeTakeFirstOrThrow()\nawait kysely.deleteFrom('item').where('id', '=', input.id).execute()\n\n// EXPRESSION BUILDER for and/or; $if for conditional building; sql for raw fragments\nawait kysely.selectFrom('item')\n .selectAll()\n .where((eb) => eb.or([eb('quantity', '=', 0), eb('discontinued', '=', true)]))\n .$if(!!input.search, (qb) => qb.where('name', 'like', `%${input.search}%`))\n .select(sql<number>`quantity * unit_cost`.as('value')) // snake_case ok inside sql``\n .execute()\n\n// NESTED DATA (no relations) — jsonObjectFrom (one) / jsonArrayFrom (many)\nawait kysely.selectFrom('warehouse')\n .select((eb) => ['warehouse.id', 'warehouse.name',\n jsonArrayFrom(eb.selectFrom('bin').select(['bin.id', 'bin.code'])\n .whereRef('bin.warehouseId', '=', 'warehouse.id')).as('bins')])\n .execute()\n\n// TRANSACTION — multi-write atomicity. Use trx (not kysely) inside.\nawait kysely.transaction().execute(async (trx) => {\n await trx.updateTable('stock').set({ quantity: 0 }).where('itemId', '=', id).execute()\n await trx.insertInto('stockMove').values({ itemId: id, delta: -qty }).execute()\n})\n```\n\nPikku provides SQL database services through four packages:\n\n- `@pikku/kysely` — Base service implementations (database-agnostic)\n- `@pikku/kysely-postgres` — PostgreSQL-specific implementations + `PikkuKysely` connection wrapper\n- `@pikku/kysely-mysql` — MySQL-specific implementations\n- `@pikku/kysely-sqlite` — SQLite-specific implementations + `createSQLiteKysely` factory\n\nAll implement standard Pikku interfaces from `@pikku/core`.\n\n## Installation\n\n```bash\n# Pick your database\nyarn add @pikku/kysely @pikku/kysely-postgres # PostgreSQL\nyarn add @pikku/kysely @pikku/kysely-mysql # MySQL\nyarn add @pikku/kysely @pikku/kysely-sqlite # SQLite\n```\n\n## API Reference\n\n### PostgreSQL Connection — `PikkuKysely`\n\n```typescript\nimport { PikkuKysely } from '@pikku/kysely-postgres'\n\nconst db = new PikkuKysely<DB>(\n logger: Logger,\n connectionOrConfig: postgres.Sql | postgres.Options | string,\n defaultSchemaName?: string\n)\n\nawait db.init()\ndb.kysely // Kysely<DB> instance for queries\nawait db.close()\n```\n\n### SQLite Factory — `createSQLiteKysely`\n\n```typescript\nimport { createSQLiteKysely } from '@pikku/kysely-sqlite'\n\nconst kysely = createSQLiteKysely(database: SqliteDatabase | (() => Promise<SqliteDatabase>))\n```\n\n### Available Services\n\nEach database variant exports these services with a prefix (`Pg`, `MySQL`, `SQLite`, or base `Kysely`):\n\n| Service | Interface | Purpose |\n| --------------------- | ------------------------------------- | ---------------------------------------------- |\n| `*ChannelStore` | `ChannelStore` | WebSocket channel state persistence |\n| `*EventHubStore` | `EventHubStore` | Event hub state persistence |\n| `*WorkflowService` | `PikkuWorkflowService` | Workflow definition storage |\n| `*WorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |\n| `*DeploymentService` | `DeploymentService` | Deployment state management |\n| `*AIStorageService` | `AIStorageService, AIRunStateService` | AI conversation/run storage |\n| `*AgentRunService` | `AgentRunService` | Agent execution tracking |\n| `*SecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |\n\nAll services take a `Kysely<KyselyPikkuDB>` instance in their constructor and have an `init()` method that creates tables if needed.\n\n### Secret Service\n\n```typescript\nimport { PgKyselySecretService } from '@pikku/kysely-postgres'\n\nconst secrets = new PgKyselySecretService(db.kysely, {\n kekSecret: 'your-key-encryption-key',\n salt: 'your-salt',\n})\nawait secrets.init()\n\nawait secrets.setSecret('api-key', { key: 'sk-...' })\nconst value = await secrets.getSecret<{ key: string }>('api-key')\nawait secrets.rotateKEK() // Re-encrypt all secrets with new KEK\n```\n\n## Usage Patterns\n\n### PostgreSQL Setup\n\n```typescript\nimport {\n PikkuKysely,\n PgKyselyChannelStore,\n PgKyselyWorkflowService,\n} from '@pikku/kysely-postgres'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n const db = new PikkuKysely(logger, config.databaseUrl)\n await db.init()\n\n const channelStore = new PgKyselyChannelStore(db.kysely)\n await channelStore.init()\n\n const workflowService = new PgKyselyWorkflowService(db.kysely)\n await workflowService.init()\n\n return { config, logger, database: db, channelStore, workflowService }\n})\n```\n\n### SQLite Setup\n\n```typescript\nimport {\n createSQLiteKysely,\n SQLiteKyselyChannelStore,\n} from '@pikku/kysely-sqlite'\nimport Database from 'better-sqlite3'\n\nconst kysely = createSQLiteKysely(new Database('app.db'))\nconst channelStore = new SQLiteKyselyChannelStore(kysely)\nawait channelStore.init()\n```\n\n### MySQL Setup\n\n```typescript\nimport { MySQLKyselyWorkflowService } from '@pikku/kysely-mysql'\n\nconst workflowService = new MySQLKyselyWorkflowService(kyselyInstance)\nawait workflowService.init()\n```\n", "pikku-machine-auth/SKILL.md": "---\nname: pikku-machine-auth\ndescription: >-\n Use when authenticating a CLI/agent/service against a Pikku server, adding machine-to-machine\n (M2M) auth, issuing scoped API keys for sandboxes/agents/workers, or wiring better-auth sessions\n into Pikku middleware. Covers `pikku login` (device-authorization), the better-auth API Key\n plugin, machine identities, and `betterAuthSession` with the api-key branch. TRIGGER when: user\n asks about CLI login, `pikku login`, machine agents, service-to-service auth, API keys, client\n credentials, sandbox/worker tokens, or resolving a better-auth session in a Pikku function. DO\n NOT TRIGGER when: user asks about end-user HTTP session/cookie auth only (use pikku-http + the\n app betterAuth config) or about WebSocket channel mechanics (use pikku-websocket).\n---\n\n# Pikku Machine Auth\n\nUnified authentication for humans **and** machines against a Pikku + better-auth\nserver. Two paths, two headers, one resolver:\n\n| Caller | Credential | Header | Obtained by |\n|---|---|---|---|\n| **Human** (CLI, dev) | better-auth session token | `Authorization: Bearer <token>` | `pikku login` (device flow) → `~/.pikku/session.json` |\n| **Machine** (agent, sandbox, worker) | scoped API key | `x-api-key: <key>` | `createApiKey` (server-side, at provision/spawn) |\n\nBoth resolve to a Pikku `UserSession` through one middleware:\n`betterAuthSession({ mapSession, apiKey: { mapKey } })`.\n\n> The literal OAuth `client_credentials` grant is **not** implemented in\n> better-auth's oidc-provider. The API Key plugin gives the same capability (a\n> baked secret a service presents for scoped access), not the wire protocol.\n\n## Agent Operating Procedure\n\n1. Discover before editing — inspect the app's `betterAuth({ plugins: [...] })`\n config and existing middleware wiring before adding anything.\n2. Server changes go in the auth factory + a middleware wiring file; never put\n auth checks in a function body (use `permissions`).\n3. The API Key plugin contributes an `apikey` table — add the matching SQL\n migration and regenerate DB types before relying on it.\n4. Validate with the narrowest command, then `pikku all`.\n\n## Human path — `pikku login`\n\n```bash\npikku login --url https://app.example.com # device-authorization flow\npikku whoami # show current session + expiry\npikku logout # remove stored session\n```\n\n`pikku login` runs the RFC 8628 device flow: it requests a code, opens the\nbrowser to the verification URL, polls until you approve, then stores the\nsession token (keyed by base URL) at `~/.pikku/session.json` with its expiry.\n\n**Server requirement** — enable the `deviceAuthorization` and `bearer` plugins:\n\n```typescript\nimport { deviceAuthorization, bearer } from 'better-auth/plugins'\n\nbetterAuth({\n // ...\n plugins: [\n deviceAuthorization({ expiresIn: '5min', interval: '5s', schema: {} }),\n bearer(), // lets `Authorization: Bearer <session-token>` resolve a session\n ],\n})\n```\n\nThe browser approval is two steps the user's browser does automatically:\n`GET /auth/device?user_code=XXXX` (claims the code while signed in) then\n`POST /auth/device/approve`. The CLI only requests the code and polls\n`POST /auth/device/token`.\n\n## Machine path — API keys\n\nInstall the plugin (separate official package) and enable it:\n\n```bash\nyarn add @better-auth/api-key # peer: better-auth ^1.6.19\n```\n\n```typescript\nimport { apiKey } from '@better-auth/api-key'\n\nbetterAuth({\n plugins: [\n apiKey({\n enableMetadata: true, // REQUIRED to store scope on the key\n enableSessionForAPIKeys: true, // lets a key resolve via getSession too\n }),\n ],\n})\n```\n\n### Identity model\n\nA **machine is an API key, not a throwaway user.** Keys are owned by a small set\nof stable **service-user** identities you provision once (e.g. `orchestrator`,\n`machine-agent`, `builder`, `sandbox-runtime`). Per-machine scope rides on the\nkey's `metadata`/`permissions`. A key requires a real owning user row — minting\none for a non-existent `userId` is created but will not resolve.\n\n### Mint a scoped key (server-side, at spawn/provision)\n\n```typescript\n// `auth` is the better-auth instance (injected service)\nconst { key } = await auth.api.createApiKey({\n body: {\n userId: sandboxRuntimeUserId, // a stable service user\n name: `sandbox:${sandboxId}`,\n expiresIn: 60 * 60, // seconds\n metadata: { sandboxId }, // keep only STABLE ids here\n permissions: { sandbox: ['read', 'write'] },\n },\n})\n// inject `key` into the machine's env; it sends it as `x-api-key`.\n```\n\nRotate by minting a new key and expiring/deleting the old (`deleteApiKey`);\nmultiple active keys per identity allow zero-downtime rotation.\n\n### Resolve scope — `verifyApiKey`, not `getSession`\n\n`getSession(x-api-key)` returns only a bare mock session **without** the\nmetadata. Scope must come from `verifyApiKey`, which returns\n`{ valid, key: { userId, metadata, permissions } }`. The\n`betterAuthSession` api-key branch does this for you:\n\n```typescript\nimport { betterAuthSession } from '@pikku/better-auth'\nimport { addHTTPMiddleware } from '@pikku/core/http'\n\naddHTTPMiddleware([\n betterAuthSession({\n // human path: getSession result -> app session\n mapSession: ({ user }) => ({ userId: user.id }),\n // machine path: verified key -> app session. `services` lets you resolve\n // CURRENT scope (e.g. look up the owning row) instead of trusting only the\n // baked metadata.\n apiKey: {\n header: 'x-api-key', // default\n mapKey: async (key, services) => {\n const sandboxId = key.metadata?.sandboxId\n if (!sandboxId) return null // reject\n const row = await services.kysely\n .selectFrom('sandboxInstance')\n .innerJoin('sandbox', 'sandbox.id', 'sandboxInstance.sandboxId')\n .select(['sandbox.orgId', 'sandbox.projectId'])\n .where('sandboxInstance.sandboxId', '=', sandboxId)\n .where('sandboxInstance.stoppedAt', 'is', null)\n .executeTakeFirst()\n if (!row) return null\n return { userId: sandboxId, orgId: row.orgId, role: 'sandbox' }\n },\n },\n }),\n])\n```\n\nWhen the api-key header is present it is authoritative — the middleware never\nfalls through to `getSession` (a bare mock session would shadow the scoped one).\nWhen it is absent, the human `getSession` path runs as normal.\n\n### WebSocket channels authenticate on the upgrade handshake\n\nGenerated channel CLI clients attach the credential as a connection header\n(`x-api-key` for `PIKKU_API_KEY`, else `Authorization: Bearer` from\n`~/.pikku/session.json`). The `@pikku/ws` server copies the upgrade-request\nheaders into the channel's `http.request` and runs the inherited HTTP `*`\nmiddleware during `runUpgradeMiddleware`, so `betterAuthSession` resolves the\nsession before the channel opens. For this to work the app must register\n`betterAuthSession` via `addHTTPMiddleware([...])` (the `*` group) — not only on\nspecific routes — so it is inherited into the channel upgrade. Browser clients\ncannot set WebSocket headers, so header-auth only covers the Node CLI path; a\nbrowser channel needs a query-param/subprotocol vector instead.\n\n## Gotchas\n\n- `apiKey()` rejects `metadata` unless `enableMetadata: true`.\n- `deviceAuthorization()` requires a `schema` option (pass `schema: {}`).\n- Keep the two paths on **different headers** — `x-api-key` (machine) vs\n `Authorization: Bearer` (human). One header for both reintroduces ambiguity.\n- The `apikey` table is plugin-contributed — add the SQL migration + regen types.\n- `~/.pikku/session.json` is written `0600` and stores the token + expiry; the\n CLI uses the expiry to detect when a re-login is needed.\n", "pikku-mcp/SKILL.md": "---\nname: pikku-mcp\ndescription: >-\n Use when exposing Pikku functions as MCP tools, resources, or prompts for AI assistants. Covers\n mcp: true flag, pikkuMCPResourceFunc, pikkuMCPPromptFunc, and MCP wire object. TRIGGER when:\n code uses mcp: true or pikkuMCPResourceFunc/pikkuMCPPromptFunc, user asks about MCP, Model\n Context Protocol, AI tool integration, or exposing functions to Claude/ChatGPT. DO NOT TRIGGER\n when: user asks about AI agents (use pikku-ai-agent) or general function definitions (use\n pikku-concepts).\ninstallGroups: [core]\n---\n\n# Pikku MCP Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nExpose Pikku functions as Model Context Protocol (MCP) tools, resources, and prompts for AI assistants like Claude, ChatGPT, and others.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions that could become MCP tools\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### MCP Tools (simplest approach)\n\nAdd `mcp: true` to any existing `pikkuFunc` to expose it as an MCP tool:\n\n```typescript\nconst myFunc = pikkuFunc({\n description: string, // Used as MCP tool description\n input: ZodSchema, // Becomes MCP tool input schema\n output: ZodSchema, // Return type\n mcp: true, // ← Expose as MCP tool\n func: async (services, data) => { ... },\n})\n```\n\n### MCP Resources (`pikkuMCPResourceFunc`)\n\n```typescript\nimport { pikkuMCPResourceFunc } from '#pikku'\n\nconst resource = pikkuMCPResourceFunc({\n uri: string, // URI template, e.g. 'todos/{id}'\n title: string, // Human-readable title\n description?: string,\n func: async (services, data, { mcp }) => {\n // Must return array of { uri, text } or { uri, blob, mimeType }\n return [{ uri: mcp.uri!, text: JSON.stringify(result) }]\n },\n})\n```\n\n### MCP Prompts (`pikkuMCPPromptFunc`)\n\n```typescript\nimport { pikkuMCPPromptFunc } from '#pikku'\n\nconst prompt = pikkuMCPPromptFunc({\n name: string,\n description: string,\n func: async (services, data) => {\n // Must return array of MCP messages\n return [\n {\n role: 'user',\n content: { type: 'text', text: '...' },\n },\n ]\n },\n})\n```\n\n### MCP Wire Object\n\nInside MCP-enabled functions, `wire.mcp` provides:\n\n```typescript\nmcp.uri // Current resource URI (for resources)\nmcp.sendResourceUpdated(uri) // Notify clients a resource changed\nmcp.enableTools({ toolName: true }) // Dynamically enable/disable tools\n```\n\n## Usage Patterns\n\n### Expose Existing Functions as MCP Tools\n\nThe simplest path — add `mcp: true` to any function:\n\n```typescript\nexport const createTodo = pikkuFunc({\n description: 'Create a new todo item',\n input: CreateTodoInput,\n output: CreateTodoOutput,\n mcp: true,\n func: async ({ db }, { text, priority }) => {\n return await db.createTodo({ text, priority })\n },\n})\n```\n\n### MCP Resources with URI Templates\n\n```typescript\nexport const getTodo = pikkuMCPResourceFunc({\n uri: 'todos/{id}',\n title: 'Todo Details',\n description: 'Get a todo by ID',\n func: async ({ db }, { id }, { mcp }) => {\n const todo = await db.getTodo(id)\n return [{ uri: mcp.uri!, text: JSON.stringify(todo) }]\n },\n})\n```\n\n### MCP Prompts\n\n```typescript\nexport const codeReview = pikkuMCPPromptFunc({\n name: 'codeReview',\n description: 'Generate a code review prompt',\n func: async ({}, { filePath, context }) => {\n return [\n {\n role: 'user',\n content: {\n type: 'text',\n text: `Review ${filePath}. Context: ${context}`,\n },\n },\n ]\n },\n})\n```\n\n### Dynamic Tool Control\n\n```typescript\nexport const manageTodos = pikkuFunc({\n description: 'Manage todo items',\n input: ManageTodosInput,\n output: ManageTodosOutput,\n mcp: true,\n func: async ({ db }, { action, id }, { mcp }) => {\n if (action === 'delete') {\n await db.deleteTodo(id)\n mcp.sendResourceUpdated(`todos/${id}`)\n await mcp.enableTools({ archiveTodos: true })\n return { deleted: true }\n }\n },\n})\n```\n\n### MCP Server Setup\n\n```typescript\n// start.ts\nimport { PikkuMCPServer } from '@pikku/modelcontextprotocol'\n\nconst server = new PikkuMCPServer(config, singletonServices, createWireServices)\nawait server.init()\nawait server.start()\n```\n\n## Complete Example\n\n```typescript\n// functions/todos.functions.ts\nexport const listTodos = pikkuSessionlessFunc({\n description: 'List all todo items',\n input: ListTodosInput,\n output: ListTodosOutput,\n mcp: true,\n func: async ({ db }, { status }) => {\n return { todos: await db.listTodos(status) }\n },\n})\n\nexport const createTodo = pikkuFunc({\n description: 'Create a new todo item',\n input: CreateTodoInput,\n output: CreateTodoOutput,\n mcp: true,\n func: async ({ db }, { text, priority }) => {\n return await db.createTodo({ text, priority })\n },\n})\n\nexport const completeTodo = pikkuFunc({\n description: 'Mark a todo as complete',\n input: CompleteTodoInput,\n output: CompleteTodoOutput,\n mcp: true,\n func: async ({ db }, { todoId }) => {\n return await db.completeTodo(todoId)\n },\n})\n\n// functions/todos.mcp.ts\nexport const getTodoResource = pikkuMCPResourceFunc({\n uri: 'todos/{id}',\n title: 'Todo Details',\n description: 'Get details of a specific todo',\n func: async ({ db }, { id }, { mcp }) => {\n const todo = await db.getTodo(id)\n return [{ uri: mcp.uri!, text: JSON.stringify(todo) }]\n },\n})\n\nexport const planDayPrompt = pikkuMCPPromptFunc({\n name: 'planDay',\n description: 'Create a daily plan based on pending todos',\n func: async ({ db }, {}) => {\n const { todos } = await db.listTodos('pending')\n return [\n {\n role: 'user',\n content: {\n type: 'text',\n text: `Plan my day. Here are my pending todos:\\n${todos.map((t) => `- ${t.text} (${t.priority})`).join('\\n')}`,\n },\n },\n ]\n },\n})\n```\n", "pikku-middleware/references/middleware-patterns.md": "# Middleware Patterns (extended)\n\nDetailed, less-common middleware recipes. The common-path bearer-auth pattern lives inline in SKILL.md; this file holds the client-side caller, session-setting, and audit recipes.\n\n## Service-to-Service: the client (caller) side\n\nUse the generated `RPCInvoke` type from `.pikku/rpc/pikku-rpc-wirings-map.gen.d.ts` — never hand-write the input/output types:\n\n```typescript\nimport type { RPCInvoke } from '../../backends/my-service/.pikku/rpc/pikku-rpc-wirings-map.gen.d.js'\n\nexport function getServiceRPC(baseUrl: string, token: string): RPCInvoke {\n return async (name: string, data?: unknown) => {\n const res = await fetch(`${baseUrl}/rpc/${String(name)}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ data: data ?? {} }),\n })\n if (!res.ok) {\n const text = await res.text().catch(() => '')\n throw new Error(`rpc ${String(name)} failed: ${res.status} ${text}`)\n }\n return res.json()\n } as RPCInvoke\n}\n```\n\n## Session-Setting Middleware\n\n```typescript\nconst apiKeyAuth = pikkuMiddleware(async ({ kysely }, { http, setSession, session }, next) => {\n if (session) return next() // already authenticated\n\n const header = http?.request?.header?.('x-api-key')\n if (!header) return next()\n\n const row = await kysely.selectFrom('apiKey').select('userId').where('key', '=', header).executeTakeFirst()\n if (row) setSession?.({ userId: row.userId })\n\n return next()\n})\n\naddTagMiddleware('api-key-auth', [apiKeyAuth])\n```\n\nFunctions tagged `'api-key-auth'` with `auth: true` reject requests without a valid key; those with `auth: false` can inspect the session but won't reject.\n\n## Request Logging / Audit\n\n```typescript\nconst auditLog = pikkuMiddleware(async ({ logger, db }, wire, next) => {\n const start = Date.now()\n await next()\n await db.createAuditLog({ duration: Date.now() - start })\n})\n\naddHTTPMiddleware('/admin/*', [auditLog])\n```\n", "pikku-middleware/SKILL.md": "---\nname: pikku-middleware\ndescription: >-\n Use when adding any middleware to a Pikku app — global HTTP middleware, tag-scoped middleware\n (including service-to-service bearer auth), per-route middleware, session-setting middleware, or\n understanding middleware execution order and priority. TRIGGER when: user wants middleware on\n some or all routes, machine-to-machine auth, tag-scoped cross-cutting concerns, global\n interceptors, or middleware priority/order questions. DO NOT TRIGGER when: user asks about\n permissions/authorization checks (use pikku-permissions), auth strategies like\n authBearer/authCookie (use pikku-security), or deployment.\ninstallGroups: [core]\n---\n\n# Pikku Middleware\n\n## Agent Operating Procedure\n\n1. Discover before editing. Run `pikku info middleware --verbose` and `pikku info tags --json` to understand the existing middleware and tag landscape.\n2. Identify the source files that own the behavior — wirings files, not generated output.\n3. Register middleware at module load time — in a `wirings/*.ts` file, never inside a function body.\n4. Validate: run `pikku all --tsc` after adding or changing middleware — it regenerates and then confirms type safety in one pass.\n\n## The `pikkuMiddleware` Factory\n\n```typescript\nimport { pikkuMiddleware } from '#pikku'\n\n// Simple: just a function\nconst myMiddleware = pikkuMiddleware(async (services, wire, next) => {\n // runs before the function\n await next()\n // runs after the function (optional)\n})\n\n// With metadata (name + priority)\nconst telemetryMiddleware = pikkuMiddleware({\n name: 'my-telemetry',\n priority: 'highest',\n func: async (services, wire, next) => {\n const start = performance.now()\n try {\n await next()\n } finally {\n services.logger.info({ duration: Math.round(performance.now() - start) })\n }\n },\n})\n```\n\nThe `wire` object gives you:\n- `wire.http` — inbound HTTP context (headers, URL, cookies)\n- `wire.setSession(session)` — set the session for this request\n- `wire.getSession()` — read the current session\n- `wire.session` — the session set so far (may be undefined)\n\nThrow a typed error to abort: `UnauthorizedError`, `ForbiddenError`, etc. from `@pikku/core/errors`.\n\n## Scoping: Five Levels\n\nFrom broadest to narrowest:\n\n```typescript\n// 1. Wire-agnostic global: all wire types (HTTP, Queue, Channel, Trigger, Workflow, ...)\naddGlobalMiddleware([telemetryOuter()])\n\n// 2. HTTP global: all HTTP routes\naddHTTPMiddleware('*', [cors(), authBearer()])\n\n// 3. Prefix-based: URL pattern\naddHTTPMiddleware('/admin/*', [auditLog])\n\n// 4. Tag-based: any wiring with matching tag\naddTagMiddleware('machine-agent', [bearerAuth]) // tag on function or wire\n\n// 5. Inline: per-wiring\nwireHTTP({\n route: '/books/:id',\n func: getBook,\n middleware: [cacheControl],\n})\n```\n\n## Global Middleware (`addGlobalMiddleware`)\n\nRuns before everything else, across every wire type: HTTP, Queue, Channel, Trigger, Scheduler, Workflow, Agent, CLI, MCP. Use it for cross-cutting concerns (e.g. telemetry) that must wrap every invocation regardless of transport.\n\n```typescript\nimport { addGlobalMiddleware } from '@pikku/core'\nimport { telemetryOuter, telemetryInner } from '@pikku/core/middleware'\n\naddGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })]) // wraps the full call\naddGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })]) // closest to the function body\n```\n\n`telemetryOuter` ships with `priority: 'highest'`, `telemetryInner` with `priority: 'lowest'` — so priority sorting places outer first regardless of array/call order.\n\n## HTTP & Prefix Middleware (`addHTTPMiddleware`)\n\n```typescript\nimport { addHTTPMiddleware } from '@pikku/core/http'\nimport { cors, authBearer } from '@pikku/core/middleware'\n\n// All routes\naddHTTPMiddleware('*', [cors({ origin: 'https://app.example.com', credentials: true })])\n\n// Scoped to /api/* prefix\naddHTTPMiddleware('/api/*', [rateLimit({ maxRequests: 100, windowMs: 60_000 })])\n```\n\n## Tag Middleware (`addTagMiddleware`)\n\nTag middleware fires for any wiring (function or wire object) that carries a matching tag. This is the canonical approach for service-to-service bearer auth, rate limiting a group, or any cross-cutting concern scoped to a subset of routes.\n\n### Setting Tags\n\n```typescript\n// On the function definition\nexport const myFunc = pikkuSessionlessFunc({\n auth: false,\n tags: ['machine-agent'],\n func: async (services, input) => { ... },\n})\n\n// On the wire object\nwireHTTP({\n route: '/internal/action',\n method: 'post',\n auth: false,\n tags: ['internal'],\n func: myFunc,\n})\n```\n\nTags from the function definition and the wire object are merged — middleware from both tag sets runs.\n\n### Registering Tag Middleware\n\n```typescript\nimport { addTagMiddleware } from '.pikku/pikku-types.gen.js'\n\naddTagMiddleware('machine-agent', [machineAgentBearerAuth])\n```\n\nCall at module load time — typically in the same `wirings/*.ts` file as the `wireHTTP` calls that use the tag.\n\n## Middleware Execution Order\n\n**Scope resolution order (broadest → narrowest):**\n\n```text\nglobal → httpGroup/* → httpGroup/prefix → wiringTags → wiringMiddleware → funcTags → funcMiddleware → function body\n```\n\n**Within each scope, sorted by priority:**\n\n```text\nhighest → high → medium (default) → low → lowest\n```\n\nSet priority using the config-object form of `pikkuMiddleware`:\n\n```typescript\nconst earlyMiddleware = pikkuMiddleware({\n name: 'early',\n priority: 'highest', // 'highest' | 'high' | 'medium' | 'low' | 'lowest'\n func: async (services, wire, next) => { ... },\n})\n```\n\nPriority is the primary sort key; within the same level, registration order is preserved. Use priority when a middleware must run before/after others regardless of registration order (e.g. telemetry wrapping everything, session extraction before auth checks).\n\n## Service-to-Service Bearer Auth (canonical pattern)\n\nA server that exposes RPCs only to a trusted caller (e.g. an API calling a machine-agent). Auth lives in a tag middleware — NOT in the function body. Authorization/permission checks belong in the `permissions` field (see `pikku-permissions`), never inside `func`.\n\n**On the server (the service being called):** tag the function, register a `pikkuMiddleware` that reads the `Authorization` header on that tag.\n\n```typescript\n// lib/host-token.ts\nlet _token: string | null = null\nexport const setToken = (t: string) => { _token = t }\nexport const getToken = () => _token\n```\n\n```typescript\n// wirings/http.wiring.ts\nimport { timingSafeEqual } from 'node:crypto'\nimport { addTagMiddleware, pikkuMiddleware } from '../../.pikku/pikku-types.gen.js'\nimport { UnauthorizedError } from '@pikku/core/errors'\nimport { getToken } from '../lib/host-token.js'\n\nconst bearerAuth = pikkuMiddleware(async (_services, { http }, next) => {\n const authHeader = http?.request?.header?.('authorization') || http?.request?.header?.('Authorization')\n const token = getToken()\n const expected = token ? `Bearer ${token}` : null\n if (\n !expected ||\n !authHeader ||\n authHeader.length !== expected.length ||\n !timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected))\n ) {\n throw new UnauthorizedError()\n }\n return next()\n})\n\naddTagMiddleware('machine-agent', [bearerAuth])\n```\n\n```typescript\n// functions/my.function.ts\nexport const myFunc = pikkuSessionlessFunc({\n expose: true,\n auth: false,\n tags: ['machine-agent'],\n func: async (services, input) => { ... },\n})\n```\n\n**On the client (the caller):** use the generated `RPCInvoke` type — never hand-write a `fetch` wrapper's types. See `references/middleware-patterns.md`.\n\n## More patterns\n\n`references/middleware-patterns.md` covers the client-side `RPCInvoke` caller, session-setting middleware (set a session from an API key), and request logging / audit middleware.\n\n## After Changes\n\n```bash\npikku all # regenerate metadata so new tags are picked up\npikku all --tsc # regenerate, then type-check (fails on type errors)\n```\n", "pikku-mongodb/SKILL.md": "---\nname: pikku-mongodb\ndescription: >-\n Use when setting up MongoDB database services in a Pikku app. Covers PikkuMongoDB connection,\n channel stores, workflow services, secret services, AI storage, agent runs, and deployment\n services. TRIGGER when: code uses PikkuMongoDB, MongoDBChannelStore, MongoDBWorkflowService,\n MongoDBSecretService, or user asks about MongoDB setup with Pikku. DO NOT TRIGGER when: user\n asks about SQL databases (use pikku-kysely) or Redis (use pikku-redis).\n---\n\n# Pikku MongoDB\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/mongodb` provides MongoDB-backed implementations of Pikku's core service interfaces.\n\n## Installation\n\n```bash\nyarn add @pikku/mongodb\n```\n\n## API Reference\n\n### `PikkuMongoDB` (Connection Wrapper)\n\n```typescript\nimport { PikkuMongoDB } from '@pikku/mongodb'\n\nconst mongo = new PikkuMongoDB(\n logger: Logger,\n clientOrUri: MongoClient | string,\n dbName: string,\n options?: MongoClientOptions\n)\n\nawait mongo.init()\nmongo.db // Db instance for queries\nawait mongo.close()\n```\n\n### Available Services\n\n| Service | Interface | Purpose |\n| --------------------------- | ------------------------------------- | ---------------------------------------------- |\n| `MongoDBChannelStore` | `ChannelStore` | WebSocket channel state persistence |\n| `MongoDBEventHubStore` | `EventHubStore` | Event hub state persistence |\n| `MongoDBWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |\n| `MongoDBWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |\n| `MongoDBDeploymentService` | `DeploymentService` | Deployment state management |\n| `MongoDBAIStorageService` | `AIStorageService, AIRunStateService` | AI conversation/run storage |\n| `MongoDBAgentRunService` | `AgentRunService` | Agent execution tracking |\n| `MongoDBSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |\n\nAll services take a `Db` instance in their constructor and have an `init()` method that creates collections/indexes.\n\n### Secret Service\n\n```typescript\nimport { MongoDBSecretService } from '@pikku/mongodb'\n\nconst secrets = new MongoDBSecretService(mongo.db, {\n kekSecret: 'your-key-encryption-key',\n salt: 'your-salt',\n})\nawait secrets.init()\n\nawait secrets.setSecret('api-key', { key: 'sk-...' })\nconst value = await secrets.getSecret<{ key: string }>('api-key')\nawait secrets.rotateKEK()\n```\n\n## Usage Patterns\n\n### Full Setup\n\n```typescript\nimport {\n PikkuMongoDB,\n MongoDBChannelStore,\n MongoDBWorkflowService,\n} from '@pikku/mongodb'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n const mongo = new PikkuMongoDB(logger, config.mongoUri, 'myapp')\n await mongo.init()\n\n const channelStore = new MongoDBChannelStore(mongo.db)\n await channelStore.init()\n\n const workflowService = new MongoDBWorkflowService(mongo.db)\n await workflowService.init()\n\n return { config, logger, database: mongo, channelStore, workflowService }\n})\n```\n", "pikku-n8n-import/references/addon-mapping.md": "# Integration stub → Pikku addon\n\nTranslate n8n integration nodes (`gmailTool`, `slackTool`, `googleSheetsTool`, plain\n`gmail` / `slack` action nodes, etc.) that the importer left as throwing stubs into\nreal `ref('<addonRpc>')` references pointing at functions in **installed**\n`@pikku/addon-*` packages.\n\nThis is **per-stub mechanical**. Do not invent business logic, chain calls, or\n\"improve\" the workflow. Read one manifest entry, find the matching addon function,\nrewrite the stub.\n\n## Inputs\n\n1. **Manifest** — `<workflow>.integrations.json` next to the `.graph.ts`. Each entry:\n ```jsonc\n {\n \"rpcName\": \"agentGmailtool__sendAMessageInGmail\",\n \"n8nType\": \"n8n-nodes-base.gmailTool\",\n \"n8nName\": \"Send a message in Gmail\",\n \"parameters\": { \"sendTo\": \"...\", \"message\": \"...\", \"subject\": \"...\" },\n \"credentials\": { \"gmailOAuth2\": { \"id\": \"...\", \"name\": \"Personal Gmail\" } },\n \"isAgentTool\": true,\n \"agentName\": \"Inbox Assistant\"\n }\n ```\n2. **Installed addons** — `@pikku/addon-*` in the project's `package.json`\n `dependencies`. Map **only** against installed packages. If the addon for an n8n\n type is not installed, surface it (see SKILL step 4) — never silently skip and\n never pick a vaguely-named function from another addon.\n\n## Per entry, in order\n\n### Step 1 — identify the target addon\n\nMap `n8nType` to a package by reading its source. Common shapes (**guesses, not\nauthoritative** — always verify against installed source):\n\n| n8n type prefix | typical addon candidate |\n|---|---|\n| `n8n-nodes-base.gmail` / `gmailTool` | `@pikku/addon-email-gmail` |\n| `n8n-nodes-base.slack` / `slackTool` | `@pikku/addon-chat-slack` |\n| `n8n-nodes-base.googleSheets` / `…Tool` | `@pikku/addon-sheets-google` |\n| `n8n-nodes-base.notion` / `notionTool` | `@pikku/addon-docs-notion` |\n| `n8n-nodes-base.telegram` / `telegramTool` | `@pikku/addon-chat-telegram` |\n\nIf no installed addon plausibly covers the n8n type, stop and report it — do not\npick a wrong addon.\n\n### Step 2 — pick the function (resource + operation → fn name)\n\nn8n nodes use a `(resource, operation)` pair. Rubric:\n\n- `resource` defaults to the integration's primary noun if absent (gmail →\n `message`, slack → `message`, sheets → `spreadsheet`). Read the addon's folder\n structure (`messages/`, `drafts/`, `channels/`) to see what nouns exist.\n- `operation` is usually a verb (`get`, `getAll`, `send`, `delete`, `addLabels`).\n- The pikku function name is almost always `<resource><Verb>` in camelCase, matching\n the file's `export const` (`messageList` for `messages/list.function.ts`).\n\nVerify: `grep -h \"^export const\" <addonPkg>/src/functions/**/*.ts` and match by name.\nConventions in `@pikku/addon-email-gmail` (a **sanity reference**, not a fallback):\n\n- `getAll → <resource>List`, `get → <resource>Get`, `send → <resource>Send`,\n `delete → <resource>Delete`, `reply → <resource>Reply`\n- `addLabels → <resource>AddLabel` (singular!), `removeLabels → <resource>RemoveLabel`\n- `markAsRead / markAsUnread → <resource>MarkRead / <resource>MarkUnread`\n- `create → <resource>Create`\n\nIf the function has a `node:` block, prefer matching its `category`/`displayName`\nover guessing.\n\n### Step 3 — rewrite the stub\n\nTwo outcomes, by `isAgentTool`:\n\n**A) `isAgentTool: true`** — the stub is an agent tool referenced via `ref()`:\n1. Delete the stub file.\n2. In the agent file, replace `ref('agentGmailtool__sendAMessageInGmail')` in\n `tools: [...]` with `ref('messageSend')` (the resolved addon function).\n3. Ensure the addon is where pikku scans functions (usually automatic via\n `node_modules/@pikku/addon-*`).\n\nIf you can't delete safely, leave a one-line re-export instead of a stub:\n```ts\nimport { messageSend } from '@pikku/addon-email-gmail'\nexport const agentGmailtool__sendAMessageInGmail = messageSend\n```\nDefault is delete + retarget; wrappers add maintenance burden.\n\n**B) `isAgentTool: false`** — the stub is a graph node:\n1. Open `<workflow>.graph.ts`.\n2. In `nodes: { … }` find the entry whose value is the stub rpc name.\n3. Replace it with the addon function name (`'messageSend'`).\n4. If `config: { <id>: { input } }` produces an `{ items }` envelope, rewrite it to\n the addon function's real input schema (read its `input: z.object({...})`).\n5. Delete the stub file.\n\n### Step 4 — port hard-coded parameters\n\n- **Hardcoded values** (`\"limit\": 20`, `\"labelIds\": [\"INBOX\"]`) were user choices.\n Preserve them in the graph node's `input` (case B). **Agent tools cannot carry\n hardcoded params** (the LLM fills args at call time) — surface the trade-off; if\n the user needs a value pinned, they keep a thin wrapper.\n- **`$fromAI('Name', '', 'string')`** placeholders are LLM-filled — the addon's Zod\n schema becomes the tool schema. Just drop the placeholder string; no other action.\n\n### Step 5 — credentials\n\n`credentials: { gmailOAuth2: { id, name } }` is the n8n credential ref. Pikku addons\nexpect a wired service (`services.gmail`). Do **not** auto-wire — leave a TODO:\n\n> `// TODO: wire services.gmail using credential \"Personal Gmail\" (n8n id: gmail_cred_1) — see @pikku/addon-email-gmail/README.md`\n\n## Never\n\n- Invent functions that don't exist — grep first.\n- Pick a wrong addon because the right one isn't installed — report `npm i @pikku/addon-<x>`.\n- Bake per-mapping tables into `@pikku/n8n-import` — it is addon-agnostic; mapping lives here.\n- Modify the manifest — it's an audit artifact.\n- Chain calls — each entry maps to exactly one addon function.\n- Silently drop a hardcoded param — surface it.\n", "pikku-n8n-import/references/code-translation.md": "# n8n Code node → Pikku function\n\nReplace a Code-node stub's `throw new Error(...)` body with a faithful TypeScript\nreimplementation. The original JS is preserved verbatim in the JSDoc above the\nfunction. Keep the signature and JSDoc intact; only widen the Zod input/output if\nthe code's data shape demands it.\n\n**Narrow, mechanical translation** — behavioral parity, not better code. Do not\nrefactor, add error handling, or invent fields.\n\n## Process\n\n1. **Read the file.** Identify the input schema, output schema, the verbatim JS in\n the JSDoc, and the `pikkuSessionlessFunc` shape.\n2. **Determine the n8n mode** from the original JSON if available (importer leaves it\n in `fixtures/`, or ask). Two modes:\n - `runOnceForAllItems` (default) — code runs once with `items: Array<{ json, binary, pairedItem }>`, returns an array of envelopes.\n - `runOnceForEachItem` — runs once per item, `$json` / `$input.item.json` in scope, returns a single envelope.\n - If unknown, infer: bare `items.X` → all-items; bare `$json.X` / `$input.item.X` → each-item.\n3. **Apply the rubric.**\n4. **Edit only the function body.** Leave imports, schemas, JSDoc, name, description, refs untouched unless step 5 forces it.\n5. **If the schemas are wrong** (code reads `$json.userId: string` but input is `items: z.array(z.unknown())`), tighten with the smallest change. Prefer `z.unknown()` over `z.any()`. Never widen output to `z.any()`.\n6. **Add one comment** at the top of the body noting the mode: `// translated from n8n Code node, mode: runOnceForAllItems`. This is the *only* comment you may add.\n7. **Typecheck** (`yarn tsc` from the package root); fix errors with the smallest change.\n\n## Rubric\n\n### Envelope unwrapping — all-items mode\n\n| n8n | Pikku |\n|---|---|\n| `items` | `(data.items ?? []) as any[]` (or typed if known) |\n| `items[i].json.X` | `items[i].X` |\n| `items[i].json` | `items[i]` |\n| `items[i].binary` | **NOT supported** — leave a TODO and explain |\n| `items.length` | `items.length` |\n| `items.map(i => i.json.X)` | `items.map((i: any) => i.X)` |\n\n### Envelope unwrapping — each-item mode\n\n| n8n | Pikku |\n|---|---|\n| `$json.X` / `$input.item.json.X` | `data.X` (input is the item itself) |\n| `$input.item.json` | `data` |\n| `$input.all()` | not available per-item — change to all-items mode |\n\n### Return statement\n\n| n8n | Pikku |\n|---|---|\n| `return [{ json: X }]` | `return { items: [X] }` |\n| `return items.map(i => ({ json: ... }))` | `return { items: items.map(...) }` |\n| `return [{ json: X }, { json: Y }]` | `return { items: [X, Y] }` |\n| `return { json: X }` (each-item) | `return X` |\n| `return [...]` (already plain) | wrap in `{ items: [...] }` only if the output schema expects it |\n\n### Built-ins — do NOT auto-translate\n\nIf the code references any of these, **stop**, leave the body a stub, and annotate\n`// TODO:` + explain:\n\n- `this.helpers.*` (binary buffers, HTTP requests, prepareBinaryData)\n- `$node['Some Node'].json` (cross-node refs — resolve via Pikku `ref()` upstream, not in the body)\n- `$workflow`, `$execution`, `$item()`, `$items('Other Node')`\n- `getBinaryDataBuffer` / `getStaticData` — no equivalent, TODO\n- `require()` / dynamic `import()` — flag and stop\n\nTranslate these only when reachable: `$now`/`$today` → `new Date()`; `$env.X` →\n`services.variables.get('X')` (never `process.env` — Pikku house rule; `services` is\nthe first param).\n\n### Async / types\n\n- Original uses `await` → the body is already `async`; keep every `await`.\n- `this.helpers.httpRequest(...)` → do NOT inline; the user should use a separate\n `httpRequest` rpc node. Leave a `// TODO:` and explain.\n- Cast `items` as `any[]` only if the schema is `z.array(z.unknown())`; use the\n inferred type if tightened. Never `as any` on the return — fix the schema instead.\n\n## Example\n\nBefore (stub):\n```ts\n/**\n * STUB — generated from n8n Code node \"Custom Code\".\n * const total = items.reduce((acc, i) => acc + i.json.amount, 0);\n * return [{ json: { total } }];\n */\nexport const codeStubCustomCode = pikkuSessionlessFunc({\n input: CodeStubCustomCodeInput,\n output: CodeStubCustomCodeOutput,\n func: async (_services, _data) => {\n throw new Error('Stub: ported from n8n Code node \"Custom Code\" — implement me')\n },\n})\n```\n\nAfter:\n```ts\nexport const codeStubCustomCode = pikkuSessionlessFunc({\n description: 'Ported from n8n Code node \"Custom Code\"',\n input: CodeStubCustomCodeInput,\n output: CodeStubCustomCodeOutput,\n func: async (_services, data) => {\n // translated from n8n Code node, mode: runOnceForAllItems\n const items = (data.items ?? []) as any[]\n const total = items.reduce((acc, i) => acc + i.amount, 0)\n return { items: [{ total }] }\n },\n})\n```\n\n## Report\n\nTerse: the mode you inferred (one sentence), the literal rubric translations\napplied, anything flagged TODO and why, any schema tightening (before → after).\n\nDo not add tests, refactor, edit other files, \"improve\" the logic, or add\ntry/catch unless the original did. If the code is empty, comment-only, or so\ndependent on n8n internals that no honest translation is possible, leave the stub\nand tell the user which n8n features block it.\n", "pikku-n8n-import/references/loops-and-control.md": "# Loops & control stubs\n\nThe importer maps the mechanical control flow (IF/Filter/Switch it can normalize →\n`graph:branch`) but leaves the **semantic** cases as `control` stubs — chiefly\n**Loop Over Items / splitInBatches** and Switch in expression mode. These need\njudgment, which is why they are not compiled. Read the loop body and the workflow\naround it; decide, or ask.\n\n## Loop Over Items / splitInBatches\n\nn8n's loop node has two outputs: **loop** (output 1, fires per batch) and **done**\n(output 0, fires once when iteration finishes). The loop body flows from the loop\noutput back into the node — a cycle. Pikku graphs are a DAG, so **the loop becomes a\n`graph:map`** (`@pikku/addon-graph`) and the back-edge disappears:\n\n```ts\ntheLoop: \"graph:map\", // (graph:fanout) — one child invocation per item\n// config:\ntheLoop: {\n input: (ref) => ({\n items: ref(\"<predecessor>\"), // what fed the loop\n child: \"<childRpc-or-subGraph>\", // the loop body\n childInput: { /* $item-rebound body input */ },\n stepPrefix: \"theLoop\",\n }),\n next: \"<done-branch target>\", // output 0\n}\n```\n\nInside `childInput`, references rebind to the current element: the body's `$json` /\npredecessor and any `$('<loop node>')` become `$item`.\n\n### Decide the shape first\n\n| Loop body does… | Emit |\n|---|---|\n| transform each item independently (enrich, format, call one thing) | `graph:map` — child = the body |\n| accumulate across items (running total, build one object/array) | a **reduce**: a single generated function over the whole array, not a map — `graph:map` collects per-item results and *loses* the accumulator |\n| pure side-effect per item, nothing downstream consumes results | `graph:map` with **no `next`** (done branch empty) — the safest, unambiguous case |\n\n### Child arity\n\n- **Single-node body** → `child: \"<that node's rpc>\"`, its input as `childInput`.\n- **Multi-node body** → the child must be a per-item **sub-graph**. Lift the body\n into its own `pikkuWorkflowGraph` (see `pikku-workflow`) and set `child` to that\n workflow's registered name. If the body references nodes **outside** the loop\n (not just the item), that value has to be threaded in as `childInput` — if you\n can't do it cleanly, stop and ask rather than emit something subtly wrong.\n\n### Done-branch semantics (ask if it matters)\n\nn8n's done output is version-dependent: it may carry the *original* items or the\n*accumulated* results. `graph:map`'s `next` receives the array of child results.\nIf a downstream node reads that array's shape and the distinction matters, add:\n\n```ts\n// TODO(n8n): done branch receives collected loop results (not original items) — confirm this matches intent\n```\n\nand call it out in your summary. When the done branch is empty, there's nothing to\ndecide.\n\n### batchSize > 1\n\n`graph:map` is one-item-at-a-time. A real numeric `batchSize` (chunk into groups,\nrun the body per chunk) has no direct primitive — leave the stub, and tell the user\nthis loop batches N-at-a-time and needs a manual pass (or a `graph:chunk` +\n`graph:map` composition if the body is chunk-shaped).\n\n## Switch / control stubs the importer couldn't normalize\n\nA Switch in **expression mode** (routing by an arbitrary JS expression rather than\ncomparable conditions) stays a `control` stub. Options, in order of preference:\n\n1. If the expression is really a set of value comparisons, rewrite the node as a\n `graph:branch` by hand (see `pikku-workflow` for the `branch` shape) and wire the\n emitted `next` keys to the branch targets.\n2. If it's genuinely computed routing, translate the stub into a small function that\n returns the branch key, then feed it a `graph:branch`.\n3. If neither is faithful, leave the stub and explain what the Switch does.\n\n## Never\n\n- Emit a `graph:map` for an accumulator loop — you'll silently drop the running state.\n- Guess the done-branch semantics when a downstream node depends on the shape — mark\n it and surface it.\n- Invent a batching primitive — say what's unsupported instead.\n", "pikku-n8n-import/SKILL.md": "---\nname: pikku-n8n-import\ndescription: 'Use to import an n8n workflow JSON export into a runnable Pikku workflow. Triggers when the user says \"import this n8n workflow\", \"convert this n8n export to pikku\", points at an n8n `.json` export or a directory of them, or picks up after `pikku import n8n` left throwing stub functions (`STUB — generated from n8n …`, `— implement me`) or a `<workflow>.integrations.json` manifest. Owns the whole flow: run the importer, triage what it could not map, fill each stub, report any missing `@pikku/addon-*` integrations, and verify the result compiles and runs with no surviving stubs. DO NOT TRIGGER for hand-written addon wiring unrelated to an n8n import (use pikku-addon), or for authoring workflows from scratch (use pikku-workflow).'\nmetadata:\n version: 1.0.0\n---\n\n# n8n → Pikku Import\n\nTake an n8n export all the way to a compiling, stub-free Pikku workflow. The\n`@pikku/n8n-import` package (invoked by `pikku import n8n`) is **frozen**: it does\nthe provable, mechanical conversion and leaves everything it cannot prove as a\ntyped stub that throws at runtime. This skill runs that package, then fills the\nremainder with judgment, reports what needs a human decision, and verifies.\n\nNever re-do what the importer already did, and never hand-edit generated files to\npaper over a stub — fix the source cause (the stub function, the graph node, or a\nmissing dependency).\n\n## Agent Operating Procedure\n\n1. Discover before editing. Prefer `pikku-meta`/`pikku meta ... --json` when\n available; inspect only the focused output you need.\n2. Identify the source file that owns the behavior. Do not start from generated\n output, `.pikku`, `node_modules`, or vendored packages.\n3. Make the smallest source change that satisfies the task. Keep generated files\n generated.\n4. Validate with the narrowest relevant command first, then `pikku all` /\n `pikku-verify` when functions, wirings, or schemas changed.\n5. If validation fails, fix the source cause and rerun. Never edit generated\n files to hide an error.\n\n## Workflow\n\n### 1 — Run the importer (do as much as possible, cheaply)\n\n```bash\npikku import n8n <export.json> [outDir]\n```\n\nIt writes `<slug>.graph.ts` (+ `.agent.ts` for AI workflows), `<slug>.addons.gen.ts`,\na `<slug>.integrations.json` manifest, and one stub function per node it could not\nmap. It **exits 1** on an un-importable input (a cross-workflow sub-workflow\nreference, a dynamic workflow target, a mid-flow `respondToWebhook`) with a\n`[reason] message` — relay that to the user; do not fake a partial scaffold.\n\nFor a directory of exports, run it per file.\n\n### 2 — Triage what it left\n\nEvery unmapped node is a stub that throws `… — implement me`. Classify each by its\nJSDoc marker and route to the matching reference:\n\n| Stub marker / signal | Handle via |\n|---|---|\n| `STUB — generated from n8n node \"…\" (type \"n8n-nodes-base.<svc>…\")` | `references/addon-mapping.md` |\n| `STUB — generated from n8n Code node \"…\"` | `references/code-translation.md` |\n| A `control` stub — Loop Over Items / **splitInBatches**, Switch expr-mode | `references/loops-and-control.md` |\n| `STUB — … vector-store … #902` | rare now (RAG ships as `<store>:query`/`:ingest`); a residual one = an unmapped store → report it, don't guess |\n| Importer `diagnostics` (already exited 1) | explain the reason; the workflow is un-importable as-is |\n\nRead a reference file only when you actually hit that stub class.\n\n### 3 — Fill each stub\n\nWork the manifest + stub files per the routed reference. The mechanical classes\n(addon, code) are near-deterministic; the loop/control class needs judgment\n(map vs reduce, done-branch semantics) — reference `loops-and-control.md` tells you\nwhen to decide vs ask.\n\n### 4 — Report missing integrations (first-class output)\n\nAn addon stub can only be wired to an **installed** `@pikku/addon-*`. When the\npackage for an n8n service is not in `dependencies`, do not guess a lookalike —\ncollect it. Give the user one upfront list:\n\n```\nMissing integrations — install these or the nodes stay stubs:\n • slackTool \"Post to channel\" → npm i @pikku/addon-chat-slack\n • hubspot \"Create contact\" → no @pikku/addon-hubspot exists yet\n```\n\n### 5 — Verify it works\n\n1. `pikku all` (regenerate) → `yarn tsc` from the package root; fix the source\n cause of any error and rerun.\n2. Grep the emitted functions for any surviving `— implement me`\n / `throw new Error('Stub:`. **Any survivor means the import is not done** —\n list them by node name.\n3. Green tsc **and** zero surviving stubs = success.\n\n## References\n\n| Open when you need to… | Read |\n|---|---|\n| map an integration stub (gmailTool, slackTool, googleSheets, plain action nodes) to an installed addon `ref(...)` | `references/addon-mapping.md` |\n| translate an n8n Code node body into a Pikku function body | `references/code-translation.md` |\n| lower a Loop Over Items / splitInBatches loop, or a Switch that stayed a stub | `references/loops-and-control.md` |\n\n## Final summary\n\nReport, terse:\n\n- Files written and workflow shape (`pure-graph` / `agent`).\n- Stubs filled, by class.\n- **Missing integrations** (the step-4 list) — the thing the user must act on.\n- Anything left as a `// TODO:` and why (credentials to wire, a loop deferred, an\n unmapped store).\n- Verification: `tsc` status + surviving-stub count (must be 0).\n", "pikku-n8n-import/SPEC.md": "# n8n → Pikku Import Specification\n\n## Intent\n\nTake an n8n workflow JSON export all the way to a compiling, stub-free, runnable\nPikku workflow. The `@pikku/n8n-import` package is treated as **frozen**: it does\nthe provable mechanical conversion and leaves everything it cannot prove as a typed,\nthrowing stub. This skill owns the end-to-end flow around it — run it, fill the\nremainder with judgment, report gaps that need a human decision, and verify.\n\n## Scope\n\nIn scope:\n- Running `pikku import n8n` and triaging its output.\n- Filling integration stubs (→ addon refs), Code stubs (→ function bodies), and\n loop/control stubs (→ `graph:map`/reduce/branch).\n- Reporting missing `@pikku/addon-*` integrations as a first-class output.\n- Verifying via `pikku all` + `tsc` + a zero-surviving-stub check.\n\nOut of scope:\n- Extending `@pikku/n8n-import` itself (it is frozen; do not add per-service tables\n or new compiler rules to it).\n- Authoring workflows from scratch (`pikku-workflow`) or hand-written addon wiring\n unrelated to an import (`pikku-addon`).\n- Inventing batching primitives or guessing ambiguous loop semantics — surface them.\n\n## Users And Trigger Context\n\n- Primary users: developers importing their own n8n workflows into a Pikku app,\n usually inside an agentic session.\n- Common requests: \"import this n8n workflow\", \"convert this n8n export to pikku\",\n finishing `— implement me` stubs, wiring a `*.integrations.json` manifest.\n- Should not trigger for: from-scratch workflow authoring, or addon wiring with no\n n8n import involved.\n\n## Runtime Contract\n\n- Required first action: run `pikku import n8n <export.json> [outDir]` (per file for\n a directory); relay any exit-1 diagnostic instead of scaffolding a partial.\n- Required outputs: filled stubs, a missing-integrations list, verification status.\n- Non-negotiable: never hand-edit generated files to hide a stub; map only to\n installed addons; zero surviving `— implement me` stubs at success.\n- Bundled files loaded at runtime: `references/addon-mapping.md`,\n `references/code-translation.md`, `references/loops-and-control.md` — each only\n when its stub class appears.\n\n## Source And Evidence Model\n\nAuthoritative sources:\n- `@pikku/n8n-import` codegen (stub markers, manifest shape, `import-n8n` command).\n- `@pikku/addon-graph` function contracts (`graph:map`/`fanout`, `branch`).\n- Installed `@pikku/addon-*` source (function names verified by grep, never guessed).\n\nUseful improvement sources: real imported workflows, harness coverage deltas,\naddon catalogue changes.\n\nData that must not be stored: credential secrets, customer data, private ids beyond\nwhat a manifest already records for reproduction.\n\n## Reference Architecture\n\n- `SKILL.md`: the run → triage → fill → report → verify workflow + router.\n- `references/`: per-stub-class depth (addon mapping, code translation, loops/control).\n\n## Validation\n\n- Lightweight: `yarn tsc` from the package root after each fill.\n- Deeper: `pikku all` regeneration; grep emitted functions for surviving stub throws.\n- Acceptance gates: green tsc **and** zero surviving `— implement me` stubs.\n\n## Known Limitations\n\n- `splitInBatches` with `batchSize > 1` and reduce-style accumulators have no direct\n primitive — surfaced, not auto-converted.\n- Missing addons block their nodes; the skill reports, it does not install.\n- Cross-workflow sub-workflow references fail import at the package level.\n\n## Maintenance Notes\n\n- Update `SKILL.md` when the import command, stub taxonomy, or verify gates change.\n- Update a reference when an addon convention, the `graph:map` contract, or a rubric\n changes.\n- This skill supersedes the former `pikku-n8n-addon-map` and `pikku-n8n-code-translate`\n skills (folded into `references/addon-mapping.md` and `references/code-translation.md`).\n", "pikku-paraglide/SKILL.md": "---\nname: pikku-paraglide\ndescription: 'Generate typed, static enum-label maps for a Paraglide i18n frontend with `@pikku/paraglide`, and reconcile them against the database enum columns so a label can never silently drift from a DB value. Enum-valued labels live under a reserved `enum__<group>__<member>` message namespace; the generator emits `i18n-enum.gen.ts` typed `satisfies EnumLabel<DbEnum>`. TRIGGER when: labelling an enum/status/kind/role value in a Paraglide app, replacing a dynamic `mKey(...)`/`m[...]` lookup with a static map, wiring `@pikku/paraglide` into Vite, or reconciling i18n against `CHECK (col IN (...))` / Postgres enum columns. DO NOT TRIGGER for plain free-text UI copy (that is a normal `m.some_key()` message), backend errors, or logs.'\ninstallGroups: [core]\n---\n\n# Pikku Paraglide enum labels\n\n## Agent Operating Procedure\n\nUse this as an execution checklist, not reference material.\n\n1. **Is the value an enum (a closed set — a status/kind/role/tag from a DB column or a fixed union)?** Then its label is a static map entry, never a dynamic lookup. Add `enum__<group>__<member>` keys to the catalog (`messages/en.json`) and read the value through the generated map: `group[value]()`.\n2. **Is the value free text or a one-off literal** (a heading, a button, a never-indexed label)? Then it is a normal Paraglide message — call `m.<key>()` directly. Do **not** invent an enum group for something that is always a literal.\n3. **Wire the generator** (`@pikku/paraglide/vite` or the CLI) so `i18n-enum.gen.ts` is regenerated from the catalog, and point it at the DB enums module (`enums.gen.ts`) so the maps are typed against the database.\n4. **Validate with the app's own `tsc`.** Reconciliation is enforced purely by types: a missing key or a dropped DB member is a compile error, and the deploy gate runs `tsc` before building. A clean `vite build` alone does not type-check.\n\n## The rules that don't change\n\n- **Never resolve an enum key dynamically.** No `mKey('status.' + value)`, no `m['enum__status__' + value]()`, no `mExists`/`mList` helpers. Dynamic keys can't be type-checked or tree-shaken. Everything is a static `m.<literal>()` reference, generated into the map.\n- **The `enum__<group>__<member>` namespace.** `__` separates the prefix / group / member segments; a single `_` joins words *within* a segment (`enum__booking_status__form_received`). The prefix (`enum`) and separator (`__`) are configurable but leave them at the defaults.\n- **Members must be valid JS identifiers.** Spell out leading digits — `two_guests`, not `2_guests`. The generator quotes an invalid member as a fallback but warns you to rename it.\n- **`asI18n(...)` is only for opaque server data** (names, slugs, ids returned from the API). Never `asI18n()` a hardcoded English string or an enum value — an enum value goes through its label map.\n\n## The generated module\n\n`i18n-enum.gen.ts` is **AUTO-GENERATED — do not edit.** It exports, per enum group:\n\n```ts\nimport { m } from './messages.js'\nimport type { I18nString } from '@pikku/react'\nimport type { BookingStatus } from '#pikku/db/enums.gen' // when reconciled\n\nexport type I18nMessage = () => I18nString\nexport type EnumLabel<E extends string> = Record<E, I18nMessage>\n\nexport const bookingStatus = {\n enquiry: m.enum__booking_status__enquiry,\n reserved: m.enum__booking_status__reserved,\n confirmed: m.enum__booking_status__confirmed,\n} satisfies EnumLabel<BookingStatus>\nexport type BookingStatusKey = keyof typeof bookingStatus\n```\n\n- Each value is an `I18nMessage` — a `() => I18nString` accessor. **Call it at render time** so the label tracks the active locale.\n- App code: `import { bookingStatus } from './i18n/i18n-enum.gen'` then `bookingStatus[value]()`.\n- For an open server value, gate it: `value in bookingStatus ? bookingStatus[value as BookingStatusKey]() : asI18n(value)`.\n\n### Module-scope hazard — store the accessor, don't call it\n\nA label used in a config built at module load (nav items, column defs) must hold the **accessor**, not the result — calling `m.foo()` at module scope freezes the label to the locale that was active at import:\n\n```ts\n// nav.config.ts\nconst items = [{ label: m.common__nav__items__dashboard /* ← reference */ }]\n// at render: <span>{item.label()}</span> // ← call here\n```\n\n## Reconciliation against the database\n\nThe DB column is the real source of truth for what an enum can be. The pikku CLI's db codegen emits a bare unions module — `.pikku/db/enums.gen.ts` — covering **both** Postgres native enums and SQLite `CHECK (col IN ('a','b',…))` constraints:\n\n```ts\nexport type BookingStatus = 'enquiry' | 'reserved' | 'confirmed' | 'ended' | 'cancelled'\n```\n\nPoint `@pikku/paraglide` at that file (`enumsFile`) and each catalog group whose member set **exactly matches** a DB enum is typed `satisfies EnumLabel<DbEnum>`. The label map then **is** the reconciliation — no separate assertion:\n\n- catalog drops a DB member, or `en.json` is missing the key → `m.enum__…` doesn't exist / `Record<DbEnum,…>` isn't exhaustive → **`tsc` error naming the gap**.\n- a DB enum with **no** catalog group → `unmatchedDbEnums: 'emit'` (default) generates a label map referencing `enum__<table>_<column>__<member>` keys, so `tsc` tells you exactly which keys to add; `'warn'` only reports it.\n- a group with a member the DB lacks (a *derived* UI state, e.g. a `waitlisted` view of a `pending` row) → a drift warning. Make that a **standalone `m.<key>()` message**, not an enum member — the enum group must mirror the DB column exactly.\n\nLabelling an enum that's never rendered costs nothing: Paraglide compiles only the messages actually referenced, so unused labels are tree-shaken away. So label every DB enum; don't add an opt-out.\n\n**To make a column an enum**, give it a closed domain in the migration so codegen can see it:\n- SQLite: `status TEXT NOT NULL CHECK (status IN ('enquiry','reserved','confirmed'))`\n- Postgres: a native `CREATE TYPE … AS ENUM (…)` column.\n\n## Wiring\n\n### Vite (dev + build)\n\nPlace `paraglideEnums` **after** `paraglideVitePlugin` (the generated file imports the compiled `m`). It regenerates on catalog/enums edits and only writes on change, so it never loops HMR.\n\n```ts\nimport { paraglideVitePlugin } from '@inlang/paraglide-js'\nimport { paraglideEnums } from '@pikku/paraglide/vite'\n\nexport default defineConfig({\n plugins: [\n paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide' }),\n paraglideEnums({\n catalog: './messages/en.json',\n outFile: './src/i18n/i18n-enum.gen.ts',\n enumsFile: './packages/functions/.pikku/db/enums.gen.ts', // reconcile against the DB\n // enumsImport: '#pikku/db/enums.gen', // explicit specifier; defaults to a relative path\n }),\n ],\n})\n```\n\n### CLI (CI / non-Vite), run right after `paraglide-js compile`\n\n```sh\n# paraglide-enums <catalog.json> <out.gen.ts> [messagesImport] [enums.gen.ts]\nparaglide-enums ./messages/en.json ./src/i18n/i18n-enum.gen.ts ./messages.js ./packages/functions/.pikku/db/enums.gen.ts\n```\n\n`i18n-enum.gen.ts` is generated — gitignore it once the plugin/CLI runs in the build.\n\n## What NOT to do\n\n- Don't write `mKey`/`mList`/`mExists` or any `m[expr]()` dynamic lookup — every enum label is a static generated reference.\n- Don't introduce a literal-key indirection helper (`k('approve_enquiry')`); a literal is `m.approve_enquiry()` directly.\n- Don't call `m.foo()` at module scope for config built at import time — store `m.foo` and call it at render.\n- Don't put an extra UI-only member into an enum group to match a derived state — make it a standalone message and keep the group an exact mirror of the DB column.\n- Don't hand-edit `i18n-enum.gen.ts` or `enums.gen.ts` — fix the catalog / the migration and regenerate.\n", "pikku-permissions/SKILL.md": "---\nname: pikku-permissions\ndescription: >-\n Use when adding authorization checks to Pikku functions — pikkuPermission, pikkuAuth,\n per-function permissions, global permissions, or understanding OR/AND permission logic.\n TRIGGER when: user wants to restrict who can call a function, check resource ownership, add\n role-based access, or understand where permission checks belong. DO NOT TRIGGER when: user asks\n about middleware or request interception (use pikku-middleware), authentication strategies (use\n pikku-security), or session management.\ninstallGroups: [core]\n---\n\n# Pikku Permissions\n\n## The Rule\n\n**ALWAYS put authorization checks in the `permissions` field of `pikkuFunc` or `pikkuSessionlessFunc` — NEVER inside the `func` body.**\n\nThis includes: org access checks, repo access checks, role checks, resource ownership, and any other authorization logic. The `permissions` field runs before `func`, is visible to the inspector, and is the only place Pikku enforces authorization.\n\n```typescript\n// CORRECT\nexport const deleteBook = pikkuFunc({\n func: async ({ db }, { bookId }) => {\n await db.deleteBook(bookId)\n },\n permissions: {\n owner: isBookOwner, // ← authorization here\n },\n})\n\n// WRONG — permission check inside func body\nexport const deleteBook = pikkuFunc({\n func: async ({ db }, { bookId }, { session }) => {\n if (!session) throw new UnauthorizedError() // ← never do this\n await db.deleteBook(bookId)\n },\n})\n```\n\n## Agent Operating Procedure\n\n1. Discover before editing. Run `pikku info permissions --verbose` and `pikku info functions --verbose` to understand what permissions are already defined and applied.\n2. Define permission checkers in a `src/permissions.ts` or domain-specific `src/lib/*-permissions.ts` file.\n3. Apply them via the `permissions` field on the function. For an app-wide baseline that every function must additionally satisfy, use `addGlobalPermission`.\n4. Validate: run `pikku all --tsc` to confirm permission checker signatures are correct.\n\n## Permission Factories\n\n### `pikkuAuth(fn)` — Session-Only Checks\n\nUse for checks that read the session but need no request data — and that assert\nsomething **beyond** merely having a session (a flag, a tier, a claim).\n\n```typescript\nimport { pikkuAuth } from '#pikku'\n\n// Good: a real gate on the session's contents, not just its existence.\nexport const isVerified = pikkuAuth(\n async (_services, session) => !!session?.emailVerified\n)\n```\n\n**Do NOT write an \"is signed in\" permission.** A checker that just returns\n`!!session` is not authorization — it re-checks authentication, which the\nfunction already enforces. A function that needs a signed-in user sets\n`auth: true` (the default for `pikkuFunc`); it does not also carry a\n`permissions: { signedIn }`.\n\n```typescript\n// WRONG — redundant with auth: true; adds a permission that gates nothing.\nexport const isSignedIn = pikkuAuth(async (_s, session) => !!session)\npikkuFunc({ auth: true, permissions: { signedIn: isSignedIn }, /* ... */ })\n\n// RIGHT — auth: true already requires the session; permissions are for capability.\npikkuFunc({ auth: true, /* ... */ })\n```\n\nA permission answers \"*may this user do this?*\" (role, ownership, tier) — never\n\"*is there a session?*\".\n\n### `pikkuPermission(fn)` — Data-Aware Checks\n\nUse when authorization depends on the actual request data (e.g., resource ownership).\n\n```typescript\nimport { pikkuPermission } from '#pikku'\n\nexport const isBookOwner = pikkuPermission(\n async ({ db }, { bookId }, { session }) => {\n const book = await db.getBook(bookId)\n return book?.authorId === session?.userId\n }\n)\n\nexport const hasBookAccess = pikkuPermission(\n async ({ db }, { bookId }, { session }) => {\n return await db.hasAccess(session?.userId, bookId)\n }\n)\n```\n\n## OR / AND Logic\n\n```typescript\npermissions: {\n verified: isVerified, // OR: verified users can access\n owner: isBookOwner, // OR: owners can access\n reviewer: [isAuthenticated, hasBookAccess], // AND: both must pass\n}\n// Logic: verified OR owner OR (isAuthenticated AND hasBookAccess)\n```\n\nGroups are OR'd. Entries within a group array are AND'd.\n\n## Where to Apply Permissions\n\n### Per-Function (preferred)\n\n```typescript\nexport const deleteBook = pikkuFunc({\n func: async ({ db }, { bookId }) => {\n await db.deleteBook(bookId)\n },\n permissions: {\n verified: isVerified,\n owner: isBookOwner,\n },\n})\n```\n\n### Global (`addGlobalPermission`) — App-Wide AND Gate\n\nA global permission is an app-wide baseline that **every** function must additionally pass. It is an independent AND gate: it can only ever *narrow* access — it never grants access a function's own `permissions` would deny.\n\n```typescript\nimport { addGlobalPermission } from '.pikku/pikku-types.gen.js'\n\naddGlobalPermission([signedInUser]) // every function now also requires a session\n```\n\nMultiple `addGlobalPermission` calls accumulate and are AND'd together.\n\n> Wire-, tag-, and HTTP-route-level permissions (`addHTTPPermission`, `addTagPermission`, and a `permissions` field on HTTP/channel/MCP wirings) were **removed in #972**. Permissions now live only on the function definition, plus the optional global gate. Tags are organizational only — use tag/HTTP *middleware* (`addTagMiddleware`, `addHTTPMiddleware`) for cross-cutting request handling, not authorization.\n\n## The Two Gates\n\nAuthorization is two independent gates, both of which must pass:\n\n1. **Global permissions** (`addGlobalPermission`) — AND'd together. A broad baseline that can only narrow access.\n2. **The function's own `permissions`** — OR'd groups (OR-of-ANDs), as above.\n\nThe gates are independent: a broad global (e.g. `signedInUser`) can **never** satisfy an admin-only function's own requirement. Each function still enforces its own `permissions` in full.\n\n## Complete Example\n\n```typescript\n// src/permissions.ts\nimport { pikkuAuth, pikkuPermission } from '#pikku'\n\nexport const isAuthenticated = pikkuAuth(\n async (_services, session) => !!session\n)\n\nexport const isVerified = pikkuAuth(\n async (_services, session) => !!session?.emailVerified\n)\n\nexport const isOrgMember = pikkuPermission(\n async ({ db }, { orgId }, { session }) => {\n return await db.isMember(session?.userId, orgId)\n }\n)\n\n// src/functions/org.function.ts\nexport const deleteOrg = pikkuFunc({\n func: async ({ db }, { orgId }) => {\n await db.deleteOrg(orgId)\n },\n permissions: {\n verified: isVerified,\n owner: [isAuthenticated, isOrgMember],\n },\n})\n```\n\n## After Changes\n\n```bash\npikku all # regenerate if wirings changed\npikku all --tsc # regenerate, then verify permission checker types (fails on type errors)\n```\n", "pikku-pino/SKILL.md": "---\nname: pikku-pino\ndescription: >-\n Use when setting up structured logging with Pino in a Pikku app. Covers PinoLogger setup and log\n levels. TRIGGER when: code uses PinoLogger, user asks about structured logging, Pino, or\n @pikku/pino. DO NOT TRIGGER when: user asks about ConsoleLogger (use pikku-services) or general\n service setup.\n---\n\n# Pikku Pino (Structured Logging)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/pino` provides structured JSON logging via [Pino](https://getpino.io/). Implements the `Logger` interface from `@pikku/core`.\n\n## Installation\n\n```bash\nyarn add @pikku/pino\n```\n\n## API Reference\n\n### `PinoLogger`\n\n```typescript\nimport { PinoLogger } from '@pikku/pino'\n\nconst logger = new PinoLogger()\n```\n\nNo constructor parameters. Creates a Pino logger instance.\n\n**Properties:**\n\n- `pino: pino.Logger` — Access the underlying Pino instance for advanced config.\n\n**Methods:**\n\n- `setLevel(level: LogLevel): void` — Set minimum log level.\n- `info(messageOrObj: string | Record<string, any> | Error): void`\n- `warn(messageOrObj: string | Record<string, any> | Error): void`\n- `error(messageOrObj: string | Record<string, any> | Error): void`\n- `debug(messageOrObj: string | Record<string, any>): void`\n\n## Usage Patterns\n\n### Basic Setup\n\n```typescript\nimport { PinoLogger } from '@pikku/pino'\n\nconst logger = new PinoLogger()\nlogger.setLevel('debug')\n```\n\n### With Pikku Services\n\n```typescript\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n return { config, logger }\n})\n```\n\n### Accessing Underlying Pino\n\n```typescript\nconst logger = new PinoLogger()\nlogger.pino.child({ module: 'auth' }).info('Token verified')\n```\n", "pikku-product-second-opinion/example/sample-report.md": "# Your app, in plain English — and where it could get better\n*A second opinion on the competitor-tracking system*\n\n> Worked example for the pikku-product-second-opinion skill. Shows the voice and the\n> layered structure on one real area (competitor tracking), drawn from a\n> pikku-software-archaeology blueprint + parity report. A full report would repeat\n> Part 2 for each major area, and cover every significant technology bet — not just\n> the one shown here.\n\n**How to read this:** no technical background needed. I'll explain what you have,\nwhat's solid, and what I'd change — and for each change, what it costs and what\nit buys you.\n\n## Part 1 — The short version\n\n**What you have.** A competitive-intelligence app: it watches your competitors'\nwebsites, spots meaningful changes (pricing, hiring, product updates), summarizes\nthem, and feeds your briefings and dashboards so your team knows first.\n\n**The headline.**\n- The hard part — reading messy websites and telling a real change from noise — is built well.\n- Until recently the app wasn't re-checking sites on its own at all *(now fixed)*.\n- When it does spot a change, the follow-up work only happens if someone clicks a button — so your dashboards can quietly go stale while looking current.\n\n**If it were me, this is the order I'd tackle things:**\n\n| Fix | Why it matters to you | Effort | Payoff |\n|---|---|---|---|\n| Turn on automatic checking | Sites weren't refreshing themselves | *Done* | High |\n| Make the follow-up automatic | Stops your intelligence going stale unnoticed | Medium | High |\n| Make failures visible | Problems surface instead of hiding | Small | Medium |\n\n---\n\n## Part 2 — Area by area\n\n### Competitor tracking\n\n**What this does.** Watches your competitors' sites for you and turns meaningful\nchanges into summaries your team can act on.\n\n**How it works today.** Like a clipping service: on a timer, the app re-reads\neach competitor's site, compares it to last time, decides whether anything\n*meaningful* changed (it ignores trivial edits), and writes up a summary when\nsomething real happens.\n\n**What's working.** The expensive, valuable part is solid — the app is genuinely\ngood at reading messy sites, separating real changes from noise, and summarizing\nthem. Keep it.\n\n**What's holding you back.**\n- **The automatic checking wasn't switched on.** The machinery existed but nothing\n pulled the trigger, so sites weren't refreshing on their own. What it means for\n you: your \"live\" intelligence wasn't live. Severity: Urgent. Effort: Small.\n *(Already fixed.)*\n- **The follow-up is manual.** When a change is found, updating your briefings and\n comparisons doesn't happen on its own — someone has to click \"regenerate.\" What\n it means for you: if nobody clicks, the dashboard shows old information while\n looking up to date, and you can't trust it. Severity: Serious. Effort: Medium.\n\n**How I'd do it differently — and why it's worth it.** Make the whole chain\nfinish as one task: when a change is found, the briefings and comparisons update\nautomatically as part of the same job, so \"done\" means \"your intelligence is\nactually current.\" And a failed step should show as a visible error, not vanish.\nThis is a **rewire, not a rebuild** — the valuable machinery stays; I'm connecting\npieces that already sit next to each other. Outcome: **more reliable, fewer\nsurprises** — which is the entire promise of the product.\n\n---\n\n### The technology bets\n\n**Pikku — the framework I'm suggesting you rebuild onto.** *Buys you:* one way to\nwrite a capability and drive it from anywhere — web, timers, background jobs,\nassistants — so the tracking rule is written once instead of three times, which is\nexactly the sprawl above. *Costs you:* it hasn't shipped a stable 1.0 (it's 0.12.x;\n0.13 is the first release promising backwards compatibility), so until then\nupgrades can break you — pin the version and budget for upgrade work. Its community\nand hiring pool are far smaller than the mainstream default's. That's normal for a\nyoung framework and survivable, but it's a real cost and it's yours to weigh.\n*Usually:* worth it when the problem is genuinely sprawl, as it is here — and worth\nwaiting if nobody has capacity to own upgrades.\n\n---\n\n## Part 3 — The fine print\n\n**How confident am I.** High on both problems — I can see them directly. The fix\nis a well-understood pattern rather than an experiment, though I'd want one live\ntest against your real data before calling it done.\n", "pikku-product-second-opinion/README.md": "# pikku-product-second-opinion\n\nTurns a `pikku-software-archaeology` blueprint into a **plain-language report for a\nnon-technical owner** — a founder/PM stuck with an app they didn't build.\nExplains how it works and how it could be better, in business terms.\n\n```\nExisting repo → pikku-software-archaeology → .knowledge/ blueprint → pikku-product-second-opinion → founder report\n (facts) (extract) (machine-readable) (translate + advise) (markdown + web page)\n```\n\n## The split from pikku-software-archaeology\n\n- **pikku-software-archaeology** extracts *facts* into `.knowledge/` for a machine (Pikku) to rebuild from. Engineer/generator audience.\n- **pikku-product-second-opinion** reads that blueprint and writes an *opinionated report* for a human to decide from. Non-technical audience.\n\nOne extracts; one advises. This skill consumes the other's output — it doesn't re-read the code.\n\n## What the report is calibrated to (locked by the author)\n\n- **Layered depth** — a one-page executive summary, then a section per major area for anyone who wants detail.\n- **Direct but fair tone** — names problems plainly, always with why-it-matters and credit for what's good.\n- **Both formats** — a markdown copy in the repo plus a clean, shareable web page (rendered via the `artifact-design` skill).\n\n## The rules that make it work\n\n1. **Translate, don't dump.** Every technical concept becomes a business outcome or a plain description. The jargon→plain table is in `SKILL.md`.\n2. **Every problem carries impact + severity + effort.** A problem with no \"what it means for you\" doesn't ship.\n3. **Always credit what works.** All-criticism reports get dismissed.\n4. **Argue improvements in business outcomes** (more reliable / faster / cheaper / safer / easier to hand off), and say whether each is a cheap **rewire** or an expensive **rebuild** — never recommend a rewrite just because the code is messy.\n5. **Mark confidence.** Certain and \"I'd need to check\" are different sentences.\n6. **Cover the frontend and the other ways the app is used** when the blueprint has them — walk the screens as a journey, call out consistency, and flag the custom-logic pieces (charts/tables/editors) as the real work vs the cheap standard pieces. Name the ways the product can be driven (people/web, developers/API+SDK, AI agents/MCP, power users/CLI) — often a genuine strength.\n7. **Give honest technology tradeoffs — both sides.** Every stack bet (framework, auth, hosting, key libraries) gets what-it-buys AND what-it-costs in business terms, tied to the founder's stage/goals. Don't cheerlead, don't trash, and **don't soften the disadvantages**. The app's own bets are derived from the blueprint (`architecture.json`/`integrations.json`/`frontend.json` + the repo's manifest) — never from a list in the skill, because a verdict you could write before reading the blueprint isn't a second opinion. Separately, and only when a rebuild is actually being recommended, the target stack (Pikku, Better Auth, TanStack Start) gets the *same* both-sides treatment with cons first-class — pinning someone's dependency for being pre-1.0 while staying quiet about the replacement being pre-1.0 too is a pitch, not an opinion.\n\n## Files\n\n```\npikku-product-second-opinion/\n├── SKILL.md # method + voice rules + red flags\n├── README.md # this file\n├── references/report-template.md # the layered structure to fill in\n└── example/sample-report.md # worked example (competitor-tracking area, founder voice)\n```\n", "pikku-product-second-opinion/references/report-template.md": "# Report template\n\nFill this in. Keep sentences short. Lead every point with what it means for the\nreader. Delete any section that would be empty rather than padding it.\n\n---\n\n# {App name}, in plain English — and where it could get better\n*A second opinion on {scope: the whole app / the competitor-tracking system / …}*\n\n**How to read this:** no technical background needed. Part 1 is the summary — if\nyou read nothing else, read that. Parts 2–3 go area by area for anyone who wants\ndetail.\n\n## Part 1 — The short version\n\n**What you have.** {2–3 sentences: what the product does, who uses it.}\n\n**The headline.** {3–5 bullets, one plain line each — the biggest risks and\nopportunities. No jargon.}\n\n**If it were me, this is the order I'd tackle things:**\n\n| Fix | Why it matters to you | Effort | Payoff |\n|---|---|---|---|\n| {…} | {business impact} | Small/Medium/Large | High/Medium/Low |\n\n---\n\n## Part 2 — Area by area\n\n### {Area name, in business terms — e.g. \"Competitor tracking\"}\n\n**What this does.** {the capability, as the business experiences it}\n\n**How it works today.** {a plain walkthrough — a small story beats a diagram}\n\n**What's working.** {genuine credit — the parts that are solid and worth keeping}\n\n**What's holding you back.**\n- **{Problem in plain terms}.** What it means for you: {business impact}.\n Severity: {Minor / Worth fixing / Serious / Urgent}. Effort to fix: {Small /\n Medium / Large}.\n\n**How I'd do it differently — and why it's worth it.** {the better design in\noutcomes: more reliable / faster to change / cheaper / safer / easier to hand\noff. Say whether it's a cheap rewire or an expensive rebuild.}\n\n{repeat per area}\n\n### The technology bets\n\n{One entry per significant choice — framework, sign-in, hosting, database, key\nlibraries — AND anything a rebuild would move them ONTO. Each gets both sides.}\n\n**{Technology}.**\n- *Buys you:* {in business terms}\n- *Costs you:* {in business terms — bills, hiring, shipping speed, upgrade work,\n the risk of betting on something young. Don't soften it. If it hasn't shipped a\n stable 1.0, say so and say what that means: pin the version, budget upgrades.}\n- *Usually:* {recommendation tied to their stage — normally \"keep it, watch this\"}\n\n{The same bar applies to anything you're recommending they move to. A stack you\npropose with no cons listed is a pitch, not a second opinion.}\n\n---\n\n## Part 3 — The fine print (optional)\n\n**How confident am I.** {where you're certain vs guessing; what you'd verify\nagainst real data before committing}\n\n**A few words explained.** {glossary — only terms that couldn't be avoided}\n", "pikku-product-second-opinion/SKILL.md": "---\nname: pikku-product-second-opinion\ndescription: 'Use when a non-technical owner (founder, PM, operator) wants a plain-language report on an app they hold but did not build — explaining how it works and how it could be better. Reads the .knowledge/ blueprint from pikku-software-archaeology and produces a layered, jargon-free report that credits what works, names what does not (with business impact + effort), and argues an opinionated better design. TRIGGER: \"explain how my app works\", \"what would you do differently\", \"review my app for a non-technical audience\", \"I inherited/am stuck with an agency-built app\", \"is this built well?\". DO NOT TRIGGER for: extracting the machine-readable blueprint itself (use pikku-software-archaeology), or an engineer-facing technical code review.'\ninstallGroups: [fabric]\n---\n\n# Product Second Opinion\n\n## Overview\n\nTurn an extracted product blueprint into a **report a non-technical owner can act on**. Two jobs, in one voice: (1) explain, in plain language, how the app they're stuck with actually works; (2) give an honest, opinionated second opinion — what's solid, what's holding them back, and how you'd build it better, argued in business outcomes, not architecture.\n\nThe reader is a founder/PM/operator, not an engineer. If they finish a section and don't know what it means for their business or what to do about it, the report failed — no matter how correct it is.\n\n**REQUIRED INPUT:** the `.knowledge/` blueprint produced by **pikku-software-archaeology**. If none exists, run that skill first — this one consumes its output (`product.json`, `domains.json`, `workflows.json`, `gaps.json`, `invariants.json`, `migration.json`, and any `parity-*.md`), it does not re-derive facts from the code. When the optional consumer-surface files are present (`interfaces.json`, `frontend.json`, `frontend-routes.json`, `frontend-components.json`), cover them too — see \"The frontend and the other ways your app is used\" and \"Technology choices\" below.\n\n## The cardinal rule: translate, don't dump\n\nEvery technical concept becomes a business outcome or a plain-language description. Never make the reader learn your vocabulary. If a term is unavoidable, define it in one clause the first time — but prefer describing the *effect* and skipping the term entirely.\n\n| Don't write | Write instead (describe the effect) |\n|---|---|\n| queue / worker / job | \"a background task that runs on its own\" |\n| workflow | \"a multi-step task that resumes where it left off if interrupted\" |\n| API / endpoint / route | \"something the app (or another tool) can ask it to do\" |\n| webhook | \"an automatic message the app sends to another tool when something happens\" |\n| event | \"a signal that something happened, that other parts can react to\" |\n| cron / scheduler | \"a timer that runs something on a schedule\" |\n| schema / migration | \"the shape of your stored data\" / \"a change to how data is stored\" |\n| auth / session / token | \"how the app knows who you are and what you're allowed to do\" |\n| refactor / rewire | \"reorganizing the inside without changing what it does\" |\n| cache | \"a saved copy kept around for speed\" |\n| race condition | \"two things happening at once and stepping on each other\" |\n| component | \"a reusable piece of the screen (a button, a chart, a table)\" |\n| route / page | \"a screen in the app\" |\n| design system / component library | \"the shared kit of screen pieces that keeps everything looking consistent\" |\n| SSR / SPA / rendering | \"how pages get built and shown\" (only mention if it affects speed or SEO) |\n| MCP server | \"a way for AI assistants to use your app's data and actions directly\" |\n| SDK | \"a ready-made toolkit so other developers can build on your app\" |\n| CLI | \"a way to drive the app by typing commands (for power users / automation)\" |\n| theme token / design variable | \"a single setting (like your brand color) reused everywhere, so you change it once\" |\n| modal / drawer | \"a pop-up box\" / \"a slide-out panel\" |\n\nWhen in doubt, say what the *user or the business* experiences, not what the machine does.\n\n## Report structure (layered — skim or dive)\n\nWrite these three parts in order. A reader can stop after Part 1.\n\n**Part 1 — Executive summary (one page).**\n- *What you have*: 2–3 sentences — what the product does and who uses it.\n- *The headline*: the 3–5 biggest risks/opportunities, one plain line each.\n- *Recommended order*: a table (Fix | Why it matters | Effort | Payoff). This is the part they act on.\n\n**Part 2 — One section per major area** (drive the areas from `domains.json`; skip domains with nothing worth saying). Each section follows this shape (see `example/sample-report.md`):\n- *What this does* — the capability in business terms.\n- *How it works today* — a plain walkthrough, ideally as a small story (\"on a timer, the app re-reads each site, compares…\").\n- *What's working* — genuine credit. Never skip this; a report that's all criticism gets dismissed.\n- *What's holding you back* — each problem MUST carry: **what it means for you** (business impact), **severity** (Minor / Worth fixing / Serious / Urgent), and **effort** (Small / Medium / Large).\n- *How I'd do it differently — and why it's worth it* — the opinionated part. Argue the improvement in one of these business outcomes: **more reliable / fewer surprises**, **faster to add features**, **cheaper to run**, **safer / less risk**, **easier to maintain or hand off**. Be explicit whether it's a cheap rewire or an expensive rebuild.\n\n**Part 3 — Appendix.**\n- *How confident am I* — REQUIRED. Where you're certain vs guessing; what you'd verify against real data first. The blueprint carries confidence tiers — anything you're relaying from a `low`/`medium` entry, or from a reconstructed (`explicit: false`) event, says so here.\n- *Glossary* (optional) — only for any term that slipped through.\n\n## Rewire vs rebuild (say which)\n\nThe blueprint's `migration.json` tells you which is which — `mappings[]` is what survives (each with its `recommendation`), `dropped[]` is what goes. The reader needs to know because the cost is 10× different.\n- **Rewire** — the valuable machinery exists; you're connecting pieces or turning something on. Cheap, low-risk. (Most \"it should be automatic but isn't\" findings are this.)\n- **Rebuild** — the capability doesn't exist or is fundamentally wrong. Expensive, risky. Reserve the word for when it's true; founders hear \"rewrite\" and panic or overspend.\n\nNever recommend a full rewrite because the code is messy. Messy-but-working is a rewire-over-time story, not a bonfire.\n\n## The frontend and the other ways your app is used\n\nWhen the blueprint has the consumer-surface files, add these to the report — they're often where a founder's questions actually live (\"why does the app feel inconsistent?\", \"can partners build on this?\").\n\n**The screens (`frontend-*.json`) — one area section, founder-framed.**\n- *What a user can do* — walk the main screens as a journey, not a component list.\n- *Consistency* — is it built from one shared kit of screen pieces, or a patchwork? A consistent kit means changes are cheap and the app feels coherent; a patchwork means every change is bespoke and the look drifts. Say which, plainly.\n- *The expensive pieces* — this is the key frontend insight. Most of the screen is standard pieces that are cheap to rebuild or restyle. A **small number carry real custom logic** — a bespoke chart, a complicated data table, a drawing/drag interaction, a rich editor. Those are the parts that take real effort to move or change, and the ones most likely to break. Name them, say what they do, and flag them as the real work — so nobody assumes \"it's just screens, it'll be quick.\"\n- *Design consistency (the \"it looks a bit off\" problems)* — from the blueprint's design findings, call out broken patterns in plain terms and, crucially, why each matters and roughly what it costs to fix. Common ones and how to frame them:\n - **The same action behaves differently in different places** (a slide-out panel here, a pop-up box there for the same task). *Why it matters:* the app feels inconsistent and users have to re-learn each screen. *Fix:* pick one pattern and apply it everywhere — cheap.\n - **Colors/spacing are hardcoded instead of set in one place.** *Why it matters:* changing your brand color, or fixing contrast, means hunting through every screen instead of editing one setting — slow and error-prone. *Fix:* move them to shared \"design tokens\" — a small, high-leverage cleanup.\n - **The same element looks different from page to page** (buttons, headings, cards). *Why it matters:* reads as unpolished and erodes trust, especially in a paid product. *Fix:* one shared version of each, reused — cheap and makes every future change faster.\n These are almost always **cheap rewires with an outsized polish/trust payoff**, not rebuilds. Give each an effort (usually Small–Medium) and say the payoff is perceived quality + faster future changes. Do NOT design-nitpick without a reason — every design point needs a \"why it matters to you.\" And credit consistency where the app already has it.\n- Frame rebuild/restyle work as **rewire vs rebuild**: restyling standard pieces to a consistent kit is cheap; re-creating a custom-logic piece is real engineering.\n\n**How your app can be driven (`interfaces.json`) — usually a short, positive section.**\nExplain, in one line each, the ways the product can be used: people through the web, developers through an API or toolkit, AI assistants through a direct connection (MCP), power users through the command line. This is often a genuine strength worth naming — an app that agents and partners can build on is more valuable than one only humans can click. But be honest about `status`: a connection that exists but only does two things is a *start*, not a feature — say so.\n\n## Technology choices — the honest tradeoffs (don't be cheap on the cons)\n\nThe app made specific technology bets. The founder deserves to know what each bet *bought* and what it *costs* — in business terms, tied to their situation (early vs scaling, chasing enterprise deals or not, big team or two people). Present every significant choice as a genuine tradeoff with **both sides**. Never cheerlead a technology, and never trash one — but do not soften the disadvantages to sound positive. A report that only lists upsides is not honest and is not useful.\n\nRules:\n- For each notable choice (framework, auth, hosting, database, key libraries): **what it buys** and **what it costs**, both in plain business terms, then a recommendation tied to *their* stage and goals — usually \"keep it, here's what to watch\" rather than \"switch.\"\n- Tie cons to consequences the founder feels: vendor bills, security/breach liability, hiring difficulty, how fast they can ship, enterprise-sales blockers, the risk of betting on something young.\n- Distinguish \"younger / smaller community\" (a real, manageable risk) from \"wrong choice\" (rare). Most stack choices are defensible; the job is informed eyes-open, not alarm.\n- **Verify before you disparage.** \"Don't be cheap on the cons\" means ACCURATE cons, not invented ones. Do NOT label a technology immature, niche, or feature-poor from vibes, its name, or its age — check its actual adoption, maturity, and feature set first. And separate an **inherent tradeoff of an approach** (e.g. self-hosting anything means you run and secure it) from a **deficiency of a specific tool** (often false — the tool may be mature and full-featured). Overstating cons is as dishonest as hiding them.\n- **Hold your own recommendation to the same bar.** If \"how I'd do it differently\" lands on a specific stack — Pikku included — it gets the same both-sides treatment as everything else, cons first-class. Pinning someone's dependency for being pre-1.0 while not mentioning that the replacement is pre-1.0 too isn't a second opinion, it's a pitch.\n\n- **Derive the app's choices from the blueprint, never from a list in this file.** Read the stack off `architecture.json`, `integrations.json`, `frontend.json`, and the manifest the repo actually has (`package.json`, `Gemfile`, `go.mod`, …). Cover the bets that are *load-bearing for this product*: typically the framework, the auth/identity approach, the datastore, the hosting/deploy model, the payment and other critical vendor integrations, and anything the blueprint marks `replacementDifficulty: hard`. A choice earns a paragraph if switching it would be expensive, or if living with it constrains the business — not because it appears in some canonical list. If you could write the verdict before reading the blueprint, you are not giving a second opinion.\n\n### The choices *this* app made\n\nWhatever the blueprint shows. A Rails app's bets are Rails, Devise, Pundit, MySQL, Sidekiq, its ERP and payment vendors; a Go app's are different again. Fill in buys/costs/usually for each, from evidence. If a legacy choice is working fine, credit it and move on — \"boring and working\" is a feature, and the pressure to find something to say about a stack is exactly what produces dishonest reports.\n\nWorth naming when it applies: adopting one coherent system in place of hand-rolled, drifted machinery (several ways of deciding who's an admin, a bespoke token table, a back-door test login, home-grown crypto) is a real reliability-and-security upgrade, not a lateral swap — say so when the blueprint's `gaps.json` and `policies.json` show that sprawl.\n\n### The stack a rebuild would land on\n\nInclude this section **only if you are actually recommending a rebuild** onto it — and then give every part of it the same both-sides treatment you gave the app's own bets, per the \"hold your own recommendation to the same bar\" rule above. These are not choices the app made; they are choices you are proposing, which is exactly why their costs are the reader's to weigh. The Pikku target stack is Pikku + Better Auth + TanStack Start + Mantine; the framings below are reference material for the parts you actually recommend, not a script to recite.\n\n**Better Auth (self-hosted sign-in) — instead of a paid service like Auth0/Clerk.**\n- *Buys you:* a mature, battle-tested, **framework-agnostic** library with a deep first-class plugin catalog — two-factor auth, multi-tenancy/organizations, multi-session, rate limiting, Stripe subscription billing, an admin panel, API keys for partners/automation, single-sign-on — plus a plugin system to add more without forking. You keep your users in your own database (single source of truth, no per-user bill that grows with success), with full control of the auth flows, and you can run it embedded in the app or as a standalone self-hosted auth server. So self-hosting here means neither giving up features nor rolling your own security.\n- *Cleans up messy auth (often the biggest win):* adopting it consolidates the kind of hand-rolled, drifted auth that accumulates in an older codebase — several different ways of deciding who's an admin, a bespoke token table, a back-door test login, home-grown encryption — into **one coherent system**. If the blueprint shows a before (legacy, bespoke) and after (on Better Auth), point at it directly: the sprawl collapses into a single well-structured setup. A concrete reliability-and-security upgrade, not just a swap.\n- *Costs you (the honest tradeoff — operational, not security-implementation):* the auth flows and security practices are handled by the library, so this is NOT \"build secure auth from scratch.\" What self-hosting means is you **operate** it — hosting, upgrades, uptime, and incident response sit with your team, where a paid SaaS runs that for you and bundles hosted extras (bot/anomaly detection, leaked-password monitoring, vendor compliance certifications) you'd otherwise operate and document yourself. You trade a per-user bill and vendor ops for control, data ownership, and predictable cost.\n- *Usually:* a strong default for an independent product — mature, full-featured, framework-agnostic, and frequently a genuine cleanup of inherited auth. The real question is who owns operating it, not whether the tool is good enough.\n\n**TanStack Start (the web framework) — instead of the incumbent (Next.js).**\n- *Buys you:* modern, tidy developer experience; strong type-safety that catches whole classes of bugs before users see them; fast iteration; fine-grained control; deploys well to modern/edge hosting. The libraries underneath it — the TanStack ecosystem (Query, Router, Table) — are mature, battle-tested and everywhere in React.\n- *Costs you (the honest tradeoff — maturity of the framework itself):* separate the ecosystem from the framework. Query/Router/Table are mature; **TanStack Start, the framework that wraps them, has not shipped a stable 1.0** — its own maintainers describe it as a release candidate that is feature-complete with a stable API, and tell production users to lock to an exact version and follow the last-mile changes into 1.0. In practice that means pinning your version and budgeting for occasional upgrade work as it settles, rather than upgrading casually. On top of that it's newer than the incumbent (Next.js), which has the largest ecosystem — fewer ready-made templates and third-party examples, and a smaller (though growing) pool of developers who've used *this specific* framework, which can make hiring slightly slower.\n- *Usually:* a credible, modern choice on a mature foundation, but a **pre-1.0 one** — so it carries pinning and upgrade risk that the incumbent does not. Reasonable if the team wants the type-safety and is willing to track the framework to 1.0; harder to justify if nobody has capacity to own upgrades.\n\n**Pikku (the framework a rebuild would land on) — instead of staying where you are.**\n- *Buys you:* one way to write a capability and drive it from anywhere — web, background jobs, timers, realtime, AI assistants, the command line — so a feature is written once instead of five times. Type-safe clients and the API spec fall out of the code rather than being hand-maintained until they drift. The sprawl an organically-grown app accumulates collapses into one shape a small team can hold in its head.\n- *Costs you (the honest tradeoff — it is younger than anything it would replace):* Pikku has **not shipped a stable 1.0** — it's 0.12.x, and 0.13 is the first release that will promise backwards compatibility. Until then upgrades can break you. In practice: pin your version, budget for upgrade work, and know that the community, the ready-made examples, and the pool of developers who have used it are all far smaller than the incumbent's — smaller than TanStack Start's, let alone Next.js's. Being pre-1.0 is normal for a young framework, and survivable, but it is a real cost and it is the reader's to weigh, not yours to skip.\n- *Usually:* worth it when the real problem is sprawl — many surfaces, hand-maintained glue, the same rule implemented three slightly different ways — and the team wants one shape instead of five. Harder to justify for an app that works and needs a few rewires: those are usually cheaper in place. If nobody has capacity to own upgrades, that's a real reason to wait.\n\nCheck these statuses before you write them up rather than repeating them from here — a framework's release stage moves, and the point is the current fact, not this example.\n\n## Delivery\n\nProduce **both**:\n1. A **markdown** report in the repo (e.g. `docs/reports/<app>-second-opinion.md`) — versioned, diffable.\n2. A **shareable web page**: load the **artifact-design** skill, then render the same report as one clean, print-friendly, theme-aware page they can send to a cofounder or the agency. Same content, nicer to read.\n\n## Red flags — you're writing the wrong report\n\n| Symptom | Fix |\n|---|---|\n| A technical term with no translation | Rephrase as the effect on the user/business, or cut the term. |\n| A problem with no \"what it means for you\" | Incomplete — add the business impact or delete it. |\n| All problems, no credit | You'll lose the reader's trust. Name what's genuinely good. |\n| A recommendation with no effort + payoff | Not decision-useful. Add both. |\n| \"Rewrite the app\" | Almost always wrong. Separate rewire (cheap) from rebuild (dear); lean on what `migration.json.mappings` says survives. |\n| A guess stated as fact | Mark confidence. \"I'm certain\" and \"I'd need to check\" are different sentences. |\n| Only listed the upsides of a technology choice | Not honest. Every bet has a cost — name it in business terms, don't soften it to sound positive. |\n| Recommended a stack (including ours) without its cons | You applied a maturity bar to their technology and exempted your own. Both sides, or cut the recommendation. |\n| Trashed a technology as \"the wrong choice\" | Equally lazy. Most choices are defensible; frame as tradeoff + \"what to watch,\" not a verdict. |\n| \"The frontend is just screens, it'll be quick\" | Wrong. The custom-logic pieces (charts, complex tables, editors) are real work — flag them separately from the cheap standard pieces. |\n| A design point with no \"why it matters\" | Taste, not advice. Tie every design finding to user perception (polish/trust) or maintenance cost (change-once vs hunt-everywhere), plus effort. |\n| Reads like a code review | Wrong audience. Would a founder know what to *do* after this paragraph? |\n\n## Relationship to pikku-software-archaeology\n\n`pikku-software-archaeology` = facts → `.knowledge/` blueprint, for a machine to rebuild from. **This skill** = blueprint → opinionated report, for a human to decide from. One extracts; one advises. Run archaeology first (or point this skill at an existing `.knowledge/`), then translate its `gaps.json` + `invariants.json` + `migration.json` into the business-language report above.\n", "pikku-queue/SKILL.md": "---\nname: pikku-queue\ndescription: >-\n Use when adding background job processing, async task queues, or distributed workers to a Pikku\n app. Covers wireQueueWorker, job enqueuing, progress tracking, retries, BullMQ and PgBoss\n adapters. TRIGGER when: code uses wireQueueWorker, user asks about background jobs, task queues,\n async processing, BullMQ, PgBoss, or job retries. DO NOT TRIGGER when: user asks about scheduled\n cron tasks (use pikku-cron) or event-driven triggers (use pikku-trigger).\ninstallGroups: [core]\n---\n\n# Pikku Queue Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions as background queue workers. Supports job control (progress, retry, discard), configurable concurrency, and type-safe job publishing.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their types\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireQueueWorker(config)`\n\n```typescript\nimport { wireQueueWorker } from '@pikku/core/queue'\n\nwireQueueWorker({\n name: string, // Queue name (unique identifier)\n func: PikkuFunc, // Worker function\n config?: {\n batchSize?: number, // Process N jobs at once\n removeOnComplete?: number | boolean, // Clean up completed jobs\n },\n})\n```\n\n### Wire Object (`wire.queue`)\n\nInside queue worker functions:\n\n```typescript\nwire.queue.updateProgress(percent: number) // Report progress (0-100)\nwire.queue.discard(reason: string) // Silently discard job\nwire.queue.fail(reason: string) // Mark job as failed\n```\n\n### Job Publishing\n\n```typescript\nconst jobId = await queue.add(queueName, data, options?)\n```\n\nOptions:\n\n```typescript\n{\n priority?: number, // Higher = processed first\n delay?: number, // Delay in ms before processing\n attempts?: number, // Max retry attempts\n backoff?: {\n type: 'exponential' | 'fixed',\n delay: number, // Base delay in ms\n },\n}\n```\n\n## Usage Patterns\n\n### Basic Queue Worker\n\n```typescript\nconst processReminder = pikkuSessionlessFunc({\n title: 'Process Reminder',\n func: async ({ db, emailService }, { todoId, userId }) => {\n const todo = await db.getTodo(todoId)\n await emailService.sendReminder(userId, todo)\n return { sent: true }\n },\n})\n\nwireQueueWorker({\n name: 'todo-reminders',\n func: processReminder,\n})\n```\n\n### Job Control (Progress, Discard, Fail)\n\n```typescript\nconst processReminder = pikkuSessionlessFunc({\n title: 'Process Reminder',\n func: async ({ db }, { todoId }, wire) => {\n await wire.queue.updateProgress(25)\n\n const todo = await db.getTodo(todoId)\n if (!todo) {\n await wire.queue.discard('Todo not found')\n return\n }\n\n if (todo.completed) {\n await wire.queue.fail('Todo already completed')\n return\n }\n\n await wire.queue.updateProgress(100)\n return { sent: true }\n },\n})\n```\n\n### Retries & Configuration\n\n```typescript\nwireQueueWorker({\n name: 'todo-reminders',\n func: processReminder,\n config: {\n batchSize: 5,\n removeOnComplete: 100,\n },\n})\n\n// Enqueue with retry options\nconst jobId = await queue.add(\n 'todo-reminders',\n {\n todoId: 'abc-123',\n userId: 'user-456',\n },\n {\n priority: 10,\n delay: 5000,\n attempts: 3,\n backoff: { type: 'exponential', delay: 1000 },\n }\n)\n```\n\n### Type-Safe Queue Publishing\n\nAfter `npx pikku all`:\n\n```typescript\nimport { PikkuQueue } from '.pikku/pikku-queue.gen.js'\n\nconst queue = new PikkuQueue(queueService)\n\nconst jobId = await queue.add('todo-reminders', {\n todoId: 'abc-123',\n userId: 'user-456',\n})\n\nconst job = await queue.getJob('todo-reminders', jobId)\nconst status = await job.status() // 'waiting' | 'active' | 'completed' | 'failed'\nconst result = await job.waitForCompletion(30_000)\n```\n\n### Queue Adapters\n\n**BullMQ** (Redis-based):\n\n```typescript\nimport { BullMQQueueService } from '@pikku/queue-bullmq'\n\nconst queueService = new BullMQQueueService({\n connection: { host: 'localhost', port: 6379 },\n})\n```\n\n**PgBoss** (PostgreSQL-based):\n\n```typescript\nimport { PgBossQueueService } from '@pikku/queue-pg-boss'\n\nconst queueService = new PgBossQueueService({\n connectionString: 'postgres://...',\n})\n```\n\n## Complete Example\n\n```typescript\n// functions/email.functions.ts\nexport const sendWelcomeEmail = pikkuSessionlessFunc({\n title: 'Send Welcome Email',\n func: async ({ emailService, db }, { userId }, wire) => {\n await wire.queue.updateProgress(10)\n\n const user = await db.getUser(userId)\n if (!user) {\n await wire.queue.discard('User not found')\n return\n }\n\n await wire.queue.updateProgress(50)\n await emailService.send({\n to: user.email,\n subject: 'Welcome!',\n template: 'welcome',\n data: { name: user.name },\n })\n\n await wire.queue.updateProgress(100)\n return { sent: true, email: user.email }\n },\n})\n\n// wirings/queue.wiring.ts\nwireQueueWorker({\n name: 'welcome-emails',\n func: sendWelcomeEmail,\n config: { removeOnComplete: 100 },\n})\n\n// Enqueue from another function\nexport const registerUser = pikkuSessionlessFunc({\n title: 'Register User',\n func: async ({ db, queue }, { email, name }) => {\n const user = await db.createUser({ email, name })\n await queue.add('welcome-emails', { userId: user.id })\n return { user }\n },\n})\n```\n", "pikku-react/SKILL.md": "---\nname: pikku-react\ndescription: 'Set up @pikku/react in a React app: PikkuProvider context, createPikku factory, and the usePikkuRPC / usePikkuFetch hooks for direct (non-React-Query) calls. TRIGGER when: the user is bootstrapping a React frontend that talks to a Pikku backend, asks how to wire `PikkuProvider`, or needs to make one-off RPC calls outside of useQuery/useMutation. DO NOT TRIGGER when: the user is asking about useQuery/useMutation hooks (use pikku-react-query) or about workflows (use pikku-workflows-client).'\ninstallGroups: [core]\n---\n\n# Pikku React\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/react` is the smallest possible binding: a Context provider plus\ntwo hooks. It does **not** depend on React Query — that's a separate\nopt-in via the generated `api.gen.ts`. Use this skill when setting up the\nprovider or making direct RPC calls.\n\n## What ships\n\n```tsx\nimport {\n PikkuProvider,\n createPikku,\n usePikkuFetch,\n usePikkuRPC,\n usePikkuRealtime,\n} from '@pikku/react'\n```\n\nFive exports. `usePikkuRealtime` is only valid when you wired a\n`PikkuRealtime` class via `createPikku` — see step 3 below.\n\n## Resolving the server URL\n\nEvery client (`createPikku`, realtime, the auth client) resolves its base\nthrough one shared helper in `src/lib/env.ts`. Write this once:\n\n```ts\n// Endpoints come from env, never hardcoded.\nexport function apiUrl(): string {\n // SSR: the client hooks only run in the browser, so a placeholder is fine.\n if (import.meta.env.SSR) {\n return import.meta.env.VITE_API_URL ?? '/__api'\n }\n return import.meta.env.VITE_API_URL ?? `${window.location.origin}/api`\n}\n```\n\n**Never fall back to `http://localhost:3000`.** `import.meta.env.VITE_API_URL`\nis substituted by Vite at *build* time, so any deploy that supplies the URL as\na *runtime* env var or platform binding leaves it `undefined` in the shipped\nbundle — the fallback is then the only branch that ever runs in the browser. A\nlocalhost fallback means every request from a deployed app goes to the user's\nown machine. `origin + '/api'` is same-origin, needs no build-time knowledge of\nthe domain, and is correct wherever the app is served from.\n\nFor local dev, set `VITE_API_URL`, or proxy `/api` → your backend in\n`vite.config.ts` under `server.proxy`. One `/api` entry also covers\n`/api/auth/*`; only add more entries for root-level routes outside `/api`.\n\n## Setup at the app root\n\n```tsx\nimport { createPikku, PikkuProvider } from '@pikku/react'\nimport { PikkuFetch } from './pikku/pikku-fetch.gen'\nimport { PikkuRPC } from './pikku/pikku-rpc.gen'\nimport { apiUrl } from './lib/env'\n\nconst pikku = createPikku(PikkuFetch, PikkuRPC, {\n serverUrl: apiUrl(),\n})\n\ncreateRoot(document.getElementById('root')!).render(\n <PikkuProvider pikku={pikku}>\n <App />\n </PikkuProvider>\n)\n```\n\nIf the project also exposes realtime events (see **pikku-realtime**), pass\nthe `PikkuRealtime` class as the third argument and the instance gets a\n`realtime` field too:\n\n```tsx\nimport { PikkuRealtime } from './pikku/realtime.gen'\n\nconst pikku = createPikku(PikkuFetch, PikkuRPC, PikkuRealtime, {\n serverUrl: apiUrl(),\n})\n// pikku.fetch / pikku.rpc / pikku.realtime — all share the same fetch\n// (server URL + auth configured once).\n```\n\nThe generated classes come from your `pikku.config.json`:\n\n| config field | generated file |\n| ---------------------------- | ----------------------------------------------------- |\n| `clientFiles.fetchFile` | typed HTTP client (`PikkuFetch` class) |\n| `clientFiles.rpcWiringsFile` | RPC client (`PikkuRPC` class) calling all exposed fns |\n| `clientFiles.realtimeFile` | `PikkuRealtime` (websocket events + SSE + channels) |\n\nIf a file isn't being generated, that field is missing from the config —\nadd it and re-run `pikku all`.\n\n`createPikku(...)` accepts the same `CorePikkuFetchOptions` as `PikkuFetch`\nplus `serverUrl`. Auth headers, request interceptors, etc. are configured\non the fetch instance — RPC and realtime inherit them automatically.\n\n## Calling an RPC directly (no React Query)\n\nInside a component:\n\n```tsx\nimport { usePikkuRPC } from '@pikku/react'\n\nfunction Logout() {\n const rpc = usePikkuRPC()\n return <button onClick={() => rpc.invoke('logoutUser', {})}>Sign out</button>\n}\n```\n\n`rpc.invoke(name, data)` is typed against `FlattenedRPCMap` — `name` must\nbe an exposed function id, `data` matches the input schema, return value\nmatches the output schema.\n\nYou also have `rpc.<funcName>(data)` if the generated RPC client builds\ndirect methods (project-dependent).\n\n## Calling fetch directly\n\n```tsx\nconst fetch = usePikkuFetch()\nconst data = await fetch.get('/some-rest-route', { searchParams: {...} })\n```\n\nUse this only when the function is wired via HTTP (REST shape) and you\nneed a path-style call. For RPC calls, `usePikkuRPC()` is cleaner.\n\n## Realtime subscriptions\n\nIf you wired a `PikkuRealtime` class into `createPikku`, use\n`usePikkuRealtime()` to grab the shared instance:\n\n```tsx\nimport { usePikkuRealtime } from '@pikku/react'\nimport type { PikkuRealtime } from './pikku/realtime.gen'\n\nfunction TodoList() {\n const realtime = usePikkuRealtime<PikkuRealtime>()\n useEffect(() => {\n return realtime.subscribe('todo-created', ({ todo }) => {\n /* ... */\n })\n }, [realtime])\n // ...\n}\n```\n\nThe hook throws if no `PikkuRealtime` was wired — that's how you know to\nadd it to `createPikku(...)`. Full event-hub setup, publishing, and SSE\nhelpers live in **pikku-realtime**.\n\n## When to reach for what\n\n| Need | Use |\n| ----------------------------------- | --------------------------------------------- |\n| Render data, dedupe + cache | **usePikkuQuery** (react-query) |\n| Trigger a write, wait for result | **usePikkuMutation** (react-query) |\n| Paginate | **usePikkuInfiniteQuery** (react-query) |\n| One-off call from an event handler | `usePikkuRPC()` direct |\n| Hit a REST endpoint (not RPC) | `usePikkuFetch()` |\n| Run a workflow | **pikku-workflows-client** |\n| Subscribe to events / SSE / channel | `usePikkuRealtime()` (see **pikku-realtime**) |\n\nThe first three live in your generated `api.gen.ts` (see the\n**pikku-react-query** skill). This skill covers the bottom four rows.\n\n## Authentication\n\nAuth is handled at the `PikkuFetch` layer — pass options to `createPikku`\nor set headers on the fetch instance after creation. Common pattern:\n\n```tsx\nconst pikku = createPikku(PikkuFetch, PikkuRPC, {\n serverUrl: apiUrl(),\n fetchOptions: {\n onRequest: (req) => {\n const token = localStorage.getItem('token')\n if (token) req.headers.set('Authorization', `Bearer ${token}`)\n },\n },\n})\n```\n\nExact option names depend on the `@pikku/fetch` version — read\n`PikkuFetch`'s constructor type if unsure.\n\n## What NOT to do\n\n- Don't instantiate `PikkuFetch`/`PikkuRPC` inside a component — `createPikku`\n goes once at the app root, the instance flows through Context.\n- Don't call `usePikkuRPC()` outside a `<PikkuProvider>` — it throws.\n- Don't write a custom RPC client. The generated one already covers every\n exposed function with full types.\n- Don't hardcode user-facing strings. Every display string goes through an\n i18n token — see **pikku-i18n** for the setup (it's English-only by default).\n", "pikku-react-query/SKILL.md": "---\nname: pikku-react-query\ndescription: 'Use the Pikku auto-generated React Query hooks (`usePikkuQuery`, `usePikkuMutation`, `usePikkuInfiniteQuery`) to call backend RPC functions from a React frontend with full type safety. TRIGGER when: writing React components that need to call a Pikku function, fetch data, mutate data, or paginate; user mentions React Query, useQuery, useMutation, or building a frontend that talks to a Pikku backend. DO NOT TRIGGER when: working on the backend (use pikku-rpc / pikku-feature) or wiring a non-React frontend.'\ninstallGroups: [core]\n---\n\n# Pikku React Query Hooks\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nPikku generates a typed React Query layer from your backend `expose: true`\nfunctions. You don''t write `useQuery`/`useMutation` against `fetch`\nyourself — you call hooks named after RPCs and get full type inference for\ninput + output.\n\n## Discover what's available on the client\n\nBefore writing a hook, get the full client surface in one call:\n\n```bash\nyarn pikku meta clients --json\n```\n\nReturns RPCs, workflows, and channels with descriptions and type names:\n\n```json\n{\n \"rpcs\": [\n { \"name\": \"createTodo\", \"description\": \"Create a todo\",\n \"readonly\": false, \"input\": \"CreateTodoInput\", \"output\": \"CreateTodoOutput\" },\n { \"name\": \"listTodos\", \"description\": \"List all todos\",\n \"readonly\": true, \"input\": null, \"output\": \"ListTodosOutput\" }\n ],\n \"workflows\": [...],\n \"channels\": [...]\n}\n```\n\nThe `name` is the RPC identifier; pass it to the hooks below. Input/output\nshapes are inferred automatically — the hook is typed against\n`FlattenedRPCMap[name]['input' | 'output']`. Use `description` to pick the\nright RPC; use `readonly` to choose `usePikkuQuery` vs `usePikkuMutation`.\n\n## Setup (once per app)\n\nIn your app entry (e.g. `main.tsx`):\n\n```tsx\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { PikkuProvider, createPikku } from '@pikku/react'\nimport { PikkuFetch } from './pikku/pikku-fetch.gen'\nimport { PikkuRPC } from './pikku/pikku-rpc.gen'\n\nimport { apiUrl } from './lib/env'\n\nconst queryClient = new QueryClient()\nconst pikku = createPikku(PikkuFetch, PikkuRPC, {\n serverUrl: apiUrl(),\n})\n\n<QueryClientProvider client={queryClient}>\n <PikkuProvider pikku={pikku}>\n <App />\n </PikkuProvider>\n</QueryClientProvider>\n```\n\nThe two generated files come from `pikku.config.json`'s\n`clientFiles.fetchFile` and `clientFiles.rpcWiringsFile`. Hooks live in\nthe file at `clientFiles.reactQueryFile` (typically `api.gen.ts`).\n\n`apiUrl()` is the shared server-URL helper — see **pikku-react**. Never\ninline `?? 'http://localhost:3000'`: a deploy that supplies the URL as a\nruntime binding leaves `import.meta.env.VITE_API_URL` undefined in the\nbundle, so the fallback is the branch that actually runs.\n\n## TanStack Start (SSR)\n\nUnder Start the provider mounts in `routes/__root.tsx` rather than\n`main.tsx`, and the same module is evaluated on the server. Three things\ndiffer:\n\n1. **`apiUrl()` must have an SSR branch.** `window` is undefined during\n render; return the build-time var or a placeholder (the client hooks\n only fire in the browser).\n2. **Build auth clients lazily.** Better Auth validates its baseURL with\n `new URL(...)` at construction, so a module-scope `createAuthClient`\n crashes SSR on the placeholder. Memoize it behind a getter:\n\n ```ts\n let _authClient: ReturnType<typeof createAuthClient> | undefined\n export const authClient = () =>\n (_authClient ??= createAuthClient({ baseURL: `${apiUrl()}/auth` }))\n ```\n\n3. **The auth baseURL needs the `/auth` suffix.** Better Auth only\n appends its default `/api/auth` when the baseURL carries no path.\n `apiUrl()` already ends in `/api`, so a bare `apiUrl()` leaves the\n client calling `/api/get-session` and 404ing.\n\nServer functions that need typed RPC access use the generated shim:\n\n```bash\npikku tanstack-start # emits the makeApi server-function shim\n```\n\n## The hooks\n\nAll hooks are imported from your generated `api.gen.ts`:\n\n```tsx\nimport {\n usePikkuQuery,\n usePikkuMutation,\n usePikkuInfiniteQuery,\n} from './pikku/api.gen'\n```\n\n### `usePikkuQuery(name, data, options?)`\n\nFor RPCs that **read** data. Cacheable. The hook is typed against the RPC's\ninput + output.\n\n```tsx\nexport function TodoList() {\n const { data, isLoading, error } = usePikkuQuery('listTodos', {})\n\n if (isLoading) return <p>Loading…</p>\n if (error) return <p>{error.message}</p>\n return (\n <ul>\n {data?.todos.map((t) => (\n <li key={t.id}>{t.title}</li>\n ))}\n </ul>\n )\n}\n```\n\nThe query key is `[name, data]` automatically — no manual key wrangling.\nPass standard `useQuery` options through (`staleTime`, `enabled`, etc.).\n\n### `usePikkuMutation(name, options?)`\n\nFor RPCs that **write**. Returns a React Query mutation object.\n\n```tsx\nexport function CreateTodoForm() {\n const queryClient = useQueryClient()\n const mutation = usePikkuMutation('createTodo', {\n onSuccess: () => queryClient.invalidateQueries({ queryKey: ['listTodos'] }),\n })\n\n const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n const title = (\n e.currentTarget.elements.namedItem('title') as HTMLInputElement\n ).value\n mutation.mutate({ title })\n }\n\n return (\n <form onSubmit={onSubmit}>\n <input name=\"title\" />\n <button type=\"submit\" disabled={mutation.isPending}>\n {mutation.isPending ? 'Adding…' : 'Add'}\n </button>\n </form>\n )\n}\n```\n\nThe input passed to `mutation.mutate(...)` is type-checked against the RPC's\ninput schema. After success, **invalidate** any list/get queries that should\nrefetch.\n\n### `usePikkuInfiniteQuery(name, data, options?)`\n\nOnly available for RPCs whose output has a `nextCursor?: string | null`\nfield — typically a list endpoint with pagination. The hook auto-feeds\n`nextCursor` into the next page's request.\n\n```tsx\nconst { data, fetchNextPage, hasNextPage, isFetchingNextPage } =\n usePikkuInfiniteQuery('listTodos', { limit: 20 })\n\nconst todos = data?.pages.flatMap((p) => p.rows) ?? []\n```\n\nIf the hook isn't generated for an RPC, the RPC's output doesn't include\n`nextCursor` — paginate it on the backend or use `usePikkuQuery` with\nmanual cursor state.\n\n## Workflow hooks\n\nWhen the project has workflows (`capabilities.workflow: true`), three\nextra hooks are generated. See the **pikku-workflows-client** skill.\n\n## Calling RPCs without React Query\n\nFor one-off calls (event handlers outside of state, side effects), use\n`usePikkuRPC()` from `@pikku/react`:\n\n```tsx\nconst rpc = usePikkuRPC()\nconst handleClick = async () => {\n const result = await rpc.invoke('createTodo', { title: 'inline' })\n}\n```\n\nBut prefer the React Query hooks for anything that touches render state —\ncaching, retries, dedup, and dev-tools come for free.\n\n## Common patterns\n\n- **Optimistic updates**: pass `onMutate` to `usePikkuMutation` to update\n the cache before the server responds. Standard React Query pattern;\n Pikku doesn't add anything special.\n- **Conditional fetching**: pass `enabled: !!someValue` to skip a query\n until you have the input.\n- **Refetch on focus**: enabled by default in React Query; disable with\n `refetchOnWindowFocus: false` in options.\n\n## What NOT to do\n\n- Don't import the RPC client directly and call it inside `useEffect` —\n use the hooks. They handle dedup, caching, and unmount safely.\n- Don't hand-write `useQuery({ queryKey: ['listTodos'], queryFn: ... })`\n — `usePikkuQuery('listTodos', {})` does it correctly with one line.\n- Don't construct hook names dynamically. Hook names = RPC names known at\n generation time.\n- Don't bypass the type system with `as any` — if a hook's types don't\n match what you expect, the backend's input/output schemas are wrong;\n fix those first.\n", "pikku-realtime/references/other-routes.md": "# Subscribing to other SSE / WebSocket routes\n\nThe same `PikkuRealtime` client also handles generic SSE + channel routes (not just\n`/events` topics). Use the path; the base URL is inherited from `PikkuFetch`.\n\n```ts\n// Any `sse: true` HTTP route\nconst sub = realtime.subscribeToSSE<{ progress: number }>(\n `/workflow-run/${runId}/stream`,\n (event) => setProgress(event.progress)\n)\n// later: sub.close()\n\n// Any wireChannel — open a raw socket, wrap in PikkuWebSocket for typed I/O\nconst ws = realtime.connectToChannel('/ws/kanban')\nconst typed = new PikkuWebSocket<'kanban-live'>(ws)\ntyped.getRoute('command').subscribe('message', (data) => {\n /* ... */\n})\n```\n\nDiscover what's available with `pikku meta clients --json` — `channels` and any HTTP\n`sse: true` routes are listed there.\n", "pikku-realtime/SKILL.md": "---\nname: pikku-realtime\ndescription: 'Use Pikku''s realtime feature — typed pub/sub events over WebSocket (multi-topic) or SSE (single-topic, auto-cleanup). Covers declaring EventHubTopics, scaffolding the /events channel, the auto-generated `PikkuRealtime` client, and publishing events from a function. TRIGGER when: the user asks for realtime updates, pub/sub, push notifications, server-sent events, websocket events, eventhub, or \"live\" data on the frontend. DO NOT TRIGGER when: the user wants RPC-style request/response (use pikku-rpc / pikku-react-query) or a custom one-off WebSocket channel (use pikku-websocket).'\ninstallGroups: [core]\n---\n\n# Pikku Realtime\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nMost realtime UI is just typed pub/sub: a server pushes `todo-created`, the client\nrenders it. Pikku ships exactly that, two ways — both use the same `EventHubService`\nand the same publish call, so choose by transport, not by code shape:\n\n- **WebSocket** at `/events` — one connection, many topic subscriptions.\n- **SSE** at `GET /events/:topic` — one connection per topic, auto-cleanup on\n disconnect. Good when WebSocket is blocked or for trivially streaming one topic.\n\n## 1. Declare your topics\n\nIn your project's types file (e.g. `types/eventhub-topics.d.ts`):\n\n```ts\nimport type { Todo } from '../src/schemas.js'\n\nexport type EventHubTopics = {\n 'todo-created': { todo: Todo }\n 'todo-updated': { todo: Todo }\n 'todo-deleted': { todoId: string }\n}\n```\n\nReference it in `application-types.d.ts` and instantiate it in `services.ts`:\n\n```ts\n// application-types.d.ts\nimport type { EventHubService } from '@pikku/core/channel'\nimport type { EventHubTopics } from './eventhub-topics.js'\n\nexport interface SingletonServices extends CoreSingletonServices<Config> {\n eventHub?: EventHubService<EventHubTopics>\n}\n\n// services.ts\nimport { LocalEventHubService } from '@pikku/core/channel'\nconst eventHub = new LocalEventHubService<EventHubTopics>()\n```\n\nFor multi-instance deployments use `CloudflareEventHubService` /\n`LambdaEventHubService` / `UWSEventHubService` instead — same interface.\n\n## 2. Enable the server side\n\n```bash\nyarn pikku enable events # auth required by default\nyarn pikku enable events --noAuth # public events\n```\n\nThis sets `scaffold.events` in `pikku.config.json`. The next `pikku all` generates\n`events.gen.ts` in your scaffold dir, wiring (using whatever `eventHub` is in your\nsingletons — you write neither by hand):\n\n- A WebSocket channel at `/events` handling `{action: 'subscribe' | 'unsubscribe', topic}` messages.\n- An SSE handler at `GET /events/:topic`.\n\n## 3. Generate the typed client\n\nAdd to `pikku.config.json`:\n\n```jsonc\n{\n \"clientFiles\": {\n \"realtimeFile\": \"packages/sdk/src/pikku/realtime.gen.ts\",\n // Optional: full type inference for subscribe/unsubscribe\n \"realtimeEventHubTopicsImport\": \"../../../functions/types/eventhub-topics.js#EventHubTopics\",\n },\n}\n```\n\nRun `pikku all` (or `pikku realtime` to regenerate just this file). The generated\nfile exports two surfaces:\n\n```ts\nexport class PikkuRealtime {\n constructor(options: { url: string; reconnect?: boolean; ... })\n subscribe<K extends keyof EventHubTopics>(topic: K, handler: (data: EventHubTopics[K]) => void): () => void\n unsubscribe<K extends keyof EventHubTopics>(topic: K, handler?: ...): void\n close(): void\n}\n\nexport function subscribeToTopicViaSSE<K extends keyof EventHubTopics>(\n baseUrl: string, topic: K, handler: (data: EventHubTopics[K]) => void\n): { close: () => void }\n```\n\nWithout `realtimeEventHubTopicsImport`, the client falls back to\n`Record<string, unknown>` — usable but untyped. Set the import for full typed\nsubscribe/unsubscribe.\n\n## 4. Publish events from a function\n\nThe `/events` channel listens for client subscriptions; the eventHub fans out\npublishes. Envelope the payload with `topic` so the client dispatcher works; the\n`null` channelId means \"broadcast to all subscribers\" (pass a specific channel id\nto exclude/include a single connection):\n\n```ts\nimport { pikkuFunc } from '#pikku'\n\nexport const createTodo = pikkuFunc({\n input: CreateTodoInput,\n output: CreateTodoOutput,\n func: async ({ kysely, eventHub }, data) => {\n const todo = await kysely\n .insertInto('todos').values(data).returningAll()\n .executeTakeFirstOrThrow()\n\n if (eventHub) {\n await eventHub.publish('todo-created', null, {\n topic: 'todo-created',\n data: { todo },\n })\n }\n return { id: todo.id }\n },\n})\n```\n\nA thin helper removes the duplication:\n\n```ts\nasync function publishEvent<K extends keyof EventHubTopics>(\n hub: EventHubService<EventHubTopics>, topic: K, data: EventHubTopics[K]\n) {\n return hub.publish(topic, null, { topic, data })\n}\n// usage: await publishEvent(eventHub, 'todo-created', { todo })\n```\n\n## 5. Wire it up — share fetch with PikkuRPC\n\n`PikkuRealtime` mirrors `PikkuRPC`: it wraps the same `PikkuFetch`, so server URL +\nauth are configured **once** and shared across HTTP, RPC, and realtime transports.\n\n```tsx\nimport { createPikku, PikkuProvider } from '@pikku/react'\nimport { PikkuFetch } from './pikku/pikku-fetch.gen'\nimport { PikkuRPC } from './pikku/pikku-rpc.gen'\nimport { PikkuRealtime } from './pikku/realtime.gen'\n\nconst pikku = createPikku(\n PikkuFetch,\n PikkuRPC,\n PikkuRealtime, // pass the realtime class as the third arg\n { serverUrl: apiUrl() } // shared env helper — see pikku-react\n)\n// pikku.fetch / pikku.rpc / pikku.realtime — all share the same fetch.\n\ncreateRoot(document.getElementById('root')!).render(\n <PikkuProvider pikku={pikku}><App /></PikkuProvider>\n)\n```\n\nOr wire manually:\n\n```ts\nconst realtime = new PikkuRealtime()\nrealtime.setPikkuFetch(pikku.fetch) // inherits serverUrl + auth\n```\n\n## 6. Subscribe from React\n\nSubscribe inside `useEffect` (never the render path, or you create a subscription\nper render). `subscribe` returns an unsubscribe function; SSE's `subscribeToTopic`\nreturns a handle with `close()`:\n\n```tsx\nimport { useEffect, useState } from 'react'\n\nfunction TodoList() {\n const { realtime } = usePikku() // a hook over your context\n const [todos, setTodos] = useState<Todo[]>([])\n\n useEffect(() => {\n // WebSocket multi-topic:\n const off = realtime.subscribe('todo-created', ({ todo }) =>\n setTodos((prev) => [...prev, todo]))\n return off\n\n // Single-topic SSE (auto-cleanup on close) instead:\n // const sub = realtime.subscribeToTopic('todo-created', ({ todo }) =>\n // setTodos((prev) => [...prev, todo]))\n // return () => sub.close()\n }, [realtime])\n\n return <ul>{todos.map((t) => <li key={t.id}>{t.title}</li>)}</ul>\n}\n```\n\n## Other SSE / WebSocket routes\n\nThe same client also subscribes to generic `sse: true` routes and raw `wireChannel`\nsockets (`subscribeToSSE`, `connectToChannel`). See\n[references/other-routes.md](references/other-routes.md).\n\n## When to pick which transport\n\n| Need | Use |\n| ------------------------------------------ | ----------------------------- |\n| Many topics in one connection | **PikkuRealtime** (WebSocket) |\n| Single live stream, simple cleanup | **subscribeToTopicViaSSE** |\n| Bidirectional (client also sends messages) | **PikkuRealtime** |\n| WebSockets blocked by infra | **subscribeToTopicViaSSE** |\n\nBoth auto-clean on the server (the eventHub's `onChannelClosed` hook unsubscribes\nall topics for the dead channel id). Don't write manual cleanup unless you're\nunsubscribing partway through a session.\n\n## What NOT to do\n\n- Don't call `eventHub.publish(topic, ..., rawData)` without the `{topic, data}`\n envelope — clients use `topic` to dispatch handlers.\n- Don't create your own `/events` channel by hand — `pikku enable events` already\n does it correctly with disconnect cleanup.\n- Don't subscribe inside the render path — use `useEffect`.\n- Don't subscribe to topics that don't exist in `EventHubTopics`. The generated\n client's types prevent it; if you reach for `as any` to subscribe to a string,\n declare the topic first.\n", "pikku-redis/SKILL.md": "---\nname: pikku-redis\ndescription: >-\n Use when setting up Redis-backed services in a Pikku app. Covers channel stores, workflow\n services, secret services, event hubs, agent runs, and deployment services backed by Redis.\n TRIGGER when: code uses RedisChannelStore, RedisWorkflowService, RedisSecretService, or user\n asks about Redis setup with Pikku. DO NOT TRIGGER when: user asks about BullMQ queues (use\n pikku-queue) or SQL databases (use pikku-kysely).\n---\n\n# Pikku Redis\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/redis` provides Redis-backed implementations of Pikku's core service interfaces using [ioredis](https://github.com/redis/ioredis).\n\n## Installation\n\n```bash\nyarn add @pikku/redis\n```\n\n## API Reference\n\n### Available Services\n\nAll services accept a Redis connection (ioredis `Redis` instance, `RedisOptions`, or connection string) in their constructor.\n\n| Service | Interface | Purpose |\n| ------------------------- | ---------------------- | ---------------------------------------------- |\n| `RedisChannelStore` | `ChannelStore` | WebSocket channel state persistence |\n| `RedisEventHubStore` | `EventHubStore` | Event hub state persistence |\n| `RedisWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |\n| `RedisWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |\n| `RedisDeploymentService` | `DeploymentService` | Deployment state management |\n| `RedisAgentRunService` | `AgentRunService` | Agent execution tracking |\n| `RedisSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |\n\n### Secret Service\n\n```typescript\nimport { RedisSecretService } from '@pikku/redis'\n\nconst secrets = new RedisSecretService(\n connectionOrConfig: Redis | RedisOptions | string,\n config: { kekSecret: string; salt: string }\n)\n\nawait secrets.getSecret<T = string>(key: string): Promise<T>\nawait secrets.getSecrets<T>(keys: (keyof T & string)[]): Promise<Partial<T>>\nawait secrets.hasSecret(key: string): Promise<boolean>\nawait secrets.setSecret(key: string, value: unknown): Promise<void>\nawait secrets.deleteSecret(key: string): Promise<void>\nawait secrets.rotateKEK(): Promise<number>\nawait secrets.close(): Promise<void>\n```\n\n## Usage Patterns\n\n### Full Setup\n\n```typescript\nimport {\n RedisChannelStore,\n RedisWorkflowService,\n RedisSecretService,\n} from '@pikku/redis'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new PinoLogger()\n\n const channelStore = new RedisChannelStore(config.redisUrl)\n const workflowService = new RedisWorkflowService(config.redisUrl)\n\n const secrets = new RedisSecretService(config.redisUrl, {\n kekSecret: config.kekSecret,\n salt: config.salt,\n })\n\n return { config, logger, channelStore, workflowService, secrets }\n})\n```\n", "pikku-rpc/SKILL.md": "---\nname: pikku-rpc\ndescription: >-\n Use when making internal function-to-function calls within a Pikku app, composing functions, or\n exposing RPC endpoints. Covers rpc.invoke, rpc.remote, rpc.exposed, and generated RPC client.\n TRIGGER when: code uses wire.rpc or expose: true, user asks about calling one Pikku function\n from another, function composition, or RPC endpoints. DO NOT TRIGGER when: user asks about HTTP\n routes (use pikku-http) or addon cross-package calls (use pikku-addon).\ninstallGroups: [core]\n---\n\n# Pikku RPC Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nCall Pikku functions from other Pikku functions internally with full type safety. Use RPC to compose business logic without importing functions directly.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and which could be called via RPC\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### RPC Methods (on `wire.rpc`)\n\nFour ways to call functions via RPC:\n\n| Method | Purpose |\n| -------------------------------- | ----------------------------------------- |\n| `rpc.invoke(name, data)` | Internal call to any wired function |\n| `rpc.remote(name, data)` | Remote call via DeploymentService |\n| `rpc.exposed(name, data)` | Call functions marked with `expose: true` |\n| `rpc.startWorkflow(name, input)` | Start a workflow |\n\n### Exposed Functions\n\nMark a function as externally callable via RPC:\n\n```typescript\nconst greet = pikkuSessionlessFunc({\n title: 'Greet',\n expose: true, // ← callable via rpc.exposed()\n func: async ({}, { name }) => {\n return { message: `Hello, ${name}!` }\n },\n})\n```\n\n### HTTP RPC Endpoint\n\nExpose all `expose: true` functions over HTTP:\n\n```typescript\nwireHTTP({\n route: '/rpc/:rpcName',\n method: 'post',\n auth: false,\n func: rpcCaller,\n})\n```\n\n## Usage Patterns\n\n### Internal Function Composition\n\n```typescript\nconst calculateTax = pikkuSessionlessFunc({\n title: 'Calculate Tax',\n func: async ({}, { amount, rate }) => {\n return { tax: amount * rate }\n },\n})\n\nconst processOrder = pikkuFunc({\n title: 'Process Order',\n func: async ({ db }, { orderId }, { rpc }) => {\n const order = await db.getOrder(orderId)\n\n // Call another pikku function internally — fully typed\n const { tax } = await rpc.invoke('calculateTax', {\n amount: order.total,\n rate: 0.08,\n })\n\n return { orderId, total: order.total + tax }\n },\n})\n```\n\n### When to Use RPC vs Direct Imports\n\n| Approach | Use When |\n| -------------- | -------------------------------------------------------------------------------------------- |\n| `rpc.invoke()` | Cross-domain calls, maintaining separation of concerns, function may be in different package |\n| Direct import | Same module, tightly coupled logic, performance critical |\n\nRPC calls go through Pikku's middleware and permission pipeline. Direct imports skip them.\n\n### Generated RPC Client\n\nAfter `npx pikku all`:\n\n```typescript\nimport { pikkuRPC } from '.pikku/pikku-rpc.gen.js'\n\npikkuRPC.setServerUrl('http://localhost:4002')\n\nconst result = await pikkuRPC.invoke('calculateTax', {\n amount: 100,\n rate: 0.08,\n})\n\npikkuRPC.setAuthorizationJWT(token)\n```\n\n## Complete Example\n\n```typescript\n// functions/billing.functions.ts\nexport const calculateTax = pikkuSessionlessFunc({\n title: 'Calculate Tax',\n func: async ({}, { amount, region }) => {\n const rates = { US: 0.08, EU: 0.2, UK: 0.2 }\n return { tax: amount * (rates[region] || 0) }\n },\n})\n\nexport const calculateShipping = pikkuSessionlessFunc({\n title: 'Calculate Shipping',\n func: async ({}, { weight, region }) => {\n const base = region === 'US' ? 5 : 15\n return { shipping: base + weight * 0.5 }\n },\n})\n\n// functions/orders.functions.ts\nexport const processOrder = pikkuFunc({\n title: 'Process Order',\n func: async ({ db }, { orderId }, { rpc }) => {\n const order = await db.getOrder(orderId)\n\n const { tax } = await rpc.invoke('calculateTax', {\n amount: order.total,\n region: order.region,\n })\n\n const { shipping } = await rpc.invoke('calculateShipping', {\n weight: order.totalWeight,\n region: order.region,\n })\n\n const finalTotal = order.total + tax + shipping\n await db.updateOrder(orderId, { tax, shipping, finalTotal })\n\n return { orderId, total: finalTotal, tax, shipping }\n },\n})\n```\n", "pikku-rtl/SKILL.md": "---\nname: pikku-rtl\ndescription: 'Make a Pikku frontend work in both English (LTR) and Arabic / right-to-left languages. Direction is derived from the active locale, applied once at the document root, and the layout mirrors itself — but only if styling is written flow-relative (margin-inline-start, text-align: start, Mantine ms/me) instead of left/right. TRIGGER when: adding Arabic (or Hebrew/Farsi/Urdu), asked to \"support RTL / right-to-left / bidi / mirror the layout\", or writing layout styles in an app that may run RTL. Builds on pikku-i18n (an RTL language is just another locale file). DO NOT TRIGGER for backend functions or for LTR-only copy changes.'\ninstallGroups: [core]\n---\n\n# Pikku RTL (Arabic + English)\n\nThis skill sits **on top of** `pikku-i18n`. That skill maps a locale to `t()`\ntokens; this one adds the second axis: a locale also has a **direction**.\nArabic is not special-cased — it is just another locale file (`ar.json`,\nregistered `satisfies typeof en`) plus the document being told it is `rtl`.\n\n## The one idea\n\nSet `dir` **once at the document root** from the active locale, then let the\nbrowser and Mantine mirror everything — _provided_ every custom style is written\n**flow-relative** (start/end), never **physical** (left/right). Get those two\nthings right and Arabic, Hebrew, Farsi and Urdu all work with zero per-component\n*layout* code — directional icons still need one manual step, covered below.\n\n## Agent Operating Procedure\n\n1. **Tokens first.** Every visible string is already a `t()` token via\n `pikku-i18n`. Arabic copy goes in `i18n/ar.json`, mirroring `en.json`'s keys,\n registered with `satisfies typeof en` so a missing key is a compile error.\n2. **Add the direction helper** to the i18n config (one home for locale→dir):\n ```ts\n const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur'])\n export function localeDir(locale: string = defaultLocale): 'rtl' | 'ltr' {\n return RTL_LOCALES.has(locale.split('-')[0]) ? 'rtl' : 'ltr'\n }\n ```\n (The bundled templates already ship this helper — use it, don't reinvent it.)\n3. **Apply `dir` + `lang` at the root**, once, from the active locale — pick the\n recipe for your framework below.\n4. **Write every layout style flow-relative.** This is the part that actually\n makes mirroring work; see the rules. When editing existing UI to be\n RTL-ready, the job is mostly a search-and-replace of physical properties.\n5. **Flip directional icons** (chevrons, back/forward arrows) — the one thing\n logical properties can't do for you.\n6. Validate with the app's `tsc`, then load `?i18n-debug` / set `dir` and\n eyeball that the layout mirrors and nothing is stuck on the wrong edge.\n\n## Flow-relative, not physical — the rules that make it mirror\n\nUse the **inline-axis logical** property; never the physical one:\n\n| Don't (physical) | Do (flow-relative) |\n| ---------------------------- | -------------------------------------------- |\n| `margin-left` / `marginLeft` | `margin-inline-start` / `marginInlineStart` |\n| `margin-right` | `margin-inline-end` / `marginInlineEnd` |\n| `padding-left/right` | `padding-inline-start/end` |\n| `left: 0` / `right: 0` | `inset-inline-start: 0` / `inset-inline-end` |\n| `text-align: left/right` | `text-align: start / end` |\n| `border-top-left-radius` | `border-start-start-radius` |\n| `float: left/right` | `float: inline-start / inline-end` |\n\nIn **Mantine**, use the logical style props — they emit the logical CSS above:\n\n| Don't | Do |\n| ----------- | ----------- |\n| `ml` / `mr` | `ms` / `me` |\n| `pl` / `pr` | `ps` / `pe` |\n\nMantine's own components already use logical properties internally, so once the\ndirection is set they mirror automatically — you only have to be disciplined in\n**your** styles.\n\n**Leave flexbox and grid alone.** `display:flex` already follows `dir`:\n`justify-content: flex-start` resolves to the right edge under RTL on its own.\nNever \"fix\" RTL by swapping to `flex-direction: row-reverse` or reordering DOM —\nthat double-flips and breaks the moment direction changes. The DOM order is\nlogical order; let `dir` handle the visual order.\n\n## Applying direction at the root\n\n### Mantine app (e.g. environment-template)\n\nMantine ships first-class RTL: wrap the tree in `DirectionProvider` and set the\nmatching `dir` on `<html>`.\n\n```tsx\nimport { DirectionProvider, MantineProvider } from '@mantine/core'\nimport i18n, { detectLocale, localeDir } from './i18n/config'\n\nconst locale =\n typeof window !== 'undefined' ? detectLocale(window.location.pathname) : 'en'\nconst dir = localeDir(locale)\n\nif (typeof document !== 'undefined') {\n document.documentElement.lang = locale\n document.documentElement.dir = dir // Mantine + browser read this\n}\n\nroot.render(\n <DirectionProvider initialDirection={dir}>\n <MantineProvider theme={theme} defaultColorScheme=\"dark\">\n {/* …app… */}\n </MantineProvider>\n </DirectionProvider>\n)\n```\n\nTo flip direction live (a language switcher) call\n`document.documentElement.setAttribute('dir', localeDir(next))` and Mantine's\n`useDirection().setDirection(dir)`; both read the same value.\n\n### Plain Vite SPA (kanban, test-harness vite-spa)\n\nNo Mantine — just put `dir`/`lang` on `<html>` at bootstrap, after the locale is\ndetected (the same `detectLocale` the i18n config uses):\n\n```ts\nimport { detectLocale, localeDir } from './i18n/config'\n\nconst locale = detectLocale(window.location.pathname)\ndocument.documentElement.lang = locale\ndocument.documentElement.dir = localeDir(locale)\n```\n\nEverything below inherits `dir` from `<html>`; logical CSS does the mirroring.\n\n### Vite SSR (test-harness vite-ssr)\n\nThe worker renders the full HTML, so set `lang`/`dir` on the server `<html>`\nfrom the **URL** locale (the client inherits it on hydration — no flash):\n\n```tsx\nimport { detectLocale, localeDir } from './i18n/config'\n\nconst locale = detectLocale(new URL(request.url).pathname)\nconst dir = localeDir(locale)\nconst html = `<!doctype html>\n<html lang=\"${locale}\" dir=\"${dir}\">\n …\n</html>`\n```\n\ni18next's active language must match: call `i18n.changeLanguage(locale)` before\n`renderToString` so the SSR'd text and `dir` agree.\n\n### Next.js app-router (test-harness next-ssr / next-static)\n\nSet it on the `<html>` in `app/layout.tsx`. With locale-prefixed routes the\nsegment gives the locale; for a single-locale build it's a constant:\n\n```tsx\nimport { localeDir, defaultLocale } from './i18n/config'\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n const locale = defaultLocale // or the [lang] route segment / params\n return (\n <html lang={locale} dir={localeDir(locale)}>\n <body>{children}</body>\n </html>\n )\n}\n```\n\nFor `output: 'export'` with `/ar` prefixes, derive `locale` from the route\nsegment so each statically-exported tree carries the right `dir`.\n\n## Directional icons — the manual bit\n\nLogical properties mirror box layout, **not glyphs**. An icon that points\nsomewhere (chevron, back/next arrow, send, undo) must flip under RTL; a\nnon-directional icon (search, settings, avatar) must **not**. Flip with the\n`:dir()` selector — no JS, no per-locale branching:\n\n```css\n:dir(rtl) .icon-directional {\n transform: scaleX(-1);\n}\n```\n\nOr in CSS-in-JS / inline, gate on the resolved direction:\n`transform: localeDir(locale) === 'rtl' ? 'scaleX(-1)' : undefined`.\nPrefer logical icon components if your icon set ships them.\n\n## Arabic typography niceties\n\n- **Font:** the default Latin stack renders Arabic with the system fallback,\n which is inconsistent. Add an Arabic-capable family (e.g. _Noto Sans Arabic_,\n _IBM Plex Sans Arabic_) to `font-family` so both scripts look intentional.\n- **Numerals:** don't hardcode digits. Format numbers/dates with\n `Intl.NumberFormat`/`Intl.DateTimeFormat` (or i18next formatters) given the\n active locale, so Western vs Arabic-Indic digits follow the locale choice.\n- **Line height:** Arabic diacritics sit tall — a slightly larger `line-height`\n on Arabic body text avoids clipping. Keep it locale-scoped, not global.\n\n## Adding Arabic to an existing app — checklist\n\n1. `i18n/ar.json` mirroring `en.json`; register\n `ar: { translation: ar satisfies typeof en }` and add `'ar'` to\n `supportedLocales`. (Type-complete or it won't compile — the deploy blocks.)\n2. Confirm the `localeDir` helper includes `ar` (it does by default).\n3. Confirm the root sets `dir` from the locale (recipe above).\n4. Sweep the app's styles: replace every `left/right`, `ml/mr`, `text-align:\nleft` with the flow-relative equivalent; revert any manual `row-reverse`.\n5. Flip directional icons.\n6. `tsc`, then load the Arabic route and verify the whole layout mirrors —\n sidebar on the right, text right-aligned, arrows pointing the other way.\n\n## What NOT to do\n\n- Don't use physical `left`/`right` (or `ml`/`mr`) in any new layout style — even\n in an English-only app. Writing logical from the start is the seam Arabic\n slots into, exactly like tokens are for copy.\n- Don't fake RTL with `flex-direction: row-reverse`, reversed DOM order, or\n per-locale `if (rtl)` layout branches. Set `dir` once; let layout follow.\n- Don't set `dir` on individual components — it belongs on `<html>` so the whole\n document (and Mantine) agrees.\n- Don't translate Arabic copy outside the `t()` token system; an RTL language is\n a normal locale, governed by `pikku-i18n`.\n", "pikku-scenario/SKILL.md": "---\nname: pikku-scenario\ndescription: >-\n Use when writing or running Pikku scenarios, or when asked to test Pikku functions or improve\n test coverage. A scenario (pikkuScenario) drives the app the way users do — steps run as actors\n over the real transport against a running server — so a flow doubles as an e2e test and a\n staged/production health check. Covers scenario.do / expectEventually / expectError /\n expectService, declared steps via pikkuScenarioStep (including browser steps driven by\n @pikku/playwright) written as intent rather than as clicks, with the actions factored into\n shared browser utilities, actors and environments in pikku.config.json, SCENARIO_ACTOR_SECRET, the\n `pikku scenario list|run` commands, live function coverage via `pikku dev --coverage`, and\n plain unit tests for pure function logic. TRIGGER when: user asks about scenarios, testing a\n Pikku function, test coverage, end-to-end flows, browser/UI e2e, or health checks. DO NOT\n TRIGGER when: user asks about running an existing test suite (use Bash) or CI configuration.\ninstallGroups: [core]\n---\n\n# Pikku Scenarios\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing: `pikku scenario list` for what exists, `pikku info functions --verbose` for what a scenario can call.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, or build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated.\n4. Validate with the narrowest relevant command first, then `pikku all --tsc` when functions, wirings or schemas may have changed.\n5. If validation fails, fix the source cause and rerun. Do not paper over generated errors by editing generated files.\n\n**`pikku tests` does not exist.** It was removed in #865 — scenarios own coverage now. Any reference you find to it is stale.\n\n## What a scenario is\n\nA scenario is a `pikkuScenario` export that drives the app **as real actors over the real transport**, against a running server. That is what lets one artifact serve as both an e2e test and a staged/production health check.\n\nConsequences that matter, and bite if ignored:\n\n- **There is no state reset.** A scenario runs against a live server. Scope what you create (unique ids, your own rows) and never assume a clean database.\n- **Every effect runs as somebody, or as a declared step.** `scenario.do(...)` without `{ actor }` throws `Scenario tried to run '<rpc>' as an internal step…` — there is no bare internal-RPC step. The other way to do work is `scenario.step/given/when/then`, which runs a `pikkuScenarioStep`; its actor is optional (setup steps have none) unless it declares `browser: true`.\n- **Actors must be configured and signed in**, or the scenario cannot run.\n\nScenarios live in `srcDirectories` like any other function — by convention `*.scenario.ts`.\n\n## Writing one\n\n`pikkuScenario` comes from the **generated** workflow types, not `@pikku/core`:\n\n```typescript\nimport { pikkuScenario } from '#pikku/workflow/pikku-workflow-types.gen.js'\n\nexport const orderSupportScenario = pikkuScenario<\n { value?: number },\n { doubled: number; message: string }\n>({\n title: 'Order support (scenario)',\n tags: ['scenario'],\n func: async ({ logger }, data, { scenario, actors }) => {\n if (!actors?.shopper || !actors?.support) {\n throw new Error(\n 'orderSupportScenario needs run actors (shopper + support) — run via `pikku scenario run <environment>`'\n )\n }\n\n const doubled = await scenario.do(\n 'shopper doubles their order',\n 'doubleValue',\n { value: data?.value ?? 21 },\n { actor: actors.shopper }\n )\n\n const settled = await scenario.expectEventually(\n 'support sees the greeting settle',\n 'formatMessage',\n { greeting: 'Hello', name: 'Support' },\n (out: { message: string }) => out.message.length > 0,\n { actor: actors.support, within: '5s', interval: 50 }\n )\n\n return { doubled: doubled.result, message: settled.message }\n },\n})\n```\n\nA scenario takes the same config fields as a workflow (`title`, `description`, `tags`, `input`/`output`, `auth`, `permissions`, `middleware`, `version`, …). The third argument is the scenario context: `{ scenario, actors }`.\n\n### The scenario API\n\n| Call | Purpose |\n| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |\n| `scenario.do(step, rpc, data, { actor })` | Run an RPC as that actor. The step name is what appears in the run output. |\n| `scenario.expectEventually(step, rpc, data, predicate, { actor, within, interval })` | Poll until `predicate(out)` passes or `within` elapses. For anything asynchronous — queues, workers, eventual state. |\n| `scenario.expectError(step, rpc, data, { actor, matches })` | Assert the call **fails**. For fault injection and negative paths. |\n| `scenario.expectService(step, 'service.method', { actor, calledWith })` | Assert a stubbed service was called. Requires the server to run with `--test`. |\n| `scenario.step(step, stepName, data, { actor })` | Run a declared `pikkuScenarioStep`. `given`/`when`/`then` are the same call with a keyword in the rendered prose. |\n\n`expectEventually` is **scenario-only**. Calling it from a `pikkuWorkflowFunc` is a critical inspector error (`PKU675`) pointing you at `pikkuScenario`.\n\nPrefer `expectEventually` over sleeping.\n\n### Setup and teardown (`before` / `after`)\n\nA scenario config takes `before` and `after`. Both have the **same signature as `func`** — `(services, data, wire)` — with the return value discarded:\n\n```typescript\nconst resetsCredentials = async (_services, _data, { actors }) => {\n await actors!.admin!.invoke('resetCredentials', {})\n}\n\nexport const credentialScenario = pikkuScenario({\n title: 'A credential is loaded on first use',\n tags: ['scenario', 'credential'],\n before: resetsCredentials,\n after: removesInstalledAddon,\n func: async (services, data, { scenario, actors }) => { /* … */ },\n})\n```\n\n| Rule |\n| -------------------------------------------------------------------------------------------------------- |\n| `before` throwing skips the body and fails the run — but `after` still runs. |\n| `after` always runs, in a `finally`, whether the scenario passed or failed. |\n| `after` throwing fails a run that would otherwise have passed. |\n| `after` throwing on an already-failed run attaches as the `cause` and never replaces the original error. |\n| Neither runs when the run is suspended or waiting — teardown only fires at a terminal outcome. |\n| Hooks are **not** ladder rows. The runner records nothing for them; a failure is labelled by phase. |\n\nA hook reaches the app the same way the body does: through `wire.actors`. If you want cleanup to be *visible* on the ladder, make it an ordinary `scenario.then(...)` instead.\n\nHooks are scenario-only. A `before`/`after` on a `pikkuWorkflowFunc` never runs — a workflow is durable and resumable, so a callback that reran on every replay would have no honest meaning.\n\n### Grouping scenarios (`pikkuFeature`)\n\n`pikkuFeature` groups scenarios the way gherkin's `Feature:` groups `Scenario:`. Scenarios are referenced by **imported identifier**, so a renamed or deleted scenario is a compile error rather than a silent skip:\n\n```typescript\nimport { pikkuFeature } from '#pikku/workflow/pikku-workflow-types.gen.js'\nimport {\n credentialLazyLoadScenario,\n credentialRoundTripScenario,\n} from './credential.scenario.js'\n\nexport const credentialFeature = pikkuFeature({\n name: 'Credential API',\n description: 'Credentials resolve lazily and are scoped per user',\n tags: ['credential'],\n before: startsMockOAuthServer,\n after: stopsMockOAuthServer,\n scenarios: [\n credentialLazyLoadScenario,\n ...['stripe', 'google', 'hmac-key'].map((name) => ({\n scenario: credentialRoundTripScenario,\n data: { name },\n })),\n ],\n})\n```\n\n| Rule |\n| ---------------------------------------------------------------------------------------------------------------------------------------- |\n| The **export identifier is the feature's id**; `name` is the human-readable label. Both must be exported or the build fails. |\n| A `{ scenario, data }` entry is gherkin's `Examples:` — one run per entry. `data` is typed against that scenario's input. |\n| Feature hooks run **once around the whole group** (`before → a → b → c → after`), _not_ per scenario. `after` runs in a `finally`. |\n| There is deliberately **no `Background:`**. Per-scenario setup is the scenario's own `before`, referencing a shared function. |\n| A scenario's effective tags are its own **plus** the feature's, so `--tags credential` selects through the feature. |\n| A scenario need not belong to a feature — one with no input still runs standalone. |\n| Membership is resolved by **object identity** at runtime, which is why a loop works and why a scenario built inline in a feature is an error. |\n\nThe **feature is the run unit**: `--flows` on a scenario whose every feature entry carries `data` errors and names the features containing it, because the feature is what supplies that data. Use `--features` for those. A scenario referenced bare anywhere, or in no feature at all, still runs standalone.\n\n### Steps describe intent, not actions\n\nA scenario records what someone was **trying to do**, never the keystrokes they used to do it. This is the one decision that determines whether a suite survives its first redesign, and it applies to every step name you write.\n\n| Action ladder — wrong | Intent ladder — right |\n| -------------------------------------- | ---------------------------------------------- |\n| `Given opens /shop` | `Given the shopper is browsing the shop` |\n| `When clicks the category filter` | `When the shopper buys the £5 strawberry milkshake` |\n| `And clicks \"Drinks\"` | `Then it is in their basket` |\n| `And clicks the first product card` | |\n| `And clicks Add to basket` | |\n| `Then sees \"1 item\"` | |\n\nThree things go wrong with the left-hand column, and all three are expensive:\n\n- **A layout change rewrites every scenario that touched that screen.** In the right-hand column it rewrites one function.\n- **The report is the deliverable.** `buys the £5 strawberry milkshake` is readable by someone who has never seen the app; `clicks [data-testid=add]` tells them nothing about whether the product works.\n- **An action step cannot arrive on its own.** It assumes the previous click left the browser somewhere, so the scenario only runs front-to-back, as a whole, in one order.\n\nSo there are three layers, and only two of them are named in the report:\n\n| Layer | What it is | On the ladder |\n| ------------------------------ | --------------------------------------------------- | ------------- |\n| Scenario | The flow, written as intents | yes — the ladder |\n| Step (`pikkuScenarioStep`) | One intent | yes — one row |\n| Utility | An ordinary TS function over `browser` | no |\n\nUtilities are **not steps**. They are plain exported functions, they take the browser handle, and they hold the clicking:\n\n```typescript\n// shop.browser.ts — shared actions. Not steps: nothing here is an intent.\nimport type { PikkuBrowserWire } from '@pikku/core/workflow'\nimport type {} from '@pikku/playwright'\n\n/** Arrive on the shop, from wherever the browser happens to be. */\nexport const ensureOnShop = async (browser: PikkuBrowserWire) => {\n if (!new URL(browser.page.url()).pathname.startsWith('/shop')) {\n await browser.goto('/shop')\n }\n await browser\n .locate({ testId: 'product-grid' })\n .first()\n .waitFor({ state: 'visible' })\n}\n\nexport const searchFor = async (browser: PikkuBrowserWire, query: string) => {\n await browser.locate({ testId: 'shop-search' }).first().fill(query)\n await browser.page.keyboard.press('Enter')\n}\n\nexport const filterByCategory = async (\n browser: PikkuBrowserWire,\n category: string\n) => {\n await browser.locate({ testId: 'category-filter' }).first().click()\n await browser\n .locate({ testId: 'category-option', where: { 'data-category': category } })\n .first()\n .click()\n}\n\nexport const addToBasket = async (browser: PikkuBrowserWire, name: string) => {\n const card = browser\n .locate({ testId: 'product-card', containing: name })\n .first()\n await card.waitFor({ state: 'visible' })\n await card.locate('[data-testid=add-to-basket]').click()\n}\n```\n\nThe step composes them, and it is the step — one row — that the report shows:\n\n```typescript\nexport const buysTheItem = pikkuScenarioStep<\n { name: string },\n { name: string }\n>({\n name: 'buysTheItem',\n description: 'finds one item in the shop and puts it in the basket',\n template: 'buys the {name}',\n // One intent, one implementation per surface an actor can drive it through.\n browser: async (_services, { name }, { browser }) => {\n await ensureOnShop(browser)\n await searchFor(browser, name)\n await addToBasket(browser, name)\n return { name }\n },\n default: async ({ rpc }, { name }) => {\n const item = await rpc.invoke('findItemByName', { name })\n await rpc.invoke('addToBasket', { itemId: item.id })\n return { name }\n },\n})\n```\n\nThe two bindings are **alternatives**: `pikku scenario run --run browser` clicks through the shop, `--run default` (the fast suite) takes the server-side path, and both report the same sentence.\n\n```typescript\nawait scenario.when(\n 'buys a milkshake',\n 'buysTheItem',\n { name: '£5 strawberry milkshake' },\n { actor: actors.shopper }\n)\n// reporter renders: When the shopper buys the £5 strawberry milkshake ✓ 1.2s\n```\n\n**Every intent step begins by arriving.** `ensureOnShop` is not defensive noise — it is what lets a scenario start at any step, run alone, and be reordered without touching it. It checks first and navigates only if needed, so a scenario already on the shop pays nothing. This is about the *browser's* starting position, not the database: there is still no state reset (see above), and you still scope what you create.\n\n**The same utilities, a different intent.** A scenario about filtering has filtering as its subject, so there the filter *is* the intent — same helper, its own step:\n\n```typescript\nexport const filtersTheShop = pikkuScenarioStep<\n { category: string },\n { shown: number }\n>({\n name: 'filtersTheShop',\n description: 'narrows the catalogue to one category',\n template: 'filters the shop by {category}',\n browser: async (_services, { category }, { browser }) => {\n await ensureOnShop(browser)\n await filterByCategory(browser, category)\n return {\n shown: await browser.locate({ testId: 'product-card' }).count(),\n }\n },\n default: async ({ rpc }, { category }) => ({\n shown: (await rpc.invoke('listItems', { categorySlug: category })).length,\n }),\n})\n```\n\nTwo scenarios, two intents, one set of utilities. That is the shape to aim for: when a helper is reused by a step whose *subject* it is, promote it to a step there — never the reverse.\n\n**Non-browser steps need none of this.** Without a browser there is no navigation to absorb and no DOM to hide, so an intent maps to one RPC and `scenario.do` names it directly:\n\n```typescript\nconst order = await scenario.do(\n 'Shopper checks out',\n 'createOrder',\n { basketId, shippingAddress },\n { actor: actors.shopper }\n)\n```\n\nReach for a `pikkuScenarioStep` on the non-browser side only when one intent genuinely spans several RPCs, or when the step asserts something the RPC result alone does not say.\n\n### `then` bindings are witnesses, not alternatives\n\nThis is the one place the surface bindings do **not** behave like a switch, and it is the part worth reading twice.\n\nOn a `given` or `when`, the bindings are alternatives — clicking Buy and calling `createOrder` are two ways to cause one effect, so exactly one runs.\n\nOn a `then`, they are not two implementations of one assertion. They are two *different claims*:\n\n| binding | what it actually proves |\n| ------- | ----------------------- |\n| `default` | the order row says `paid` — the system of record is right |\n| `browser` | the confirmation panel says paid — the truth reached the human |\n\nThe gap between them is the bug nobody catches: 200 OK, database correct, user still watching a spinner. So a `then` runs **every** binding it declares and fails if they disagree.\n\n```typescript\nexport const seesTheOrderConfirmed = pikkuScenarioStep<\n { orderId: string },\n { status: string }\n>({\n name: 'seesTheOrderConfirmed',\n template: 'sees order {orderId} confirmed',\n // Both run on `--run browser`. Each returns what it observed, and the runner\n // compares them — so this fails when the page disagrees with the database.\n browser: async (_services, { orderId }, { browser }) => ({\n status: await browser\n .locate({ testId: 'order-status', where: { 'data-order': orderId } })\n .getAttribute('data-status'),\n }),\n default: async ({ rpc }, { orderId }) => ({\n status: (await rpc.invoke('getOrder', { orderId })).status,\n }),\n})\n```\n\nThree rules follow, and they are the ones that get broken:\n\n- **A browser witness must observe on the page.** One that quietly calls an RPC to check the result is worse than no binding at all — it reports a tick for a surface it never looked at.\n- **Return what you observed, don't just assert.** A witness returning a value lets the runner diff the two. A witness that only throws still works, but it can never disagree with anything, so it proves less. Read structured state with `where` on the test-id selector rather than parsing translated copy.\n- **A step with no binding for the run's surface is counted, not excused.** `--run browser` prints `n/m steps ran on browser` over *every* step, so an action that quietly fell back to the server lowers the number just as an assertion does. A `then` that fell back is additionally named — `--strict` fails on those, because a sentence saying the actor saw something nobody looked at is a different problem from a shortcut. Not being in the UI *is* the finding: do not add a browser binding that fakes it.\n\n**Always give a `then` a `default` witness.** It is the floor every run can fall back to, and an assertion with no witness the run can execute is fatal (`ScenarioNoWitness`) — not a coverage gap. The distinction is the point: a `then` checked server-side under `--run browser` did happen, it just wasn't seen where the prose claims; one checked nowhere never happened at all, and without the error it would return `undefined` and render as a tick. A browser-only `then` is therefore a step that fails the fast suite, which is rarely what you want.\n\n**Every scenario must assert.** A flow of only `given`/`when` is a PKU680 critical — it proves nothing threw. Since coverage counts every step, an assertion-free ladder of browser-bound actions would score a perfect `3/3` while checking nothing, so clicking through the UI and never looking at the result is the cheapest way to fake the number. The rule closes that.\n\nAssertions with no possible browser witness are a different thing and should not be written as a `then`: \"the audit log recorded it\" is a system check, and \"the receipt email arrives\" is `expectEventually`, which is always out-of-band and always server-side.\n\n### Declared steps (`pikkuScenarioStep`)\n\n`scenario.do` can only name an RPC. A **step** is a named, typed unit of scenario behaviour whose body is an ordinary pikku function — so it can call several RPCs as its actor, assert, or drive a browser.\n\n```typescript\nimport { pikkuScenarioStep } from '#pikku/workflow/pikku-workflow-types.gen.js'\nimport { requireActor } from '@pikku/core/workflow'\n\nexport const buysAnApple = pikkuScenarioStep<\n { qty: number },\n { orderId: string }\n>({\n name: 'buysAnApple',\n description: 'buys an apple',\n template: 'buys {qty} apples',\n func: async (_services, { qty }, { scenarioStep }) => {\n return await requireActor(scenarioStep).invoke('placeOrder', { qty })\n },\n})\n```\n\n```typescript\nawait scenario.given(\n 'buys an apple',\n 'buysAnApple',\n { qty: 1 },\n { actor: actors.shopper }\n)\n// reporter renders: Given the shopper buys 1 apples ✓ 412ms\n```\n\nRules that bite:\n\n- **The step is referenced by its typed string name, not by importing the const** — exactly like `workflow.do`. The name is the step's `pikkuFuncId` and is checked against the generated step map. A non-literal target is a critical error (`PKU678`).\n- **Steps are not RPCs.** They are deliberately never network-callable — a browser-driving step must not be.\n- **`actor.invoke` is typed over the exposed RPC map**, so the name and the payload are checked and the result comes back narrowed — no cast. `actor.invokeRaw(name, data, { headers })` is the same call reporting `{ status, ok, body }` instead of throwing; use it whenever the refusal *is* the assertion.\n- **`actor` and `env` are optional on the wire**, because a pure assertion step needs neither. Narrow them with `requireActor(scenarioStep)` and `requireScenarioEnv(scenarioStep)` from `@pikku/core/workflow` rather than a local guard — both name the step and say what to pass. `env` is `{ apiUrl, appUrl? }` from the environment the run targets, and is how a raw-HTTP step learns the target's URL: a step runs in the CLI process, where there is no `variables` service and `process.env` is not the answer.\n- **Steps default to `retries: 0`**, unlike ordinary workflow steps. Retrying a failed assertion is wrong; pass `retries` explicitly if a step is genuinely flaky-by-nature.\n- **Step results are persisted**, so return JSON-serialisable data — never a `Locator` or a client object.\n- **`description` documents the step; `template` is what the report renders.** `template`'s `{placeholders}` are filled from the input the step was called with, so one step reads differently for each call — `sees {state} addon {packageName}` reports as \"sees available addon @pikku/addon-stripe\". Reflect every input field in the template, and type the values so they read as words (`state?: 'installed' | 'available'`, not `installed?: boolean`). A placeholder with no value renders as nothing and the whitespace collapses.\n- Prose precedence is `options.description` → the step's `template` → the step's own `description` → the positional step name. Repeated names get `#1`, `#2` ordinals, so a `for` loop over a data set is how you write a Scenario Outline. A loop-generated step name is not statically known, so it is matched back to its declaration by step function instead — which works as long as that function's call sites agree on their phase, actor and prose. Two call sites that disagree make the loop step report under its bare runtime name.\n\n### Browser steps\n\n`browser` is a boolean on the step, and it is the whole switch: `browser: true` and a browser is guaranteed present on the wire, `browser: false` (the default) and there is none. There is no third state and nothing to null-check — `wire.browser` is optional in the type only for the steps that did not ask for one.\n\nA step declaring `browser: true` gets `wire.browser` — a session bound to **its actor**, signed in through the same `signInPath` + `SCENARIO_ACTOR_SECRET` path the HTTP actors use, so the browser and the RPC calls are one identity. Calling such a step without an actor is a critical error (`PKU677`).\n\nBrowser steps are where **intent, not actions** earns its keep: the step is one intent, the clicking lives in shared utilities, and the step arrives before it acts. Write the mechanics below into utilities and keep the step body to three or four calls that read as a sentence.\n\n```typescript\nexport const opensTheCart = pikkuScenarioStep<\n { path: string },\n { url: string },\n true\n>({\n name: 'opensTheCart',\n description: 'opens the cart',\n browser: true,\n func: async (_services, { path }, { browser }) => {\n await browser.goto(path)\n return { url: browser.page.url() }\n },\n})\n```\n\n- Install `@pikku/playwright` and `@playwright/test`, and import `@pikku/playwright` once (`import type {} from '@pikku/playwright'`) so `browser.page` is a typed Playwright `Page`. Without it you still get the structural `goto`/`screenshot` handle.\n- The environment needs an `appUrl` beside its `apiUrl`. `pikku scenario run` fails fast before running anything if a browser scenario has no `appUrl` or the driver is not installed.\n- `pikku scenario run <env> --no-browser` **skips** scenarios containing browser steps and reports them as skipped — it does not fail them. That is how a machine with no browser stays green.\n- Playwright auto-waits; do not wrap `page.click` in `expectEventually`.\n\n## Configuration\n\nPersonas, actors and environments live in `pikku.config.json`:\n\n```json\n{\n \"scenarios\": {\n \"personas\": {\n \"shopper\": { \"description\": \"Buys things here\", \"primary\": true },\n \"support\": { \"description\": \"Answers for the shop\", \"proficiency\": \"power\" },\n \"reminders\": { \"description\": \"The shop chasing abandoned carts\", \"kind\": \"system\" }\n },\n \"actors\": {\n \"shopper\": {\n \"email\": \"shopper@actors.local\",\n \"name\": \"Shopper\",\n \"jobTitle\": \"First-time buyer\",\n \"personality\": \"Impatient shopper who abandons slow checkouts\"\n },\n \"shopperB\": { \"persona\": \"shopper\", \"email\": \"shopper-b@actors.local\" }\n },\n \"environments\": {\n \"local\": {\n \"apiUrl\": \"http://localhost:4077\",\n \"signInPath\": \"/api/auth/sign-in/actor\"\n }\n }\n }\n}\n```\n\n### Personas and actors\n\nA **persona** is a kind of person; an **actor** is one body that signs in as one. Above, `support` is declared only as a persona — its actor is materialised (`support@actors.local`), so `actors.support` works without an `actors` entry. Write an actor by hand only when you need something the materialised one wouldn't have:\n\n- a **real email or personality** for it, like `shopper`;\n- a **second body of the same persona**, like `shopperB` — which is what tenant isolation, peer sharing, and \"another member's row\" scenarios are made of. Two actors of one persona must be two different users, so **two actors sharing an email is an error**.\n\nA persona holds only what is true of that kind of person for the app's whole lifetime — `description`, `primary` (whose experience the product is), `kind`, `proficiency`. What someone is trying to get done, and the circumstances they are doing it in, belong to the **scenario**, not to them.\n\n`kind: \"system\"` is the app acting on its own — a schedule, a cleanup, a send. It gets **no actor**: there is nobody to sign in. Give it one by hand only if it genuinely has a service account.\n\nAn actor with no `persona` is its own persona, so a project that never declares any keeps working unchanged.\n\n- `environments.<name>.apiUrl` is required. `signInPath` defaults to `/auth/sign-in/actor`, `rpcPath` to `/rpc`.\n- **`SCENARIO_ACTOR_SECRET` is an environment variable and never goes in `pikku.config.json`.** It signs actors in. `pikku scenario run` throws without it; a server auto-building actors warns and runs without them.\n\n## Running\n\n```bash\npikku scenario list # features with their scenarios indented, then ungrouped scenarios\nSCENARIO_ACTOR_SECRET=… pikku scenario run local\nSCENARIO_ACTOR_SECRET=… pikku scenario run local --flows orderSupportScenario\nSCENARIO_ACTOR_SECRET=… pikku scenario run local --features credentialFeature\nSCENARIO_ACTOR_SECRET=… pikku scenario run local --tags smoke,scenario\n```\n\n`run` takes the environment as a **required positional** — the key from `environments`. `--flows`/`-f` filters by scenario name, `--features` by feature id, `--tags`/`-t` by tag (match-any). Every filter narrows the same plan, so narrowing a feature to two of its five scenarios still runs the feature's hooks exactly once around those two.\n\nOutput is `PASS <name> (<ms>) → <output>` / `FAIL <name> (<ms>): <error>`, then `N/M scenarios passed against '<env>'`. A scenario inside a feature is named `<Feature> › <scenario> <data>`.\n\n**Exit code is 1** if any scenario fails _or_ if no scenario matched the filter — a typo'd `--flows` is a hard error, not a silent zero-run pass. It throws outright on an unknown environment, an unknown flow name, or a missing `SCENARIO_ACTOR_SECRET`.\n\n## Coverage\n\nCoverage is attributed by running scenarios against a server that is collecting it. It is **not** derived from unit tests.\n\nPrerequisite in `pikku.config.json`:\n\n```json\n{ \"scaffold\": { \"scenarios\": \"auth\" } }\n```\n\n`scaffold.scenarios` generates the coverage and stub RPCs into your project (`pikkuScenarioTakeLiveCoverage`, `pikkuScenarioResetLiveCoverage`, `pikkuScenarioResetStubs`, `pikkuScenarioGetStubCalls`), so scenario runs work against any server. The coverage RPC reads `<outDir>/function/pikku-functions-meta-verbose.gen.json` off disk at request time — codegen always writes it, but it has to be deployed alongside the app or the RPC returns `null`.\n\n```bash\npikku dev --coverage # V8 precise coverage, in-process\npikku dev --coverage --test # also enable stubs (needed for expectService)\nSCENARIO_ACTOR_SECRET=… pikku scenario run local --coverage\n```\n\nThe run resets coverage before each scenario and snapshots after, writing **`<outDir>/coverage/scenario-coverage.json`**:\n\n```jsonc\n{\n \"generatedAt\": \"…\",\n \"environment\": \"local\",\n \"scenarios\": {\n \"<name>\": {\n /* FunctionCoverageReport */\n },\n },\n}\n```\n\nCoverage is best-effort: it disables itself with a warning if the server is not collecting or the first actor cannot invoke, and it needs at least one configured actor. If you get no coverage, check those first.\n\n**There is no AI-prompt output.** The old `--ai-out` flag died with `pikku tests`; nothing replaced it. To find what needs work, read `scenario-coverage.json` yourself and cross-reference `pikku meta functions list` for input/output schemas.\n\n### Filling coverage\n\n1. `pikku scenario run <env> --coverage`, then read `<outDir>/coverage/scenario-coverage.json` to see what is unexercised.\n2. `pikku meta functions list` for those functions' schemas.\n3. Write a `pikkuScenario` that reaches them **through a real user flow** with an actor — not a scenario per function. Scenarios are flows; coverage is a consequence.\n4. Re-run to confirm.\n\n## Unit tests for pure logic\n\nScenarios are the repo-idiomatic way to test functions, and the only thing that contributes to live coverage. For pure logic with heavy branching, a plain unit test calling `func` directly is still valid and cheap:\n\n```typescript\nimport { describe, test } from 'node:test'\nimport assert from 'node:assert'\n\ndescribe('createTodo', () => {\n test('creates a todo', async () => {\n const services = {\n todoStore: { add: async (title: string) => ({ id: '1', title }) },\n }\n const result = await createTodo.func(services as any, { title: 'Buy milk' })\n assert.equal(result.title, 'Buy milk')\n })\n})\n```\n\n```bash\nnode --import tsx --test src/**/*.test.ts\n```\n\nServices are plain objects — a Pikku function is pure business logic, so a mock is just the shape the function destructures. Build real services via the `pikkuServices` / `pikkuWireServices` factories when a test needs them.\n\n## Red flags\n\n| Smell | Why it's wrong |\n| --------------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| `pikku tests …` | Removed in #865. Use `pikku scenario`. |\n| `.feature` files / Gherkin for function tests | Scenarios are TypeScript, not Gherkin. The in-process cucumber function world was deleted. |\n| `scenario.do(...)` with no `{ actor }` | Throws. Every step runs as somebody. |\n| A scenario per function | Scenarios are user flows. One flow covers many functions; that is the point. |\n| Assuming a clean database | There is no state reset — it may be a staging server. Scope what you create. |\n| `sleep()` before asserting | Use `expectEventually`. |\n| A step named `clicksAddToBasket` / `opensThePage` | That is an action, not an intent. Name the step for what the actor wanted; put the clicking in a utility. |\n| A browser step that assumes it is already on a page | It can then only run mid-flow. Arrive first — check the URL, navigate if needed. |\n| A `browser: true` step guarding `if (!browser)` | `browser: true` guarantees it. The guard hides the real error, which is a missing actor (`PKU677`). |\n| `expectEventually` in a `pikkuWorkflowFunc` | `PKU675` — scenario-only. |\n| Coverage silently 0 | Server not run with `--coverage`, verbose functions meta not deployed, `scaffold.scenarios` unset, or no actors configured. |\n\n`@pikku/cucumber` is a **browser/e2e** harness (`Actor`, `BrowserWorld`, `PersonaData`, `DbUtils`) — out of scope here.\n\nSee `pikku-concepts` for the core mental model.\n", "pikku-schedule/SKILL.md": "---\nname: pikku-schedule\ndescription: >-\n Use when setting up in-memory cron scheduling in a Pikku app. Covers InMemorySchedulerService\n for running scheduled tasks. TRIGGER when: code uses InMemorySchedulerService,\n PikkuTaskScheduler, or user asks about in-memory scheduling, cron jobs without external\n dependencies, or @pikku/schedule. DO NOT TRIGGER when: user asks about cron wiring (use\n pikku-cron) or queue-based scheduling with BullMQ/PgBoss (use pikku-queue).\ninstallGroups: [core]\n---\n\n# Pikku Schedule (In-Memory Scheduler)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/schedule` provides an in-memory cron scheduler for running Pikku scheduled functions without external dependencies like Redis or PostgreSQL.\n\n## Installation\n\n```bash\nyarn add @pikku/schedule\n```\n\n## API Reference\n\n### `InMemorySchedulerService`\n\n```typescript\nimport { InMemorySchedulerService } from '@pikku/schedule'\n\nconst scheduler = new InMemorySchedulerService()\n```\n\nImplements the scheduler service interface. Schedules are held in memory — they do not survive process restarts. Suitable for development and single-instance deployments.\n\n## Usage Patterns\n\n### Basic Setup\n\n```typescript\nimport { InMemorySchedulerService } from '@pikku/schedule'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const scheduler = new InMemorySchedulerService()\n return { config, scheduler }\n})\n```\n\nFor distributed or persistent scheduling, use BullMQ (`BullSchedulerService`) or PgBoss (`PgBossSchedulerService`) from the queue packages instead. See `pikku-queue` for details.\n", "pikku-schema-ajv/SKILL.md": "---\nname: pikku-schema-ajv\ndescription: >-\n Use when setting up JSON schema validation with AJV in a Pikku app. Covers AjvSchemaService for\n request/response validation. TRIGGER when: code uses AjvSchemaService, user asks about AJV, JSON\n schema validation, or @pikku/schema-ajv. DO NOT TRIGGER when: user asks about Cloudflare Workers\n schema validation (use pikku-schema-cfworker).\ninstallGroups: [core]\n---\n\n# Pikku Schema AJV (JSON Schema Validation)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/schema-ajv` provides JSON schema validation using [AJV](https://ajv.js.org/). Implements the `SchemaService` interface from `@pikku/core`. This is the default schema validator for Node.js environments.\n\n## Installation\n\n```bash\nyarn add @pikku/schema-ajv\n```\n\n## API Reference\n\n### `AjvSchemaService`\n\n```typescript\nimport { AjvSchemaService } from '@pikku/schema-ajv'\n\nconst schema = new AjvSchemaService(logger: Logger)\n```\n\n**Methods:**\n\n- `compileSchema(schema: string, value: any): void` — Compile and register a JSON schema\n- `validateSchema(schemaName: string, json: any): void` — Validate data against a compiled schema (throws on failure)\n- `getSchemaNames(): Set<string>` — Get all registered schema names\n- `getSchemaKeys(schemaName: string): string[]` — Get property keys for a schema\n\n## Usage Patterns\n\n### With Pikku Services\n\n```typescript\nimport { AjvSchemaService } from '@pikku/schema-ajv'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new ConsoleLogger()\n const schema = new AjvSchemaService(logger)\n return { config, logger, schema }\n})\n```\n\nPikku automatically uses the schema service to validate function inputs and outputs when schemas are defined in your function definitions.\n", "pikku-schema-cfworker/SKILL.md": "---\nname: pikku-schema-cfworker\ndescription: >-\n Use when setting up JSON schema validation for Cloudflare Workers in a Pikku app. Covers\n CFWorkerSchemaService as a lightweight alternative to AJV. TRIGGER when: code uses\n CFWorkerSchemaService, user asks about schema validation on Cloudflare Workers, or\n @pikku/schema-cfworker. DO NOT TRIGGER when: user asks about AJV schema validation (use\n pikku-schema-ajv).\ninstallGroups: [core, fabric]\n---\n\n# Pikku Schema CFWorker (Cloudflare Workers Validation)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/schema-cfworker` provides JSON schema validation using [@cfworker/json-schema](https://github.com/cfworker/cfworker), a lightweight validator compatible with Cloudflare Workers (no `eval` or `new Function`). Implements the `SchemaService` interface from `@pikku/core`.\n\n## Installation\n\n```bash\nyarn add @pikku/schema-cfworker\n```\n\n## API Reference\n\n### `CFWorkerSchemaService`\n\n```typescript\nimport { CFWorkerSchemaService } from '@pikku/schema-cfworker'\n\nconst schema = new CFWorkerSchemaService(logger: Logger)\n```\n\n**Methods:**\n\n- `compileSchema(schema: string, value: any): void` — Compile and register a JSON schema\n- `validateSchema(schemaName: string, json: any): void` — Validate data against a compiled schema (throws on failure)\n- `getSchemaNames(): Set<string>` — Get all registered schema names\n- `getSchemaKeys(schemaName: string): string[]` — Get property keys for a schema\n\n## Usage Patterns\n\n### With Cloudflare Workers\n\n```typescript\nimport { CFWorkerSchemaService } from '@pikku/schema-cfworker'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new ConsoleLogger()\n const schema = new CFWorkerSchemaService(logger)\n return { config, logger, schema }\n})\n```\n\nUse this instead of `@pikku/schema-ajv` when deploying to Cloudflare Workers, as AJV uses `eval` which is not permitted in the Workers runtime.\n", "pikku-security/SKILL.md": "---\nname: pikku-security\ndescription: >-\n Use when adding authentication or session management to a Pikku app — pikkuAuth, session\n lifecycle (setSession/clearSession), built-in auth strategies (authBearer, authCookie,\n authAPIKey), or JWT setup. TRIGGER when: user asks about login, logout, session, bearer tokens,\n cookie auth, API keys, or JWT. DO NOT TRIGGER when: user asks about middleware (use\n pikku-middleware), permissions/authorization checks (use pikku-permissions), or secrets/env vars\n (use pikku-config).\ninstallGroups: [core]\n---\n\n# Pikku Security (Authentication & Sessions)\n\n## Agent Operating Procedure\n\n1. Discover before editing. Run `pikku info middleware --verbose` and `pikku info functions --verbose` to understand existing auth setup.\n2. Auth strategies live in wirings files — do not put `addHTTPMiddleware` calls inside function bodies.\n3. Validate with `pikku all --tsc` after changes — it regenerates and then type-checks in one pass, and fails on type errors. Use `--tsc-summary` for a compact one-line-per-error report.\n\nFor **middleware** (including tag middleware and service-to-service bearer auth) see `pikku-middleware`.\nFor **permissions** (pikkuPermission, pikkuAuth, per-function authorization) see `pikku-permissions`.\n\n## Session Management\n\n```typescript\n// Read session in pikkuFunc (session guaranteed to exist)\nconst getProfile = pikkuFunc({\n func: async ({ db }, _data, { session }) => {\n return await db.getUser(session.userId)\n },\n})\n\n// Set session (e.g., after login)\nconst login = pikkuFunc({\n auth: false,\n func: async ({ jwt, db }, { email, password }, { setSession }) => {\n const user = await db.verifyCredentials(email, password)\n setSession({ userId: user.id })\n return { token: jwt.sign({ userId: user.id }) }\n },\n})\n\n// Clear session (logout)\nconst logout = pikkuFunc({\n func: async ({}, _data, { clearSession }) => {\n clearSession()\n },\n})\n```\n\n## Built-in Auth Strategies\n\nApply these via `addHTTPMiddleware` in a wirings file:\n\n```typescript\nimport { authBearer, authCookie, authAPIKey } from '@pikku/core/middleware'\nimport { addHTTPMiddleware } from '@pikku/core/http'\n\n// JWT bearer token — reads Authorization header\naddHTTPMiddleware('*', [authBearer()])\n\n// Cookie-based sessions — auto-refreshes JWT\naddHTTPMiddleware('*', [\n authCookie({\n name: 'session',\n expiresIn: { value: 30, unit: 'day' },\n options: { httpOnly: true, secure: true },\n }),\n])\n\n// API key — from x-api-key header or ?apiKey= query param\naddHTTPMiddleware('*', [authAPIKey({ source: 'all' })])\n```\n\n## Complete Example\n\n```typescript\n// permissions.ts\nimport { pikkuAuth, pikkuPermission } from '#pikku'\n\nexport const isAuthenticated = pikkuAuth(async (_services, session) => !!session)\nexport const isVerified = pikkuAuth(async (_services, session) => !!session?.emailVerified)\n\n// wirings/auth.wiring.ts\nimport { authCookie } from '@pikku/core/middleware'\nimport { addHTTPMiddleware } from '@pikku/core/http'\n\naddHTTPMiddleware('*', [\n authCookie({ name: 'session', expiresIn: { value: 30, unit: 'day' } }),\n])\n\n// functions/auth.functions.ts\nexport const login = pikkuFunc({\n auth: false,\n func: async ({ jwt, db }, { email, password }, { setSession }) => {\n const user = await db.verifyCredentials(email, password)\n setSession({ userId: user.id })\n return { token: jwt.sign({ userId: user.id }) }\n },\n})\n\nexport const logout = pikkuFunc({\n func: async ({}, _data, { clearSession }) => {\n clearSession()\n },\n})\n```\n", "pikku-services/references/audit-wire-service.md": "# Audit Wire Service\n\n`createInvocationAudit` creates a per-request `InvocationAuditLog` that buffers audit events in memory and flushes them as a batch when the function-runner calls `closeWireServices` at the end of the request. If `singletonServices.audit` is not configured (local dev without Fabric), it returns a no-op `DisabledInvocationAudit` — no crash, events are silently dropped.\n\nPair with `createAuditedKysely` to auto-capture every Kysely query as an audit event.\n\n```typescript\n// services.ts\nimport { createInvocationAudit } from '@pikku/core/services'\nimport { createAuditedKysely } from '@pikku/kysely'\n\nexport const createWireServices = pikkuWireServices(async (singletonServices, wire) => {\n const audit = createInvocationAudit(singletonServices.audit, wire)\n const kysely = singletonServices.kysely\n ? createAuditedKysely(singletonServices.kysely, { audit })\n : undefined\n return { audit, ...(kysely ? { kysely } : {}) }\n})\n```\n\nThe `audit` wire service is typed as `AuditLog` (from `@pikku/core`). Functions that emit custom events use it directly:\n\n```typescript\nconst deleteUser = pikkuFunc({\n func: async ({ audit }, { userId }) => {\n await audit.audit({ type: 'user.deleted', actor_user_id: userId })\n // ...\n },\n})\n```\n\n`closeWireServices` (called automatically by the function-runner) invokes `audit.close()` → `singletonServices.audit.write(batch)` → platform-specific flush (e.g. CF Queue, libsql INSERT). No manual flushing needed.\n\n> **Fabric note:** Fabric provisions the audit queue and consumer worker automatically. The audit table schema is in `db/sqlite/0003-audit.sql` (starter-template). Run `pikku fabric validate` to confirm the migration is in place.\n", "pikku-services/SKILL.md": "---\nname: pikku-services\ndescription: >-\n Use when setting up dependency injection, creating custom services, or configuring the service\n layer in a Pikku app. Covers pikkuServices (singleton), pikkuWireServices (per-request), service\n typing, built-in services, and tree-shaking. TRIGGER when: code uses\n pikkuServices/pikkuWireServices, user asks about services.ts, dependency injection, service\n factories, or built-in services (ConsoleLogger, JoseJWTService). DO NOT TRIGGER when: user asks\n about auth middleware (use pikku-security) or secrets/variables (use pikku-config).\ninstallGroups: [core]\n---\n\n# Pikku Services (Dependency Injection)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nPikku uses factory functions for dependency injection. Singleton services are created once at startup; wire services are created fresh per request/job/command. See `pikku-concepts` for the core mental model.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See which services existing functions use\npikku info tags --verbose # Understand project organization\n```\n\n## API Reference\n\n### `pikkuServices(factory)` — singleton services (created once at startup)\n\n```typescript\nimport { pikkuServices } from '#pikku'\nimport { ConsoleLogger } from '@pikku/core/services'\nimport { JoseJWTService } from '@pikku/jose'\n\nexport const createSingletonServices = pikkuServices(\n async (config, existingServices?) => {\n // config: your CoreConfig object\n // existingServices: optional, for chaining factories\n const logger = new ConsoleLogger()\n const database = new DatabasePool(config.database)\n await database.connect()\n const jwt = new JoseJWTService(\n async () => [{ id: 'my-key', value: config.jwtSecret }],\n logger\n )\n return { config, logger, database, jwt, books: new BookService() }\n }\n)\n```\n\n### `pikkuWireServices(factory)` — per-request services (fresh per HTTP request, queue job, CLI command, etc.)\n\n```typescript\nimport { pikkuWireServices } from '#pikku'\n\nexport const createWireServices = pikkuWireServices(\n async (singletonServices, wire) => {\n // singletonServices: all singleton services\n // wire: transport context (session, channel, etc.)\n // Pikku merges these with singleton services automatically\n return {\n userSession: createUserSessionService(wire),\n dbTransaction: new DatabaseTransaction(singletonServices.database),\n }\n }\n)\n```\n\n### Auto-Generated Service Manifest\n\nAfter `npx pikku all`, Pikku generates `.pikku/pikku-services.gen.ts`, a manifest of which services are actually used by wired functions:\n\n```typescript\nexport const requiredSingletonServices = {\n database: true, // used by getUser, deleteUser\n audit: true, // used by deleteUser\n cache: false, // not used by any wired function\n jwt: true, // used by auth middleware\n} as const\n\nexport type RequiredSingletonServices = Pick<\n SingletonServices,\n 'database' | 'audit' | 'jwt'\n> &\n Partial<Omit<SingletonServices, 'database' | 'audit' | 'jwt'>>\n```\n\n## Usage Patterns\n\n### Using Services in Functions\n\n**Every service must be declared in `SingletonServices` (or `Services`) in `application-types.d.ts`.** Never access a service via a body-level cast (`services as typeof services & { myService: MyService }`) — that means the type is missing. Add the import and the field to `SingletonServices`, then destructure inline in the function signature. The inspector emits `SERVICES_NOT_DESTRUCTURED` and tree-shaking breaks when the first param is a plain identifier rather than an object pattern. Never `new` a service inside a function — services arrive only via injection.\n\n```typescript\n// ✅ Correct — inline destructure, no cast\nconst getUser = pikkuFunc({\n title: 'Get User',\n func: async ({ db, logger, jwt }, { userId }) => {\n logger.info('Fetching user', { userId })\n return { user: await db.getUser(userId) }\n },\n})\n\n// ❌ Wrong — named param + body cast; inspector warns + tree-shaking breaks\nconst getUser = pikkuFunc({\n func: async (services, { userId }) => {\n const { db } = services as typeof services & { db: DbService }\n // ...\n },\n})\n```\n\n### Services Are Never Optional Inside a Function\n\n**Never write a `if (!service) throw ...` existence guard in a function body.** It is dead code, and it defeats the platform.\n\nOptionality lives in exactly one place — `services.ts` / the `SingletonServices` declaration — and it means *\"this may not be created\"*, not *\"this may be missing at call time\"*. A service is optional precisely because **nothing destructures it**, and `requireSingletonServices` therefore never creates it. The moment any wired function destructures it, Pikku creates it and guarantees it is there.\n\nThe types enforce this rather than merely documenting it. The inspector records the services destructured by every wired `func`, `permissions` **and** `middleware`, and emits them as `RequiredSingletonServices`. The generated function types then default their service parameter to:\n\n```typescript\nexport type WiredSingletonServices = RequiredSingletonServices & SingletonServices\nexport type WiredServices = RequiredSingletonServices & Services\n```\n\nso a service that is `foo?: Foo` in `SingletonServices` arrives as a non-optional `Foo` in every function, permission and middleware that uses it. There is nothing to guard against.\n\n```typescript\n// ✅ Correct — destructure and use; creation is guaranteed by the manifest\nconst listThreads = pikkuFunc({\n func: async ({ agentRunService }, { threadId }) => {\n return await agentRunService.getThreadMessages(threadId)\n },\n})\n\n// ❌ Wrong — unreachable guard; signals a misunderstanding of service wiring\nconst listThreads = pikkuFunc({\n func: async ({ agentRunService }, { threadId }) => {\n if (!agentRunService) throw new MissingServiceError('agentRunService')\n return await agentRunService.getThreadMessages(threadId)\n },\n})\n```\n\nIf a service really is conditional at runtime (e.g. an optional integration a deployment may not configure), that is a **configuration** concern: branch on config, or fail fast at startup in `services.ts` — not per-request in every function.\n\n### Dynamic Import Optimization\n\nUse the generated manifest to conditionally import heavy dependencies — only the services actually wired get instantiated:\n\n```typescript\nimport { requiredSingletonServices } from '.pikku/pikku-services.gen.js'\n\nconst createSingletonServices = pikkuServices(async (config) => {\n const logger = new ConsoleLogger()\n\n let jwt: JWTService | undefined\n if (requiredSingletonServices.jwt) {\n const { JoseJWTService } = await import('@pikku/jose')\n jwt = new JoseJWTService(keys, logger)\n }\n\n let database: Database | undefined\n if (requiredSingletonServices.database) {\n database = await createDatabase(config.databaseUrl)\n }\n\n return { config, logger, jwt, database }\n})\n```\n\n### Audit Wire Service\n\n`createInvocationAudit` + `createAuditedKysely` add per-request audit buffering that flushes on request close (no-op if `audit` is unconfigured). For the full pattern, no-op behavior, custom-event usage, and Fabric notes, read `references/audit-wire-service.md`.\n\n### Built-in Services\n\n| Service | Package | Purpose |\n| -------------------------- | ---------------------- | -------------------------------- |\n| `ConsoleLogger` | `@pikku/core/services` | Console-based logging |\n| `JoseJWTService` | `@pikku/jose` | JWT sign/verify via jose |\n| `LocalSecretService` | `@pikku/core/services` | Local development secrets |\n| `LocalVariablesService` | `@pikku/core/services` | Local environment variables |\n| `PinoLogger` | `@pikku/pino` | Structured logging via Pino |\n| `createInvocationAudit` | `@pikku/core/services` | Per-request audit buffer |\n| `createAuditedKysely` | `@pikku/kysely` | Auto-capture DB queries as audit events |\n\n## Complete Example\n\n```typescript\n// services.ts\nimport { pikkuServices, pikkuWireServices } from '#pikku'\nimport { ConsoleLogger } from '@pikku/core/services'\nimport { JoseJWTService } from '@pikku/jose'\n\n// Custom service\nclass TodoStore {\n private todos: Map<string, Todo> = new Map()\n async create(title: string, priority: string) {\n const todo = { id: crypto.randomUUID(), title, priority, completed: false }\n this.todos.set(todo.id, todo)\n return todo\n }\n async get(id: string) { return this.todos.get(id) }\n async list() { return [...this.todos.values()] }\n async delete(id: string) { this.todos.delete(id) }\n}\n\nexport const createSingletonServices = pikkuServices(async (config) => {\n const logger = new ConsoleLogger()\n const jwt = new JoseJWTService(\n async () => [{ id: 'my-key', value: config.jwtSecret }],\n logger\n )\n return {\n config,\n logger,\n jwt,\n secrets: new LocalSecretService(),\n variables: new LocalVariablesService(),\n todoStore: new TodoStore(),\n }\n})\n\nexport const createWireServices = pikkuWireServices(\n async (singletonServices, wire) => ({\n scopedLogger: new ScopedLogger(wire.session?.initial?.userId),\n })\n)\n\n// functions/todos.functions.ts — services are auto-injected\nexport const createTodo = pikkuFunc({\n title: 'Create Todo',\n func: async ({ todoStore, logger }, { title, priority }) => {\n const todo = await todoStore.create(title, priority)\n logger.info('Created todo', { id: todo.id })\n return { todo }\n },\n})\n```\n", "pikku-software-archaeology/README.md": "# pikku-software-archaeology\n\nReverse-engineers an existing repository into a **Product Blueprint**: the product intelligence hidden inside an implementation (domains, entities, commands, queries, events, policies, workflows, invariants, integrations, gaps), extracted as schema-validated JSON that a generator — in our case Pikku — can rebuild from.\n\n```\nExisting Repository → pikku-software-archaeology → .knowledge/ blueprint → new Pikku application\n```\n\nThis is **not** a code indexer or doc generator. It extracts _intent over implementation_: `POST /api/users/:id/status` becomes the command `ActivateUser`; three scattered `if (inv.user_id !== req.user.id)` checks become one `InvoiceOwnerOnly` policy with three `enforcedAt` citations.\n\n## Design decision: the AI is the parser\n\nThere is deliberately **no scanner/AST tooling** in this skill. Static extraction is brittle and per-language (the first prototype's regex scanner broke before it ran once); the analyzing model already reads every language — JS, TS, Ruby, Python, PHP, Go — follows indirection, and understands intent. Determinism lives in the **output contract** instead: fixed file names, schema-validated shapes, sorted unordered collections (sequence-bearing arrays keep their observed order), and stable concept names, all enforced by a dumb JSON validator (`scripts/validate.mjs`). The audit is expensive; that's the trade we chose.\n\n## How to run it\n\nIn Claude Code, from (or pointing at) the target repo:\n\n> Use the pikku-software-archaeology skill to extract a product blueprint from /path/to/repo\n\nThe agent then:\n\n1. **Surveys** the repo (manifests, entry points, routes, jobs, webhooks, schema, config, TODO/HACK markers) — facts only.\n2. **Excavates the test suite** — `describe`/`it` names become workflow scenarios; assertions confirm policies and upgrade confidence; rules that exist _only_ in tests are captured.\n3. **Extracts** through twelve lenses (domains, entities, commands, …) per the pipeline in `SKILL.md`. Large repos fan out subagents per lens and merge.\n4. **Cross-checks and validates**:\n ```bash\n node .claude/skills/pikku-software-archaeology/scripts/validate.mjs <repo>/.knowledge\n ```\n The validator checks every file against `references/blueprint.schema.json` plus referential integrity across files (commands reference defined domains, api surfaces map to defined commands/queries, events have producers, …).\n5. Writes `blueprint.md`, the human synthesis.\n\nOutput lands in `<repo>/.knowledge/` — 14 core JSON files + `blueprint.md` (see `SKILL.md` for the full listing). Repos with a frontend and/or non-HTTP consumer channels also get an **optional consumer-surface layer**: `interfaces.json` (every way the product is used — web UI, CLI, MCP server for AI agents, OpenAPI/REST, SDK, realtime, webhooks) plus `frontend.json` / `frontend-routes.json` / `frontend-components.json`. The frontend component inventory's `rebuild` field is the key output: it separates trivially-rebuildable standard components from the **custom-logic** pieces (charts, complex tables, editors) that must be carefully ported. Backend-only repos omit these and the validator does not complain.\n\n### Incremental re-analysis\n\nConcept names are the stable IDs. On re-run after code changes, re-extract only the affected lens/domain, diff against the existing `.knowledge/`, and leave unrelated entries verbatim. Unordered collections are sorted so diffs stay reviewable; sequence-bearing arrays stay in observed order.\n\n## How Pikku consumes the blueprint\n\nFull mapping table in `references/pikku-mapping.md`. Summary: entities → Kysely migrations + Zod schemas; commands/queries → `pikkuFunc`s; api surfaces → `wireHTTP`; policies → shared permission functions (collapsing duplicated legacy checks); system workflows → `wireScheduler`/`wireQueueWorker`/`pikkuWorkflowFunc`; integrations → injected services with `defineSecret`/`defineCredential`; test-derived scenarios → `pikkuUserFlow` stories / e2e tests. Humans resolve `migration.json.decisionsNeeded` before any generation starts.\n\n## How uncertainty is represented\n\nEvery extracted concept carries:\n\n```json\n{\n \"evidence\": [\n {\n \"file\": \"controllers/invoices.js\",\n \"lines\": \"52\",\n \"note\": \"guard: only drafts editable\"\n }\n ],\n \"confidence\": \"high\"\n}\n```\n\n- **high** — the behavior itself is in the cited code/schema/test. Generates directly.\n- **medium** — inferred from converging signals. Generates with a review marker.\n- **low** — plausible reconstruction. Never auto-generated; surfaced for human review.\n\nTwo further distinctions keep facts and guesses separate:\n\n- `events[].explicit: false` — the event was _reconstructed_ from side-effect clusters (email + status flip), not emitted by the code.\n- Comments/docs vs code: comments describe intent, code describes behavior. Disagreements are recorded as the code's behavior plus a `gaps.json` entry.\n\n## Repo layout\n\n```\npikku-software-archaeology/\n├── SKILL.md # skill definition + extraction pipeline\n├── README.md # this file\n├── references/\n│ ├── blueprint.schema.json # the output contract (JSON Schema)\n│ └── pikku-mapping.md # blueprint → Pikku primitives\n└── scripts/\n └── validate.mjs # schema + cross-file validation (node, no deps)\n```\n", "pikku-software-archaeology/references/blueprint.schema.json": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"Product Blueprint (.knowledge/) contract\",\n \"description\": \"One schema per output file, keyed by filename under the `files` map. Every extracted concept carries evidence[] and confidence. Facts (observed in code) and inferences (reconstructed intent) must never be merged silently: confidence expresses how directly the evidence supports the claim.\",\n \"$defs\": {\n \"evidence\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"file\", \"lines\"],\n \"properties\": {\n \"file\": { \"type\": \"string\", \"description\": \"repo-relative path\" },\n \"lines\": { \"type\": \"string\", \"description\": \"e.g. '42' or '42-58'\" },\n \"note\": { \"type\": \"string\", \"description\": \"what this location shows\" }\n }\n }\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\"high\", \"medium\", \"low\"],\n \"description\": \"high = behavior directly observed in code/schema; medium = strong inference from multiple signals; low = plausible guess, needs human confirmation\"\n },\n \"conceptName\": {\n \"type\": \"string\",\n \"pattern\": \"^[A-Z][A-Za-z0-9]*$\",\n \"description\": \"PascalCase domain-language name (SendInvoice, InvoicePaid), never a route or filename\"\n }\n },\n \"files\": {\n \"product.json\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"purpose\", \"actors\", \"capabilities\", \"terminology\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"purpose\": { \"type\": \"string\", \"description\": \"1-3 sentences: what problem, for whom\" },\n \"targetUsers\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"actors\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"kind\", \"description\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"human\", \"system\", \"external\"] },\n \"description\": { \"type\": \"string\" }\n }\n }\n },\n \"capabilities\": { \"type\": \"array\", \"minItems\": 1, \"items\": { \"type\": \"string\" } },\n \"terminology\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"term\", \"meaning\"],\n \"properties\": { \"term\": { \"type\": \"string\" }, \"meaning\": { \"type\": \"string\" } }\n }\n },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n },\n \"domains.json\": {\n \"type\": \"object\",\n \"required\": [\"domains\"],\n \"properties\": {\n \"domains\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"description\", \"entities\", \"commands\", \"queries\", \"events\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\" },\n \"description\": { \"type\": \"string\" },\n \"entities\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"commands\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"queries\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"events\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"policies\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" }, \"description\": \"policy names from policies.json owned by this domain\" },\n \"sourcePaths\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"where this domain currently lives (usually scattered)\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"entities.json\": {\n \"type\": \"object\",\n \"required\": [\"entities\"],\n \"properties\": {\n \"entities\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"domain\", \"description\", \"attributes\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\" },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"description\": { \"type\": \"string\" },\n \"attributes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"type\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"type\": { \"type\": \"string\" },\n \"required\": { \"type\": \"boolean\" },\n \"notes\": { \"type\": \"string\" }\n }\n }\n },\n \"relationships\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"kind\", \"target\"],\n \"properties\": {\n \"kind\": { \"type\": \"string\", \"enum\": [\"belongs-to\", \"has-many\", \"has-one\", \"references\", \"many-to-many\"] },\n \"target\": { \"$ref\": \"#/$defs/conceptName\" },\n \"description\": { \"type\": \"string\" }\n }\n }\n },\n \"states\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"transitions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"from\", \"to\", \"trigger\"],\n \"properties\": {\n \"from\": { \"type\": \"string\" },\n \"to\": { \"type\": \"string\" },\n \"trigger\": { \"type\": \"string\", \"description\": \"command or event name that causes it\" }\n }\n }\n },\n \"ownership\": { \"type\": \"string\", \"description\": \"which actor/tenant owns rows of this entity\" },\n \"constraints\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"commands.json\": {\n \"type\": \"object\",\n \"required\": [\"commands\"],\n \"properties\": {\n \"commands\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"domain\", \"actor\", \"input\", \"preconditions\", \"effects\", \"eventsProduced\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\", \"description\": \"imperative VerbNoun: ApproveInvoice\" },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"actor\": { \"type\": \"string\" },\n \"trigger\": { \"type\": \"string\", \"description\": \"how it is invoked today (route, job, webhook, cron)\" },\n \"input\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"type\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"type\": { \"type\": \"string\" },\n \"required\": { \"type\": \"boolean\" }\n }\n }\n },\n \"preconditions\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"effects\": { \"type\": \"array\", \"minItems\": 1, \"items\": { \"type\": \"string\" } },\n \"eventsProduced\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"policies\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"policy names from policies.json enforced here\" },\n \"currentImplementation\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"file paths implementing it today\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"queries.json\": {\n \"type\": \"object\",\n \"required\": [\"queries\"],\n \"properties\": {\n \"queries\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"domain\", \"actor\", \"description\", \"returns\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\", \"description\": \"GetX / ListX / SearchX\" },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"actor\": { \"type\": \"string\" },\n \"description\": { \"type\": \"string\" },\n \"input\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"type\"],\n \"properties\": { \"name\": { \"type\": \"string\" }, \"type\": { \"type\": \"string\" }, \"required\": { \"type\": \"boolean\" } }\n }\n },\n \"returns\": { \"type\": \"string\" },\n \"scoping\": { \"type\": \"string\", \"description\": \"tenancy/visibility rule applied (e.g. 'rows owned by caller only')\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"events.json\": {\n \"type\": \"object\",\n \"required\": [\"events\"],\n \"properties\": {\n \"events\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"domain\", \"description\", \"producedBy\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\", \"description\": \"past-tense business fact: InvoicePaid. No technical events (ButtonClicked).\" },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"description\": { \"type\": \"string\" },\n \"producedBy\": { \"type\": \"array\", \"minItems\": 1, \"items\": { \"type\": \"string\" }, \"description\": \"command/workflow names\" },\n \"consumedBy\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"what reacts today (email send, status flip, webhook out)\" },\n \"payloadHints\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"explicit\": { \"type\": \"boolean\", \"description\": \"true if the code emits a real event; false if reconstructed from inline side-effects\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"policies.json\": {\n \"type\": \"object\",\n \"required\": [\"policies\"],\n \"properties\": {\n \"policies\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"rule\", \"type\", \"domain\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"$ref\": \"#/$defs/conceptName\" },\n \"rule\": { \"type\": \"string\", \"description\": \"plain-language statement: 'Only the invoice owner can send it'\" },\n \"type\": { \"type\": \"string\", \"enum\": [\"authorization\", \"validation\", \"state\", \"business-constraint\", \"rate-limit\"] },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"enforcedAt\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"every code location enforcing it — multiple locations = divergence risk, note in gaps.json\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"workflows.json\": {\n \"type\": \"object\",\n \"required\": [\"workflows\"],\n \"properties\": {\n \"workflows\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"kind\", \"actor\", \"trigger\", \"steps\", \"result\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"user\", \"admin\", \"system\"] },\n \"actor\": { \"type\": \"string\" },\n \"trigger\": { \"type\": \"string\" },\n \"steps\": { \"type\": \"array\", \"minItems\": 1, \"items\": { \"type\": \"string\" } },\n \"result\": { \"type\": \"string\" },\n \"commandsInvolved\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"eventsInvolved\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"schedule\": { \"type\": \"string\", \"description\": \"cron expression for system workflows, if any\" },\n \"scenarios\": {\n \"type\": \"array\",\n \"description\": \"concrete use cases, primarily excavated from the test suite (describe/it names, fixtures, assertions). A scenario sourced from a test is high-confidence by definition.\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"outcome\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"given\": { \"type\": \"string\" },\n \"when\": { \"type\": \"string\" },\n \"outcome\": { \"type\": \"string\" },\n \"fromTest\": { \"type\": \"string\", \"description\": \"test file path + test name, when sourced from a test\" }\n }\n }\n },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"api.json\": {\n \"type\": \"object\",\n \"required\": [\"surfaces\"],\n \"properties\": {\n \"surfaces\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"kind\", \"path\", \"auth\", \"mapsTo\", \"evidence\"],\n \"properties\": {\n \"kind\": { \"type\": \"string\", \"enum\": [\"rest\", \"graphql\", \"rpc\", \"webhook-in\", \"webhook-out\", \"page\", \"cli\", \"websocket\", \"sse\"] },\n \"method\": { \"type\": \"string\" },\n \"path\": { \"type\": \"string\" },\n \"auth\": { \"type\": \"string\", \"description\": \"none | session | jwt | api-key | signature | capability-url | ...\" },\n \"mapsTo\": {\n \"type\": \"object\",\n \"required\": [\"type\", \"name\"],\n \"properties\": {\n \"type\": { \"type\": \"string\", \"enum\": [\"command\", \"query\", \"event-ingress\"], \"description\": \"command/query for normal surfaces. event-ingress ONLY for surfaces that relay an external event without a domain command of their own; its name must be an events.json event. A webhook whose handler changes state maps to a command (usual case).\" },\n \"name\": { \"$ref\": \"#/$defs/conceptName\" }\n }\n },\n \"notes\": { \"type\": \"string\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" }\n }\n }\n }\n }\n },\n \"integrations.json\": {\n \"type\": \"object\",\n \"required\": [\"integrations\"],\n \"properties\": {\n \"integrations\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"category\", \"purpose\", \"direction\", \"importance\", \"replacementDifficulty\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"category\": { \"type\": \"string\", \"description\": \"payments | email | auth | storage | analytics | llm | sms | ...\" },\n \"purpose\": { \"type\": \"string\" },\n \"dataExchanged\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"direction\": { \"type\": \"string\", \"enum\": [\"outbound\", \"inbound\", \"both\"] },\n \"importance\": { \"type\": \"string\", \"enum\": [\"critical\", \"important\", \"peripheral\"] },\n \"replacementDifficulty\": { \"type\": \"string\", \"enum\": [\"trivial\", \"moderate\", \"hard\"] },\n \"configVia\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"env vars / secrets used\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"architecture.json\": {\n \"type\": \"object\",\n \"required\": [\"components\", \"datastores\"],\n \"properties\": {\n \"components\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"kind\", \"responsibility\", \"evidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"frontend\", \"api\", \"worker\", \"job\", \"webhook-handler\", \"cli\", \"service\", \"proxy\", \"other\"] },\n \"responsibility\": { \"type\": \"string\" },\n \"dependsOn\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"runtime\": { \"type\": \"string\", \"description\": \"how it runs today (process, cron, lambda, ...)\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" }\n }\n }\n },\n \"datastores\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"kind\", \"evidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\" },\n \"usedBy\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"notes\": { \"type\": \"string\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" }\n }\n }\n },\n \"notes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"deployment constraints worth preserving (ports, raw-body routes, ordering)\" }\n }\n },\n \"invariants.json\": {\n \"type\": \"object\",\n \"required\": [\"invariants\"],\n \"properties\": {\n \"invariants\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"statement\", \"domain\", \"enforcedBy\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"statement\": { \"type\": \"string\", \"description\": \"must ALWAYS hold: 'Invoice numbers are sequential per user with no gaps'\" },\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"enforcedBy\": { \"type\": \"string\", \"description\": \"db-constraint | code-guard | convention | nothing (!)\" },\n \"atRiskBecause\": { \"type\": \"string\", \"description\": \"why current enforcement is fragile, if it is\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"gaps.json\": {\n \"type\": \"object\",\n \"required\": [\"gaps\"],\n \"properties\": {\n \"gaps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"problem\", \"kind\", \"impact\", \"recommendation\", \"evidence\"],\n \"properties\": {\n \"problem\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"incomplete-feature\", \"todo\", \"duplication\", \"bug\", \"hack\", \"dead-code\", \"unclear-ownership\", \"architecture\", \"security\", \"open-product-decision\"] },\n \"impact\": { \"type\": \"string\" },\n \"recommendation\": { \"type\": \"string\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" }\n }\n }\n }\n }\n },\n \"migration.json\": {\n \"type\": \"object\",\n \"required\": [\"mappings\", \"decisionsNeeded\"],\n \"properties\": {\n \"mappings\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"current\", \"future\", \"recommendation\"],\n \"properties\": {\n \"current\": { \"type\": \"array\", \"minItems\": 1, \"items\": { \"type\": \"string\" }, \"description\": \"file paths in the existing repo\" },\n \"future\": {\n \"type\": \"object\",\n \"required\": [\"domain\"],\n \"properties\": {\n \"domain\": { \"$ref\": \"#/$defs/conceptName\" },\n \"concepts\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"commands/queries/events/entities this code becomes\" }\n }\n },\n \"recommendation\": { \"type\": \"string\" }\n }\n }\n },\n \"dropped\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"path\", \"reason\"],\n \"properties\": { \"path\": { \"type\": \"string\", \"description\": \"file path; for sub-file drops use 'path (symbolName)' when part of a file survives\" }, \"reason\": { \"type\": \"string\" } }\n }\n },\n \"decisionsNeeded\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"question\"],\n \"properties\": {\n \"question\": { \"type\": \"string\" },\n \"options\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"blockedConcepts\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n }\n }\n }\n }\n },\n \"interfaces.json\": {\n \"x-optional\": true,\n \"type\": \"object\",\n \"description\": \"Every way the product is CONSUMED, one entry per channel. The web UI is one channel (detailed further in frontend*.json); CLI, MCP server (for AI agents), OpenAPI/REST, generated SDK, realtime, and webhooks are others. Answers 'who can drive this product and how'.\",\n \"required\": [\"interfaces\"],\n \"properties\": {\n \"interfaces\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"kind\", \"audience\", \"purpose\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"web-ui\", \"cli\", \"mcp\", \"openapi-rest\", \"graphql\", \"sdk\", \"websocket-realtime\", \"webhook-in\", \"webhook-out\", \"email\", \"other\"] },\n \"audience\": { \"type\": \"string\", \"enum\": [\"human\", \"developer\", \"ai-agent\", \"external-system\", \"internal\"] },\n \"purpose\": { \"type\": \"string\" },\n \"surfaceCount\": { \"type\": \"number\", \"description\": \"how many ops/tools/commands/routes this channel exposes\" },\n \"generated\": { \"type\": \"boolean\", \"description\": \"true if generated from another source (OpenAPI from routes, typed SDK, MCP tools from funcs) rather than hand-written\" },\n \"domainsServed\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/$defs/conceptName\" } },\n \"status\": { \"type\": \"string\", \"enum\": [\"complete\", \"partial\", \"stub\", \"deprecated\"] },\n \"notes\": { \"type\": \"string\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"frontend.json\": {\n \"x-optional\": true,\n \"type\": \"object\",\n \"description\": \"Web-UI app-level shape. Names the framework/router/styling/data/auth so the rebuild can weigh their tradeoffs (e.g. TanStack Start + better-auth + Mantine).\",\n \"required\": [\"framework\", \"styling\", \"dataLayer\", \"auth\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"framework\": { \"type\": \"string\", \"description\": \"e.g. TanStack Start, Next.js, Remix, Vite SPA\" },\n \"rendering\": { \"type\": \"string\", \"enum\": [\"spa\", \"ssr\", \"ssg\", \"streaming-ssr\", \"mixed\"] },\n \"router\": { \"type\": \"string\", \"description\": \"e.g. TanStack Router (file-based), React Router\" },\n \"styling\": { \"type\": \"string\", \"description\": \"the design system, e.g. Mantine, Tailwind, MUI, CSS modules\" },\n \"designSystemConsistency\": { \"type\": \"string\", \"enum\": [\"single-system\", \"mostly-consistent\", \"mixed\", \"ad-hoc\"], \"description\": \"how uniformly ONE component/theme system is used — the rebuild target is 'everything Mantine', so divergence is porting work\" },\n \"designFindings\": {\n \"type\": \"array\",\n \"description\": \"Concrete broken/inconsistent design patterns observed across the UI. Each is a specific, cited observation of a consistency defect — not a taste opinion. These become the design recommendations in the second-opinion report.\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"pattern\", \"observation\", \"recommendation\", \"evidence\"],\n \"properties\": {\n \"pattern\": {\n \"type\": \"string\",\n \"enum\": [\n \"interaction-inconsistency\",\n \"theming-not-tokenized\",\n \"cross-page-inconsistency\",\n \"component-duplication\",\n \"spacing-typography-scale\",\n \"design-system-bypass\",\n \"accessibility\",\n \"responsive\",\n \"other\"\n ],\n \"description\": \"interaction-inconsistency = same job done different ways (modal here, drawer there; inconsistent confirm dialogs). theming-not-tokenized = hardcoded colors/spacing/fonts instead of theme tokens/variables. cross-page-inconsistency = the same element (button/header/card) styled differently across pages. component-duplication = several near-identical components for one purpose. spacing-typography-scale = ad-hoc magic numbers off any scale. design-system-bypass = raw HTML/CSS where a design-system component exists. accessibility / responsive = a11y or breakpoint defects.\"\n },\n \"observation\": { \"type\": \"string\", \"description\": \"what is inconsistent, with concrete examples (e.g. 'Add-site uses a Drawer, Edit-site uses a Modal for the same task')\" },\n \"impact\": { \"type\": \"string\", \"description\": \"what it does to the user (feels unpolished/confusing) or to maintenance (a color change means hunting every file)\" },\n \"recommendation\": { \"type\": \"string\", \"description\": \"the fix — usually standardize on one pattern, move values to theme tokens, or extract one shared component\" },\n \"severity\": { \"type\": \"string\", \"enum\": [\"minor\", \"worth-fixing\", \"serious\"] },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n },\n \"stateManagement\": { \"type\": \"string\" },\n \"dataLayer\": { \"type\": \"string\", \"description\": \"how the UI talks to the backend: e.g. pikku-react-query, REST fetch helpers, tRPC, GraphQL client\" },\n \"auth\": { \"type\": \"string\", \"description\": \"client auth mechanism: e.g. better-auth, next-auth, custom JWT\" },\n \"buildTool\": { \"type\": \"string\" },\n \"i18n\": { \"type\": \"string\", \"description\": \"internationalization approach, or 'none'\" },\n \"notes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n },\n \"frontend-routes.json\": {\n \"x-optional\": true,\n \"type\": \"object\",\n \"description\": \"The page/route tree — what a user can navigate to and do, mapped back to the data (queries/commands) each route uses and the components it renders.\",\n \"required\": [\"routes\"],\n \"properties\": {\n \"routes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"path\", \"purpose\", \"evidence\"],\n \"properties\": {\n \"path\": { \"type\": \"string\", \"description\": \"URL path, e.g. /app/sites/:id\" },\n \"kind\": { \"type\": \"string\", \"enum\": [\"page\", \"layout\", \"index\", \"modal-or-drawer\", \"redirect\"] },\n \"purpose\": { \"type\": \"string\", \"description\": \"what the user does/sees here, in product terms\" },\n \"auth\": { \"type\": \"string\", \"description\": \"none | authenticated | admin | role:xyz\" },\n \"dataFrom\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"query/command names (from queries.json/commands.json) this route reads/calls\" },\n \"usesComponents\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"component names from frontend-components.json\" },\n \"userFlows\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"workflows.json (kind=user) names this route participates in\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n },\n \"frontend-components.json\": {\n \"x-optional\": true,\n \"type\": \"object\",\n \"description\": \"Component inventory. The load-bearing field is `rebuild`: it separates components that are trivially rebuildable in the target design system (Mantine) from those carrying bespoke logic that must be carefully PORTED (custom charts, complex tables, canvas, drag/drop). That split is the frontend's real migration cost.\",\n \"required\": [\"components\"],\n \"properties\": {\n \"components\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"name\", \"role\", \"rebuild\", \"evidence\", \"confidence\"],\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"role\": { \"type\": \"string\", \"enum\": [\"layout\", \"navigation\", \"presentational\", \"feature\", \"form\", \"data-display\", \"chart\", \"table\", \"overlay\", \"provider\", \"other\"] },\n \"purpose\": { \"type\": \"string\" },\n \"reuse\": { \"type\": \"string\", \"enum\": [\"shared\", \"one-off\"], \"description\": \"used across features vs single-use\" },\n \"rebuild\": {\n \"type\": \"string\",\n \"enum\": [\"mantine-standard\", \"mantine-composition\", \"custom-style\", \"custom-logic\"],\n \"description\": \"mantine-standard = maps 1:1 to a Mantine component (trivial); mantine-composition = built from Mantine primitives (straightforward); custom-style = diverges visually from the design system (normalize to Mantine); custom-logic = bespoke behavior (chart/table/canvas/drag/editor) that must be PORTED, not re-skinned\"\n },\n \"customLogic\": { \"type\": \"string\", \"description\": \"REQUIRED when rebuild=custom-logic: what the bespoke behavior is and why a stock component can't replace it\" },\n \"dependencies\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"notable libs it pulls in (charting/table/editor) — replacement considerations for the port\" },\n \"usedBy\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"routes/components that use it\" },\n \"evidence\": { \"$ref\": \"#/$defs/evidence\" },\n \"confidence\": { \"$ref\": \"#/$defs/confidence\" }\n }\n }\n }\n }\n }\n }\n}\n", "pikku-software-archaeology/references/pikku-mapping.md": "# How Pikku Consumes a Product Blueprint\n\nThe `.knowledge/` blueprint is designed so each concept maps onto exactly one Pikku primitive. A generator (or an agent following `pikku-feature`) walks the JSON files in this order:\n\n| Blueprint source | Pikku target |\n| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `entities.json` attributes + relationships + constraints | Kysely migrations + generated `DB` types; Zod schemas per entity |\n| `entities.json` states/transitions | a `status` column + transition guards inside the owning commands (or a state-machine helper) |\n| `commands.json` | `pikkuFunc` / `pikkuSessionlessFunc` with `input:` Zod schema built from `input[]`; `preconditions` become guard clauses; name is the camelCased command name (`SendInvoice` → `sendInvoice`) |\n| `queries.json` | `pikkuFunc` reads; `scoping` becomes the mandatory `WHERE` / session filter |\n| `events.json` | EventHub topics (realtime) or queue messages; `consumedBy` become `wireQueueWorker` handlers — implicit events (`explicit: false`) get promoted to real emissions |\n| `policies.json` (authorization) | Pikku `permissions` / middleware; one policy = one named permission function, wired everywhere `enforcedAt` listed — this collapses duplicated legacy checks into a single definition |\n| `policies.json` (validation) | Zod schema refinements on the command's `input` |\n| `workflows.json` kind=user | frontend flows + the commands they chain |\n| `workflows.json` kind=system, with `schedule` | `wireScheduler` entries |\n| `workflows.json` multi-step / checkpointing | `pikkuWorkflowFunc` with one `workflow.do(...)` step per blueprint step |\n| `workflows.json` `scenarios[]` | **`pikkuUserFlow` stories — this is the canonical target.** Each scenario's given/when/outcome maps 1:1 onto a user-flow step sequence; group scenarios by their workflow into one flow per journey. Only scenarios with no user-facing surface (pure system workflows: cron sweeps, webhook ingest) fall back to API/e2e tests |\n| `api.json` | `wireHTTP` routes: keep `path`+`method` for compatibility, point at the mapped command/query func; `auth: none`/capability-URL surfaces get `auth: false` |\n| `api.json` kind=webhook-in | `wireHTTP` with `auth: false` + signature-verification middleware from the integration |\n| `integrations.json` | services in `services.ts` (constructor-injected classes); `configVia` env vars become `defineSecret` / config; per-user credentials become `defineCredential` |\n| `architecture.json` notes | deployment config (ports, raw-body routes, proxy expectations) |\n| `invariants.json` enforcedBy=db-constraint | migration constraints (UNIQUE, CHECK, FK) |\n| `invariants.json` enforcedBy=code-guard/nothing | guard clauses + a test each; `atRiskBecause` entries get a hardening task |\n| `gaps.json` | excluded from generation; `open-product-decision` + `migration.json.decisionsNeeded` go to a human BEFORE generation starts |\n| `migration.json.mappings` | the work plan: one mapping = one migration slice |\n| `interfaces.json` kind=cli | `wireCLI` entrypoints — the CLI commands are the same funcs the routes expose |\n| `interfaces.json` kind=mcp | `wireMCP` — each MCP tool IS a `pikkuFunc` (reuse the command/query funcs; don't author tool duplicates) |\n| `interfaces.json` kind=openapi-rest / sdk | generated, not hand-written: the OpenAPI spec + typed client SDK fall out of the `wireHTTP` routes + codegen |\n| `interfaces.json` kind=websocket-realtime | `pikku-realtime` EventHub topics / channels |\n| `frontend.json` | `apps/app` (TanStack Start) shell: router, `@pikku/mantine` theme, `pikku-react-query` data layer, `better-auth` client — the target stack the legacy UI is rebuilt onto |\n| `frontend-routes.json` | TanStack Router routes under `apps/app/src/routes/**` (thin data containers calling `usePikkuQuery`); `dataFrom` names become the generated hooks; subpath routes for rich detail views |\n| `frontend-components.json` rebuild=`mantine-standard`/`mantine-composition` | components in `packages/components` composed from `@pikku/mantine` — the trivial/straightforward bulk |\n| `frontend-components.json` rebuild=`custom-logic` | the PORT list — each becomes a `packages/components` component that reimplements the bespoke behavior (chart/table/editor); its `dependencies` inform whether the lib is kept or replaced. These are the frontend's real work items |\n| `frontend-components.json` rebuild=`custom-style` | normalize to Mantine/theme tokens; usually deleted-and-recomposed, not ported |\n\n## Order of generation\n\n1. Human resolves `decisionsNeeded`.\n2. Entities → migrations + types.\n3. Policies → permission functions (before commands, so commands can reference them).\n4. Commands + queries → funcs; api.json → wirings.\n5. Events → topics/queues; system workflows → schedulers/workers/workflows.\n6. Scenarios → tests. Run them against the new implementation; they encode the legacy behavior worth preserving.\n\n## Uncertainty handling\n\n- `confidence: high` concepts generate directly.\n- `confidence: medium` concepts generate, but are listed for review in the pre-generation report — id, evidence summary, and what is uncertain — rather than carrying a marker comment in the generated code. The report is the review surface; the generated code stays clean.\n- `confidence: low` concepts are NOT generated automatically — they surface in the pre-generation review along with `decisionsNeeded`.\n", "pikku-software-archaeology/scripts/validate.mjs": "#!/usr/bin/env node\n// Validates a .knowledge/ blueprint directory against references/blueprint.schema.json,\n// then runs cross-file referential checks (does every command's domain exist, does every\n// api surface map to a real command/query, ...). Exit 0 = valid, 1 = errors.\n//\n// Usage: node validate.mjs <path-to-.knowledge-dir>\n\nimport { readFileSync, existsSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst here = dirname(fileURLToPath(import.meta.url));\nconst schemaDoc = JSON.parse(readFileSync(join(here, '..', 'references', 'blueprint.schema.json'), 'utf8'));\n\nconst dir = process.argv[2];\nif (!dir) { console.error('usage: node validate.mjs <.knowledge dir>'); process.exit(2); }\n\nconst errors = [];\nconst warnings = [];\n\n// --- minimal JSON-Schema-subset validator (type, required, properties, items, enum, minItems, pattern, $ref -> $defs) ---\nfunction resolveRef(ref) {\n const m = /^#\\/\\$defs\\/(\\w+)$/.exec(ref);\n if (!m || !schemaDoc.$defs[m[1]]) throw new Error(`unresolvable $ref ${ref}`);\n return schemaDoc.$defs[m[1]];\n}\n\nfunction check(value, schema, path) {\n if (schema.$ref) schema = { ...resolveRef(schema.$ref), ...schema, $ref: undefined };\n if (schema.enum && !schema.enum.includes(value)) {\n errors.push(`${path}: expected one of [${schema.enum.join(', ')}], got ${JSON.stringify(value)}`);\n return;\n }\n const t = schema.type;\n if (t === 'object') {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n errors.push(`${path}: expected object`); return;\n }\n for (const req of schema.required || []) {\n if (!(req in value)) errors.push(`${path}: missing required field \"${req}\"`);\n }\n for (const [k, v] of Object.entries(value)) {\n if (schema.properties?.[k]) check(v, schema.properties[k], `${path}.${k}`);\n }\n } else if (t === 'array') {\n if (!Array.isArray(value)) { errors.push(`${path}: expected array`); return; }\n if (schema.minItems && value.length < schema.minItems) {\n errors.push(`${path}: needs at least ${schema.minItems} item(s), has ${value.length}`);\n }\n if (schema.items) value.forEach((v, i) => check(v, schema.items, `${path}[${i}]`));\n } else if (t === 'string') {\n if (typeof value !== 'string') { errors.push(`${path}: expected string`); return; }\n if (schema.pattern && !new RegExp(schema.pattern).test(value)) {\n errors.push(`${path}: \"${value}\" does not match ${schema.pattern}`);\n }\n } else if (t === 'boolean' && typeof value !== 'boolean') {\n errors.push(`${path}: expected boolean`);\n } else if (t === 'number' && typeof value !== 'number') {\n errors.push(`${path}: expected number`);\n }\n}\n\n// --- load + per-file validation ---\n// Files marked `x-optional` (the frontend layer) only validate when present, so a\n// backend-only repo does not fail for lacking them.\nconst docs = {};\nfor (const [filename, fileSchema] of Object.entries(schemaDoc.files)) {\n const p = join(dir, filename);\n if (!existsSync(p)) {\n if (!fileSchema['x-optional']) errors.push(`${filename}: missing`);\n continue;\n }\n try {\n docs[filename] = JSON.parse(readFileSync(p, 'utf8'));\n } catch (e) {\n errors.push(`${filename}: invalid JSON (${e.message})`); continue;\n }\n check(docs[filename], fileSchema, filename);\n}\nif (!existsSync(join(dir, 'blueprint.md'))) errors.push('blueprint.md: missing');\n\n// --- cross-file referential checks ---\nif (docs['domains.json'] && docs['commands.json']) {\n const domains = new Set((docs['domains.json'].domains || []).map((d) => d.name));\n const commandNames = new Set((docs['commands.json'].commands || []).map((c) => c.name));\n const queryNames = new Set((docs['queries.json']?.queries || []).map((q) => q.name));\n const eventNames = new Set((docs['events.json']?.events || []).map((e) => e.name));\n\n const wantDomain = (owner, d) => {\n if (d && !domains.has(d)) errors.push(`${owner}: domain \"${d}\" not defined in domains.json`);\n };\n for (const c of docs['commands.json'].commands || []) {\n wantDomain(`commands.json:${c.name}`, c.domain);\n for (const ev of c.eventsProduced || []) {\n if (!eventNames.has(ev)) warnings.push(`commands.json:${c.name} produces \"${ev}\" which is not in events.json`);\n }\n }\n for (const q of docs['queries.json']?.queries || []) wantDomain(`queries.json:${q.name}`, q.domain);\n for (const e of docs['entities.json']?.entities || []) wantDomain(`entities.json:${e.name}`, e.domain);\n for (const ev of docs['events.json']?.events || []) wantDomain(`events.json:${ev.name}`, ev.domain);\n\n for (const s of docs['api.json']?.surfaces || []) {\n const { type, name } = s.mapsTo || {};\n if (type === 'command' && !commandNames.has(name)) errors.push(`api.json:${s.method || ''} ${s.path}: maps to unknown command \"${name}\"`);\n if (type === 'query' && !queryNames.has(name)) errors.push(`api.json:${s.method || ''} ${s.path}: maps to unknown query \"${name}\"`);\n if (type === 'event-ingress' && !eventNames.has(name)) errors.push(`api.json:${s.method || ''} ${s.path}: event-ingress maps to unknown event \"${name}\" (state-changing webhooks should map to a command instead)`);\n }\n // every domain's listed concepts should exist\n for (const d of docs['domains.json'].domains || []) {\n for (const c of d.commands || []) if (!commandNames.has(c)) warnings.push(`domains.json:${d.name}: lists command \"${c}\" not in commands.json`);\n for (const q of d.queries || []) if (!queryNames.has(q)) warnings.push(`domains.json:${d.name}: lists query \"${q}\" not in queries.json`);\n for (const e of d.events || []) if (!eventNames.has(e)) warnings.push(`domains.json:${d.name}: lists event \"${e}\" not in events.json`);\n const policyNames = new Set((docs['policies.json']?.policies || []).map((p) => p.name));\n for (const p of d.policies || []) if (!policyNames.has(p)) warnings.push(`domains.json:${d.name}: lists policy \"${p}\" not in policies.json`);\n }\n // commands with no policies and no preconditions are suspicious for mutating ops\n for (const c of docs['commands.json'].commands || []) {\n if (!(c.policies || []).length && !(c.preconditions || []).length) {\n warnings.push(`commands.json:${c.name}: no policies or preconditions — really unguarded, or missed extraction?`);\n }\n }\n}\n\n// --- frontend layer cross-checks (only when the optional frontend files exist) ---\nif (docs['frontend-components.json']) {\n const componentNames = new Set(\n (docs['frontend-components.json'].components || []).map((c) => c.name),\n );\n // routes should reference components that were actually inventoried\n for (const r of docs['frontend-routes.json']?.routes || []) {\n for (const c of r.usesComponents || []) {\n if (!componentNames.has(c)) {\n warnings.push(`frontend-routes.json:${r.path}: uses component \"${c}\" not in frontend-components.json`);\n }\n }\n }\n // a component flagged as needing a port must say WHY (the custom logic), or the\n // port-risk is unactionable\n for (const c of docs['frontend-components.json'].components || []) {\n if (c.rebuild === 'custom-logic' && !c.customLogic) {\n warnings.push(`frontend-components.json:${c.name}: rebuild=custom-logic but no customLogic description — port risk is unactionable`);\n }\n }\n // data-fetching queries named on routes should resolve to a real query/command\n if (docs['queries.json'] || docs['commands.json']) {\n const known = new Set([\n ...(docs['queries.json']?.queries || []).map((q) => q.name),\n ...(docs['commands.json']?.commands || []).map((c) => c.name),\n ]);\n for (const r of docs['frontend-routes.json']?.routes || []) {\n for (const d of r.dataFrom || []) {\n if (!known.has(d)) {\n warnings.push(`frontend-routes.json:${r.path}: reads \"${d}\" which is not a known query/command`);\n }\n }\n }\n }\n}\n\n// an inconsistent UI with no specific design findings = under-extraction\n// (guarded on frontend.json alone — independent of the component inventory)\nif (docs['frontend.json']) {\n const consistency = docs['frontend.json'].designSystemConsistency;\n const findingCount = (docs['frontend.json'].designFindings || []).length;\n if ((consistency === 'mixed' || consistency === 'ad-hoc') && findingCount === 0) {\n warnings.push(`frontend.json: designSystemConsistency=\"${consistency}\" but designFindings is empty — name the specific broken patterns (interaction/theming/cross-page/…)`);\n }\n}\n\nfor (const w of warnings) console.log(`WARN ${w}`);\nfor (const e of errors) console.log(`ERROR ${e}`);\nconsole.log(`\\n${errors.length} error(s), ${warnings.length} warning(s) across ${Object.keys(docs).length} files`);\nprocess.exit(errors.length ? 1 : 0);\n", "pikku-software-archaeology/SKILL.md": "---\nname: pikku-software-archaeology\ndescription: 'Use when reverse-engineering an existing repository into a Product Blueprint — recovering what product an undocumented or organically-grown codebase implements so it can be rebuilt cleanly (e.g. as a Pikku app). TRIGGER when: user says \"extract a blueprint\", \"reverse engineer this app\", \"what does this codebase actually do as a product\", \"prepare this repo for a rewrite/migration\", or points at a legacy repo (any language — JS, TS, Ruby, Python, PHP, Go) and asks for its domains, workflows, business rules, or a rebuild plan. DO NOT TRIGGER for: documenting code structure, generating API docs from an already-clean codebase, or code review.'\ninstallGroups: [fabric]\n---\n\n# Software Archaeology\n\n## Overview\n\nExtract **intent over implementation**. A repository is a fossil record of product decisions; your job is to recover the product — domains, entities, commands, queries, events, policies, workflows, invariants — not to describe the code. The output is a `.knowledge/` directory of schema-validated JSON plus a human-readable `blueprint.md`, consumable by a generator (Pikku) to rebuild the application cleanly.\n\n**You are the parser.** Do not build or rely on regex/AST scanners — read the code with your own tools (Grep, Read, subagents). This is what makes the skill language-agnostic: an Express app, a Rails app, and a Django app all yield the same blueprint shape.\n\n**Two layers, never merged silently:**\n- **Facts** — behavior directly observed in code, schema, or tests. Cite them.\n- **Inferred intent** — the product reasoning you reconstruct. Mark it with `confidence` and say what evidence it rests on.\n\nNever present a guess as a fact. `confidence: \"high\"` requires file:line evidence of the behavior itself.\n\n## Output Contract\n\nEverything goes in `<repo>/.knowledge/` (or a caller-specified directory):\n\n```\n.knowledge/\n├── product.json # purpose, actors, capabilities, terminology\n├── domains.json # business domains (NEVER folder names)\n├── entities.json # domain entities: attributes, relationships, states, transitions\n├── commands.json # state-changing actions (SendInvoice, not POST /invoices/:id/send)\n├── queries.json # read operations and views\n├── events.json # business facts, past tense (InvoicePaid) — no technical events\n├── policies.json # authorization, validation, state, business-constraint rules\n├── workflows.json # user + admin + system workflows, with test-derived scenarios\n├── api.json # every surface, each mapped to a command/query/event-ingress\n├── integrations.json # external services: purpose, direction, replaceability\n├── architecture.json # components, datastores, deployment constraints worth keeping\n├── invariants.json # what must ALWAYS be true, and what enforces it today\n├── gaps.json # TODOs, hacks, duplication, dead code, open product decisions\n├── migration.json # current files -> future concepts, drops, decisions needed\n├── blueprint.md # human synthesis of all of the above\n│\n│ # OPTIONAL — the consumer-surface layer. Emit these when the repo has a\n│ # frontend and/or non-HTTP consumer channels. A backend-only repo omits them\n│ # and the validator does not complain.\n├── interfaces.json # every way the product is consumed: web-ui, cli, mcp, openapi-rest, sdk, realtime, webhooks\n├── frontend.json # web-UI app shape: framework, router, styling, data layer, auth (e.g. TanStack Start + better-auth + Mantine)\n├── frontend-routes.json # the page/route tree — what a user navigates to, its data + components\n└── frontend-components.json# component inventory; `rebuild` splits trivial-Mantine from custom-logic-to-port\n```\n\nThe exact field shapes live in `references/blueprint.schema.json` (in this skill's directory — read it before writing any output file). After writing, ALWAYS run:\n\n```bash\nnode <skill-dir>/scripts/validate.mjs <repo>/.knowledge\n```\n\nand fix every ERROR (WARNs are prompts to double-check, not necessarily wrong). Do not declare the extraction done with validation errors outstanding.\n\n## The Pipeline\n\nWork in phases. For small repos (< ~50 source files) do them inline; for larger repos, fan out subagents per phase-3 lens and merge (see \"Scaling up\").\n\n### Phase 1 — Survey (facts only)\n\nBuild an inventory before interpreting anything:\n- Manifests (`package.json`, `Gemfile`, `pyproject.toml`, `go.mod`, `composer.json`): dependencies are integration hints; scripts are entry points.\n- Entry points: servers, route registrations, cron/scheduler setup, queue workers, CLI binaries.\n- Data layer: migrations, schema files, model classes, raw DDL (check comments too — schemas hide in comments in migration-less repos).\n- Every HTTP/GraphQL/RPC surface, webhook, scheduled job, queue consumer.\n- **All consumer channels, not just HTTP**: a frontend app (`apps/`, `frontend/`, `web/`, `client/`), a CLI (`bin/`, `wireCLI`, a `cli/` dir, an `openapi`-generated command tool), an MCP server (`wireMCP`, `@modelcontextprotocol`, a `mcp`/`tools` dir), an OpenAPI/Swagger spec (`openapi.json`, `swagger`), a published/generated SDK, realtime channels (websocket/SSE). Each is an `interfaces.json` entry.\n- Env vars and config files.\n- TODO / FIXME / HACK / XXX / deprecated markers — each is a gaps.json candidate.\n- **The test suite** — locate it now, excavate it in Phase 2.\n\nWhere intent hides, per ecosystem (read these first):\n\n| Ecosystem | Highest-yield locations |\n|---|---|\n| Rails | `config/routes.rb`, model validations + callbacks + `aasm`/state machines, `app/policies` (Pundit) / `ability.rb` (CanCan), Sidekiq/ActiveJob workers, `db/schema.rb`, specs (esp. request + model specs) |\n| Express/Node | route registration files, middleware chains (auth!), inline `if` guards in handlers, SQL/ORM models, `jobs/`+crontab refs, webhook handlers |\n| Django | `urls.py`, model `Meta`/constraints/`clean()`, DRF serializers + permissions classes, celery tasks, admin.py (reveals internal workflows) |\n| Laravel | `routes/`, FormRequests (validation), Policies/Gates, Jobs + scheduler in `Kernel.php`, migrations |\n| Go | mux/router setup, middleware, struct tags, `cmd/` binaries (each is a component) |\n| Frontend (React/Vue/etc.) | router config / file-based routes (pages a user reaches), the component tree, the design-system import (`@mantine/*`, `@mui/*`, Tailwind config) to judge consistency, the data layer (react-query/tRPC/fetch wrappers) to tie UI back to backend queries, charts/tables/editors (the `custom-logic` port risk), auth wiring |\n\n### Phase 2 — Test excavation (do not skip)\n\nTests are the closest thing to an executable product spec. For every test file:\n- `describe`/`context`/`it` names → **scenarios** (attach to the matching workflow in `workflows.json` under `scenarios[]`, with `fromTest` set).\n- User-flow/journey harnesses (`pikkuUserFlow` stories, cucumber `.feature` files, Playwright journeys) are the highest-grade scenario source — they already ARE given/when/outcome sequences; extract them verbatim.\n- Assertions → confirmations of policies and invariants (upgrade their `confidence` to `high`, add the test as evidence).\n- Fixtures/factories → entity attribute shapes and realistic example data.\n- Edge-case tests → business rules that exist **nowhere else in the code** (e.g. \"replayed webhook events are idempotent\" may only be stated in a test).\n- Untested-but-critical paths, or a test suite that can't run (missing helpers, broken setup) → `gaps.json`.\n\nA rule attested by both an implementation guard AND a test is your strongest possible evidence — cite both.\n\n### Phase 3 — Extraction lenses\n\nRun each lens over the surveyed material. Rules that counter the classic failure modes:\n\n**Domains** — infer from data ownership, workflows, and vocabulary; NEVER from folder names. `controllers/` is not a domain; \"Billing\" is. A domain owns entities and the commands that mutate them.\n\n**Entities** — domain concepts, not tables. Include: attributes (from schema + serializers + fixtures), relationships, **lifecycle states and transitions** (grep status/state columns, then find every write to them — each write site is a transition with a trigger), ownership (which actor's rows), constraints.\n\n**Commands** — every way state changes: routes, jobs, webhooks, CLI, admin consoles, DB triggers. Name them imperative `VerbNoun` in domain language: `POST /api/users/:id/status` → `ActivateUser`. For each: actor, preconditions (every `if (...) return 4xx` guard is a precondition or policy), effects, events produced. Convention: authentication/token issuance is a command (`LogInUser`, effect: \"issues a session/JWT\") even though it writes no rows — it changes the caller's security state.\n\n**Queries** — reads and views, `GetX`/`ListX`/`SearchX`. Record the tenancy scoping each applies (a missing `WHERE user_id=` that exists elsewhere is a gaps.json security entry).\n\n**Events** — meaningful business facts, past tense. Most legacy apps have **implicit** events: an email send, a status flip, and a counter bump inside one handler are the event's consumers — reconstruct `InvoicePaid` from them and set `explicit: false`. Exclude technical noise (ButtonClicked, FunctionCalled). Inclusion threshold for implicit events: at least one observed consumer beyond the row write itself (an email, a downstream job, a webhook out, a derived-state flip). Plain CRUD facts with no reaction (`ClientCreated` that nothing listens to) do not become events.\n\n**Policies** — authorization, validation, state rules, business constraints. Record **every** location enforcing each rule in `enforcedAt`; the same rule enforced in 3 places (or worse, 2 slightly different versions) is a gaps.json duplication entry.\n\n**Workflows** — ALL of them: user journeys, admin/support operations, and **system workflows** (cron jobs, queue consumers, webhook reactions, syncs, notification sweeps). A crontab line in a comment is a workflow. Attach Phase-2 scenarios.\n\n**API** — list every surface but map each to its concept (`mapsTo: {type: command, name: SendInvoice}`). The route is evidence; the command is the deliverable. Auth per surface (including \"none\" and capability-URLs like tokened public links). A webhook whose handler changes state maps to a **command** (`RecordInvoicePayment`); reserve `event-ingress` for pure relay surfaces, where `name` must be an events.json event.\n\n**Integrations** — from deps + config + calls: purpose, data exchanged, direction, importance, replacement difficulty, env vars.\n\n**Architecture** — components as they actually run (API process, worker, cron job, SPA), datastores, and deployment constraints that must survive the rewrite (hardcoded ports with upstream expectations, raw-body middleware ordering, webhook retry semantics).\n\n**Invariants** — what must always hold, and `enforcedBy`: db-constraint, code-guard, convention, or `nothing` (an unenforced invariant is a gap). Include `atRiskBecause` when enforcement is fragile (e.g. read-then-insert sequence numbering races).\n\n**Gaps** — incomplete features, TODOs, hacks (hardcoded admin emails), duplicated logic that drifted, dead code, unclear ownership, and **open product decisions** the code never resolved (a FIXME asking \"should deleting a client void invoices?\" is a product decision, record it in both gaps.json and migration.json `decisionsNeeded`).\n\n**Migration** — map current file clusters → future domain + concepts; list files to drop with reasons; list decisions a human must make before rebuild.\n\n**Interfaces** (`interfaces.json`, optional) — every way the product is CONSUMED, one entry per channel, not per route. A product is usually driven through several: a **web UI** (humans), a **CLI** (developers/operators), an **MCP server** (AI agents — in a Pikku app each MCP tool IS a `pikkuFunc`), an **OpenAPI/REST** surface (developers/external systems, often *generated* from the routes), a **generated SDK**, **realtime** (websocket/SSE), and **webhooks** (in/out). For each: `kind`, `audience`, `purpose`, roughly how many ops it exposes, whether it's `generated` vs hand-written, which domains it serves, and `status` (complete/partial/stub — an MCP server with two tools is `stub`). This layer answers \"who can drive this, and how\" — it is the map the second-opinion skill needs to explain that the app is usable by people, developers, and agents.\n\n**Frontend** (`frontend.json` + `frontend-routes.json` + `frontend-components.json`, optional) — the web UI, which needs its own treatment because frontends vary wildly (framework, router, styling, state, data, auth) and the rebuild target is opinionated: **everything in one component system (Mantine), one data layer, one auth**.\n- `frontend.json` records the stack as FACTS: framework (e.g. TanStack Start), rendering (SSR/streaming/SPA), router, styling/design system, `designSystemConsistency`, state management, data layer (e.g. pikku-react-query vs REST helpers), auth (e.g. better-auth), i18n. Name the real technologies — the second-opinion skill weighs their tradeoffs, so record them precisely (do NOT editorialize here; this file is facts).\n- `frontend-routes.json` is the page tree: each route's `purpose` in product terms, `auth`, the `dataFrom` (query/command names it reads — reuse the backend concept names so the UI ties back to the domain), the `usesComponents`, and the `userFlows` it belongs to.\n- `frontend.json.designFindings` captures **broken/inconsistent design patterns** as concrete, cited observations (not taste). Actively hunt for: *interaction inconsistency* (the same job done as a modal in one place and a drawer in another; inconsistent confirm dialogs); *theming not tokenized* (hardcoded hex colors, magic spacing/font sizes, inline styles instead of theme tokens/variables — grep for `#[0-9a-f]{3,6}`, `style={{`, raw `px` values); *cross-page inconsistency* (the same element — button, page header, card — styled differently across routes); *component duplication* (three near-identical cards/tables for one purpose); *design-system bypass* (raw HTML/CSS where a library component exists). Each finding gets an example, its impact (feels unpolished / a color change means hunting every file), and a fix (standardize on one pattern / move to tokens / extract one shared component). These are almost always cheap cleanups, and they are exactly what a non-technical owner perceives as \"the app looks off\" without being able to say why.\n- `frontend-components.json` is where the frontend's real migration cost lives, in the **`rebuild`** field: `mantine-standard` (maps 1:1 to a Mantine component — trivial), `mantine-composition` (built from Mantine primitives — straightforward), `custom-style` (diverges only visually — normalize to Mantine), or **`custom-logic`** (bespoke behavior — a custom chart, a virtualized/complex table, a canvas, drag-and-drop, a rich editor — that must be **ported**, not re-skinned). A `custom-logic` component MUST fill `customLogic` explaining the behavior, and should list the `dependencies` (charting/table/editor libs) that make it a real port. This split — \"trivially re-Mantine-able\" vs \"carries logic that must survive the port\" — is the single most useful thing the frontend extraction produces.\n- **Server-rendered / non-React frontends still get all three files — do NOT skip them.** The `rebuild` enum is named for the target stack, but the distinction it draws is target-agnostic: *trivial stock element* vs *composed from primitives* vs *visual divergence only* vs **bespoke behavior that must be ported**. A Rails app (Slim/ERB + ViewComponents + Hotwire/Stimulus), a Django app (templates + HTMX), or a Laravel app (Blade + Livewire) all have that same split, and it is just as load-bearing there. Use the enum values verbatim (the validator enforces them), classify by what the thing actually *does*, and record the vocabulary mismatch in `frontend.json.notes`. Concretely: treat a template partial + its behavior controller (Stimulus/Alpine/Livewire) as ONE component and classify the pair; a server-rendered app's `custom-logic` is the same list as a SPA's (maps, charts, video players, payment elements, drag-to-reorder, rich text, QR, live-updating regions), plus anything whose behavior rides on a streaming/partial-update contract (Turbo Streams, HTMX swaps) — get that mapping wrong on rebuild and pages show stale data. Omitting these files because \"it isn't a React app\" hides the frontend's entire migration cost, which is the one thing this file exists to expose.\n- The `designFindings` grep hints above are React-flavored; the server-rendered equivalents are inline `style=` attributes in templates, hardcoded hex in the stylesheet tree, per-locale forked templates instead of i18n, and design-system bypass = raw markup where a component/partial already exists. Same findings, different needles.\n\n### Phase 4 — Cross-check, validate, synthesize\n\n1. Cross-checks before writing: every command has an actor and at least one precondition or policy (or you explain why it is genuinely unguarded); every state transition appears in some command/workflow — if a state is only reachable by manual/DB intervention, record the transition with trigger `\"none (manual/DB-only)\"` AND add a gaps.json entry; every event has a producer; every api surface maps to a defined concept; every integration is used by some command/workflow.\n2. Write all 14 JSON files, sorting **unordered identity collections** by `name` (or `path`) — on first extraction too, not just re-runs. Arrays whose order carries meaning (`workflows[].steps`, and any other observed sequence) stay in their observed order: sorting them would rewrite the behavior you extracted. Run the validator: done means `0 error(s)` and exit 0, and every WARN explicitly reviewed and either fixed or justified in your summary.\n3. Write `blueprint.md`: product summary → domain map → per-domain narrative (entities/commands/events with the interesting rules) → workflows → integrations/architecture → invariants → gaps and open decisions → rebuild recommendation. Write it for the engineer who will rebuild the product and has never seen the legacy code.\n\n## Evidence & Confidence Discipline\n\n- Every concept object carries `evidence: [{file, lines, note}]` and `confidence`. (In `api.json` and `architecture.json`, `confidence` is optional — include it whenever a surface/component is inferred rather than directly observed, e.g. an SPA known only from comments.)\n- `high` — the behavior itself is in the cited code/schema/test.\n- `medium` — inferred from multiple converging signals (naming + partial code + a test name).\n- `low` — plausible reconstruction; MUST also be phrased tentatively in blueprint.md and usually deserves a `decisionsNeeded` entry.\n- Comments and docs describe *intended* behavior; code describes *actual* behavior. When they disagree, record the code's behavior as the fact and the disagreement as a gap.\n\n## Scaling up (large repos)\n\n- Fan out one subagent per lens (or per candidate domain) with: the survey notes, the schema file path, and instructions to return JSON fragments with evidence. Merge, dedupe by concept name, then run Phase 4 yourself.\n- Fix the domain cut YOURSELF after the survey and hand every lens agent the same canonical domain list — domains are the shared IDs everything cross-references.\n- Give each lens agent ownership of whole files (never two agents writing one file), and pair coupled files under one agent (commands+queries+api must share names).\n- **Budget a reconciliation pass — parallel agents WILL drift on names** (observed at real scale: 472 validator warnings from one 7-agent run). The pattern: rebuild `domains.json`'s entity/command/query/event/policy roll-up lists LAST, generated by grouping the authoritative files by their `domain` field — never hand-written before those files exist; reconcile `eventsProduced` against curated `events.json` by rename (spelling variant) / drop (CRUD noise, no consumer) / add (only with verified consumer evidence); fill empty command policies by joining `api.json`'s per-surface `auth` against `policies.json` names. The validator's warning list is the reconciliation worklist.\n- **Incremental re-runs:** keep concept names stable (they are the IDs). Re-run the affected lens only, diff against the existing `.knowledge/` files, and preserve unrelated entries verbatim. Sort every array by `name` (or `path`) so diffs are meaningful.\n\n## Red Flags — you are about to produce a worthless blueprint\n\n| Thought | Reality |\n|---|---|\n| \"I'll write it up as markdown docs\" | Only `blueprint.md` is prose. The 14 JSON files ARE the deliverable; a generator consumes them. |\n| \"The routes are the API contract\" | Routes are evidence. Lift each to a command/query or you've documented plumbing, not product. |\n| \"This is obvious, no citation needed\" | Uncited claims are indistinguishable from hallucinations. Evidence on everything. |\n| \"The folder structure tells me the domains\" | Folders are how it grew, not what it is. Derive domains from ownership + vocabulary. |\n| \"Tests are just tests, skip them\" | Tests are the spec. Some rules exist ONLY in tests. Phase 2 is mandatory. |\n| \"No events are emitted, so events: []\" | Reconstruct implicit events from side-effect clusters; mark `explicit: false`. |\n| \"Cron jobs aren't workflows\" | System workflows are workflows. Include schedules, queue consumers, webhook reactions. |\n| \"The frontend is just the web routes\" | The web UI is ONE interface. Inventory the CLI, MCP server, OpenAPI/SDK, realtime, webhooks in `interfaces.json` too — the product is driven by people, developers, AND agents. |\n| \"A component list is enough\" | Without the `rebuild` split, you've hidden the frontend's real cost. Flag every `custom-logic` component (chart/table/canvas/editor) and say what the logic is — that's the port work. |\n| \"I'll skip the validator, the JSON looks right\" | Run it. Missing domains refs, dangling event names, and undescribed custom-logic components are exactly what it catches. |\n\n## Quick Reference\n\n```bash\n# 1. survey + excavate + extract (you, with Read/Grep/subagents)\n# 2. write <repo>/.knowledge/*.json + blueprint.md per references/blueprint.schema.json\n# 3. validate:\nnode <skill-dir>/scripts/validate.mjs <repo>/.knowledge\n```\n\n- Schema/contract: `references/blueprint.schema.json`\n- How Pikku consumes the blueprint: `references/pikku-mapping.md`\n", "pikku-tag-middleware/SKILL.md": "---\nname: pikku-tag-middleware\ndescription: 'Deprecated — use pikku-middleware instead. Tag middleware (addTagMiddleware) is now documented as a section within the pikku-middleware skill, alongside global HTTP middleware, execution order, and the service-to-service bearer auth pattern.'\n---\n\n# Deprecated: use `pikku-middleware`\n\nTag middleware is covered in the **`pikku-middleware`** skill, which also covers:\n- `addHTTPMiddleware` (global / prefix-based)\n- `addTagMiddleware` (tag-scoped)\n- Middleware execution order and priority\n- Service-to-service bearer auth pattern\n- Session-setting middleware pattern\n", "pikku-template-clone/SKILL.md": "---\nname: pikku-template-clone\ndescription: 'Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `npm create pikku`, `git clone <template>`, or the user says \"I cloned the kanban template / starter / template\"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.'\nallowed-tools: Bash(git status *), Bash(git add *), Bash(git commit *), Bash(git rm *), Bash(git mv *), Bash(git log *)\ninstallGroups: [core]\n---\n\n# Pikku Template Post-Clone Cleanup\n\n## Agent Operating Procedure\n\nRun this **once**, right after a template is cloned or scaffolded into a new\nproject. The goal is to turn template scaffolding into a real project. Make the\nsmallest changes and land them as one focused `chore: post-clone cleanup`\ncommit, separate from any feature work.\n\n1. **Replace the template README.** The shipped `README.md` describes the\n _template_, not the user's project — leaving it in place is misleading.\n Either delete it (`git rm README.md`) or rewrite it with the new project's\n name and purpose. Never ship a clone with the generic template README.\n2. **Keep the lockfile committed.** Templates ship a committed `yarn.lock`; do\n NOT re-add `yarn.lock` to `.gitignore`. A real project commits its lockfile\n for reproducible installs. The correct pattern is `yarn.lock` followed by\n `!/yarn.lock`, which commits the root lockfile while keeping generated\n per-unit lockfiles under `.deploy/` (and `e2e/`) ignored.\n3. **Rename template identifiers.** Update `name` in the root `package.json`\n (and any `@project/*` or other placeholder names) to the real project.\n4. **Drop template-only artifacts.** Remove any `TEMPLATE.md`, demo docs, or\n placeholder content that only made sense for the template.\n\nDo not touch generated files (`.pikku/`, `*.gen.*`) or run a full reinstall as\npart of cleanup — this step is project hygiene, not a build.\n\n## Why this exists\n\nTemplates are structure-only starting points. Without this pass, clones carry a\nmisleading README, a placeholder package name, and (historically) a gitignored\nlockfile — all of which leak template assumptions into a real project. Running\nit immediately after clone keeps every Pikku project, OSS or Fabric, starting\nfrom a clean, honest baseline.\n", "pikku-trigger/SKILL.md": "---\nname: pikku-trigger\ndescription: >-\n Use when adding event-driven functions that respond to system events like Redis pub/sub,\n PostgreSQL LISTEN/NOTIFY, or custom event sources. Covers wireTrigger, wireTriggerSource, and\n pikkuTriggerFunc. TRIGGER when: code uses wireTrigger/wireTriggerSource/pikkuTriggerFunc, user\n asks about event-driven functions, Redis pub/sub, PostgreSQL LISTEN/NOTIFY, or reacting to\n external events. DO NOT TRIGGER when: user asks about scheduled tasks (use pikku-cron) or\n background job queues (use pikku-queue).\ninstallGroups: [core]\n---\n\n# Pikku Trigger Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions to fire when external events occur. Triggers connect event sources (Redis pub/sub, PostgreSQL LISTEN/NOTIFY, polling, webhooks) to Pikku functions.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their types\npikku info tags --verbose # Understand project organization\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireTrigger(config)`\n\nDefine the target function that handles trigger events:\n\n```typescript\nimport { wireTrigger } from '@pikku/core/trigger'\n\nwireTrigger({\n name: string, // Trigger name (matches source)\n func: PikkuFunc, // Function to call when event fires\n})\n```\n\n### `wireTriggerSource(config)`\n\nDefine the event source that fires triggers:\n\n```typescript\nimport { wireTriggerSource } from '@pikku/core/trigger'\n\nwireTriggerSource({\n name: string, // Must match wireTrigger name\n func: PikkuTriggerFunc, // Source function (sets up listener)\n input: object, // Configuration for the source\n})\n```\n\n### `pikkuTriggerFunc<TInput, TEvent>`\n\nDefine a trigger source function. Returns a cleanup function.\n\n```typescript\nimport { pikkuTriggerFunc } from '#pikku'\n\nconst source = pikkuTriggerFunc<\n InputType, // Configuration input\n EventType // Shape of events it emits\n>(async (services, input, { trigger }) => {\n // Set up listener...\n trigger.invoke(eventData) // Fire the trigger\n\n // Return cleanup function\n return async () => {\n /* teardown */\n }\n})\n```\n\n## Usage Patterns\n\n### Redis Pub/Sub Source\n\n```typescript\nconst redisSubscribe = pikkuTriggerFunc<\n { channels: string[] },\n { channel: string; message: any }\n>(async ({ redis }, { channels }, { trigger }) => {\n const subscriber = redis.duplicate()\n\n subscriber.on('message', (channel, message) => {\n trigger.invoke({ channel, message: JSON.parse(message) })\n })\n\n await subscriber.subscribe(...channels)\n\n return async () => {\n await subscriber.unsubscribe()\n await subscriber.quit()\n }\n})\n\n// Target function\nconst onOrderEvent = pikkuSessionlessFunc({\n title: 'On Order Event',\n func: async ({ db, logger }, { channel, message }) => {\n logger.info(`Order event on ${channel}`, message)\n await db.processOrderEvent(message)\n },\n})\n\n// Wire them together\nwireTrigger({\n name: 'order-events',\n func: onOrderEvent,\n})\n\nwireTriggerSource({\n name: 'order-events',\n func: redisSubscribe,\n input: { channels: ['orders:created', 'orders:updated'] },\n})\n```\n\n### Triggers vs Queues\n\n| Feature | Trigger | Queue |\n| ----------- | ---------------------------------- | ------------------------------ |\n| Execution | Synchronous, in-process | Async, distributed |\n| Reliability | At-most-once | At-least-once (with retries) |\n| Use case | React to events immediately | Reliable background processing |\n| Source | External systems (Redis, PG, etc.) | Enqueued programmatically |\n\nUse triggers for real-time reactions. Use queues for reliable, retryable background work.\n\n## Complete Example\n\n```typescript\n// functions/triggers.functions.ts\nconst pgListen = pikkuTriggerFunc<{ channel: string }, { payload: any }>(\n async ({ db }, { channel }, { trigger }) => {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(channel)) {\n throw new Error(`Invalid channel name: ${channel}`)\n }\n const client = await db.pool.connect()\n\n client.on('notification', (msg) => {\n trigger.invoke({ payload: JSON.parse(msg.payload) })\n })\n\n await client.query(`LISTEN ${channel}`)\n\n return async () => {\n await client.query(`UNLISTEN ${channel}`)\n client.release()\n }\n }\n)\n\nconst onUserCreated = pikkuSessionlessFunc({\n title: 'On User Created',\n func: async ({ emailService, logger }, { payload }) => {\n logger.info('New user created', { userId: payload.id })\n await emailService.sendWelcome(payload.email)\n },\n})\n\n// wirings/triggers.wiring.ts\nwireTrigger({ name: 'user-created', func: onUserCreated })\nwireTriggerSource({\n name: 'user-created',\n func: pgListen,\n input: { channel: 'user_created' },\n})\n```\n", "pikku-versioning/SKILL.md": "---\nname: pikku-versioning\ndescription: >-\n Use when versioning Pikku function contracts, detecting breaking changes, or managing API\n backward compatibility. Covers the version property, versions.pikku.json manifest, contract\n hashing, and CI integration. TRIGGER when: code uses version: on a pikkuFunc, user asks about\n API versioning, breaking changes, contract hashes, backward compatibility, or \"pikku versions\"\n CLI commands. DO NOT TRIGGER when: user asks about secrets/variables/OAuth2 (use pikku-config)\n or general function definitions (use pikku-concepts).\ninstallGroups: [core]\n---\n\n# Pikku Function Versioning\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nTrack and protect function contracts across releases. Pikku hashes each function's input/output schema into a manifest so you can detect breaking changes before they ship.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their versions\n```\n\nSee `pikku-concepts` for the core mental model.\n\n## Function Versioning\n\nWhen you need to introduce a breaking change, keep the old function as a pinned version and let the new one become the latest.\n\n**The pattern:**\n\n1. Create a new file `my-function-v1.function.ts` — export a variable with the `V1` suffix\n2. Set `override: 'myFunction'` — this is the contract key the manifest groups under\n3. Set `version: 1` — pins this as version 1 of the contract\n4. The existing `my-function.function.ts` (no `version:` field) automatically becomes the latest version\n\n```typescript\n// my-function-v1.function.ts — old contract, kept for running workflows/agents\nexport const getBookV1 = pikkuFunc({\n override: 'getBook', // REQUIRED — links this to the 'getBook' contract family\n version: 1,\n input: z.object({ bookId: z.string() }),\n output: z.object({ title: z.string() }),\n func: async ({ db }, { bookId }) => {\n return db.getBook(bookId)\n },\n})\n\n// my-function.function.ts — latest contract, no version: field\nexport const getBook = pikkuFunc({\n input: z.object({\n bookId: z.string(),\n format: z.enum(['full', 'summary']),\n }),\n output: z.object({\n title: z.string(),\n author: z.string(),\n isbn: z.string(),\n }),\n func: async ({ db }, { bookId, format }) => {\n return db.getBook(bookId, format)\n },\n})\n```\n\n**Why `override` is required:** The manifest groups functions by a shared contract key. Without `override: 'getBook'`, `getBookV1` is stored internally as `getBookV1@v1` (key: `getBookV1`), which is a different contract family from `getBook`. With `override: 'getBook'`, it becomes `getBook@v1` (key: `getBook`), which groups with the unversioned `getBook` — and the unversioned one is automatically promoted to `getBook@v2`.\n\n## Version Manifest (`versions.pikku.json`)\n\nPikku tracks contract hashes to detect breaking changes:\n\n```json\n{\n \"manifestVersion\": 1,\n \"contracts\": {\n \"createTodo\": {\n \"latest\": 1,\n \"versions\": {\n \"1\": { \"inputHash\": \"a1b2c3d4\", \"outputHash\": \"e5f6a7b8\" }\n }\n },\n \"getTodos\": {\n \"latest\": 2,\n \"versions\": {\n \"1\": { \"inputHash\": \"i9j0k1l2\", \"outputHash\": \"m3n4o5p6\" },\n \"2\": { \"inputHash\": \"q7r8s9t0\", \"outputHash\": \"u1v2w3x4\" }\n }\n }\n }\n}\n```\n\nEach hash is derived from the function's input and output schemas plus the contract key. If a schema changes without a version bump, `pikku versions check` will fail.\n\n## CLI Commands\n\n```bash\nnpx pikku versions init # Initialize versioning manifest (run once)\nnpx pikku versions check # Detect contract changes (use in CI)\nnpx pikku versions update # Update contract hashes after version bump\n```\n\n**Workflow:**\n\n1. `pikku versions init` — run once to create `versions.pikku.json`\n2. Develop normally — add/modify functions\n3. `pikku versions check` — CI catches unversioned breaking changes\n4. If intentional: create `my-function-v1.function.ts` with `override` + `version: 1`, then `pikku versions update`\n\n## CI Integration\n\n```yaml\n# .github/workflows/ci.yml\nname: CI\non: [push, pull_request]\n\njobs:\n check:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: npm ci\n - run: npx pikku versions check\n```\n\n## Complete Example\n\n```typescript\n// create-todo-v1.function.ts — v1 locked contract\nexport const createTodoV1 = pikkuSessionlessFunc({\n override: 'createTodo', // groups under 'createTodo' contract family\n version: 1,\n input: z.object({ title: z.string() }),\n output: z.object({ id: z.string(), title: z.string() }),\n func: async ({ todoStore }, { title }) => todoStore.add(title),\n})\n\n// create-todo.function.ts — v2 (latest), called by default\nexport const createTodo = pikkuSessionlessFunc({\n input: z.object({\n title: z.string(),\n priority: z.enum(['low', 'medium', 'high']),\n }),\n output: z.object({\n id: z.string(),\n title: z.string(),\n priority: z.string(),\n }),\n func: async ({ todoStore }, { title, priority }) =>\n todoStore.add(title, priority),\n})\n```\n\nResult in manifest:\n\n```json\n\"createTodo\": {\n \"latest\": 2,\n \"versions\": {\n \"1\": { \"inputHash\": \"...\", \"outputHash\": \"...\" },\n \"2\": { \"inputHash\": \"...\", \"outputHash\": \"...\" }\n }\n}\n```\n", "pikku-websocket/SKILL.md": "---\nname: pikku-websocket\ndescription: >-\n Use when adding real-time features, WebSocket channels, live updates, chat, or pub/sub to a\n Pikku app. Covers wireChannel, action routing, auth, EventHub pub/sub, channel middleware, and\n generated WebSocket client. TRIGGER when: code uses wireChannel, user asks about WebSocket,\n real-time, live updates, chat, pub/sub, or the generated WebSocket client. DO NOT TRIGGER when:\n user asks about HTTP/REST (use pikku-http), SSE (use pikku-http with sse: true), or WebSocket\n deployment specifics (use pikku-deploy-uws).\n---\n\n# Pikku WebSocket Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWire Pikku functions to WebSocket channels with structured message routing, auth per-action, pub/sub via EventHub, and auto-generated type-safe clients.\n\n## Before You Start\n\n```bash\npikku info functions --verbose # See existing functions and their types\npikku info tags --verbose # Understand project organization\n```\n\nFollow existing patterns. See `pikku-concepts` for the core mental model.\n\n## API Reference\n\n### `wireChannel(config)`\n\n```typescript\nimport { wireChannel } from '@pikku/core/channel'\n\nwireChannel({\n name: string, // Channel name (e.g. 'todos')\n onConnect: async () => {}, // Called when client connects\n onDisconnect: async () => {}, // Called when client disconnects\n onMessageWiring: { // Action → function mapping\n [actionName: string]: {\n func: PikkuFunc,\n auth?: boolean, // Override channel-level auth\n permissions?: Record<string, PikkuPermission | PikkuPermission[]>,\n }\n },\n channelMiddleware?: PikkuChannelMiddleware[],\n})\n```\n\n### `pikkuChannelMiddleware(fn)`\n\n```typescript\nimport { pikkuChannelMiddleware } from '@pikku/core'\n\nconst middleware = pikkuChannelMiddleware(async (services, event, next) => {\n // Transform or filter events before/after\n await next(event) // Pass modified event, or next(null) to drop\n})\n```\n\n### `addChannelMiddleware(domain, middlewares)`\n\n```typescript\naddChannelMiddleware('todos', [addTimestamp, filterSensitive])\n```\n\n## Usage Patterns\n\n### Basic Channel\n\n```typescript\nwireChannel({\n name: 'todos',\n onConnect: async () => {},\n onDisconnect: async () => {},\n onMessageWiring: {\n create: { func: createTodo },\n list: { func: listTodos, auth: false },\n },\n})\n```\n\n### Action Routing with Auth\n\nClients send `{ action: 'create', data: {...} }`. Pikku routes to the matching function.\n\n```typescript\nconst authenticate = pikkuFunc({\n title: 'Authenticate',\n func: async ({ setSession }, { token }) => {\n const session = await verifyJWT(token)\n setSession(session)\n return { success: true }\n },\n})\n\nwireChannel({\n name: 'todos',\n onConnect: async () => {},\n onDisconnect: async () => {},\n onMessageWiring: {\n auth: { func: authenticate, auth: false }, // No session required\n subscribe: { func: subscribeTodos }, // Session required\n create: { func: createTodo },\n },\n})\n```\n\n### Pub/Sub with EventHub\n\nUse EventHub for real-time broadcasting across connections:\n\n```typescript\nwireChannel({\n name: 'todos',\n onConnect: async ({ eventHub, channel }) => {\n eventHub.subscribe('todos:updated', (data) => {\n channel.send(data)\n })\n },\n onDisconnect: async () => {},\n onMessageWiring: {\n create: {\n func: pikkuFunc({\n title: 'Create Todo',\n func: async ({ db, eventHub }, { text }) => {\n const todo = await db.createTodo({ text })\n eventHub.publish('todos:updated', {\n event: 'created',\n todo,\n })\n return { todo }\n },\n }),\n },\n },\n})\n```\n\n### Channel Middleware\n\n```typescript\nconst addTimestamp = pikkuChannelMiddleware(\n async ({ logger }, event, next) => {\n logger.info({ phase: 'before-send', event })\n await next({ ...event, sentAt: Date.now() })\n }\n)\n\nconst filterSensitive = pikkuChannelMiddleware(\n async (_services, event, next) => {\n if (event.internal) return await next(null) // Drop event\n await next(event)\n }\n)\n\n// Apply globally to a domain\naddChannelMiddleware('todos', [addTimestamp, filterSensitive])\n\n// Or inline on wiring\nwireChannel({\n name: 'todos',\n channelMiddleware: [addTimestamp],\n onConnect: async () => {},\n onDisconnect: async () => {},\n onMessageWiring: { ... },\n})\n```\n\n### Generated WebSocket Client\n\nAfter `npx pikku all`:\n\n```typescript\nimport { PikkuWebSocket } from '.pikku/pikku-websocket.gen.js'\n\nconst pikku = new PikkuWebSocket(ws)\nconst todosRoute = pikku.getRoute('todos')\n\n// Send action (type-safe)\nconst result = await todosRoute.send('create', { text: 'Buy milk' })\n\n// Subscribe to events\ntodosRoute.subscribe('todos:updated', (data) => {\n console.log(data.event, data.todo)\n})\n```\n\n## Complete Example\n\n```typescript\n// functions/chat.functions.ts\nexport const authenticate = pikkuFunc({\n title: 'Authenticate',\n func: async ({ jwt }, { token }, { setSession }) => {\n const payload = await jwt.verify(token)\n setSession({ userId: payload.userId })\n return { success: true }\n },\n})\n\nexport const sendMessage = pikkuFunc({\n title: 'Send Message',\n func: async ({ db, eventHub }, { text }, { session }) => {\n const message = await db.createMessage({\n text,\n userId: session.userId,\n })\n eventHub.publish('chat:message', { message })\n return { message }\n },\n})\n\nexport const listMessages = pikkuSessionlessFunc({\n title: 'List Messages',\n func: async ({ db }, { limit }) => {\n return { messages: await db.listMessages(limit) }\n },\n})\n\n// wirings/chat.channel.ts\nwireChannel({\n name: 'chat',\n onConnect: async ({ eventHub, channel }) => {\n eventHub.subscribe('chat:message', (data) => {\n channel.send(data)\n })\n },\n onDisconnect: async () => {},\n onMessageWiring: {\n auth: { func: authenticate, auth: false },\n send: { func: sendMessage },\n history: { func: listMessages, auth: false },\n },\n})\n```\n", "pikku-workflow/references/workflow-reference.md": "# Pikku Workflow Reference\n\n## Step execution: inline vs queue dispatch\n\nWhether a step runs **inline** (same process/session, no queue round-trip) or is **dispatched to the queue** is decided **purely by the step's function** — there is no workflow-level or per-call `inline` flag. `workflow.do(...)` options are only `retries`/`retryDelay`/`description`.\n\n- **Steps default to inline.** Most steps don't need their own worker; running them inline avoids a queue round-trip per step, so a normally-started workflow executes its whole chain in one orchestrator pass.\n- **`inline: false` opts a function out.** Set `inline: false` on the **function config** (`pikkuFunc` / `pikkuSessionlessFunc`, same level as `auth`/`expose`) to dispatch that step via the queue — for expensive/long-running steps that deserve their own worker, retry isolation, and concurrency limits.\n- **Run-level `inline` is separate** and only controls whether the *whole run* executes in-process without queue infrastructure (set automatically when there is no `queueService`, or via `startWorkflow(..., { inline: true })`). It governs sleep handling, not per-step dispatch.\n\nThe rule (`dispatchStep`):\n\n| Function `inline` | `queueService` present? | Result |\n|---|---|---|\n| default / `true` | any | **inline** |\n| `false` | yes | **queued** (own worker) |\n| `false` | no | **inline + a `logger.warn`** (misconfiguration: can't dispatch) |\n\n```typescript\n// Push this one expensive step onto the queue; every other step stays inline:\nexport const renderLargeReport = pikkuSessionlessFunc({\n inline: false, // dispatch via queue instead of running inline\n input: ReportInput,\n output: ReportOutput,\n func: async (services, data) => { /* ... */ },\n})\n```\n\n`inline: false` requires a `queueService`; without one the step still runs (so the workflow progresses) but emits a `logger.warn` so the misconfiguration is visible.\n\n## HTTP workflow wiring (manual)\n\nUsually auto-scaffolded via `scaffold.workflow`. To wire by hand:\n\n```typescript\n// Start a workflow\nwireHTTP({ method: 'post', route: '/onboard', func: workflowStart('onboardUser') })\n\n// Execute workflow steps (called by the orchestrator)\nwireHTTP({ method: 'post', route: '/onboard/run', func: workflow('onboardUser') })\n\n// Check workflow status\nwireHTTP({ method: 'get', route: '/onboard/status/:runId', func: workflowStatus('onboardUser') })\n```\n\n## Suspend / resume example\n\n```typescript\nimport { z } from 'zod'\nimport { pikkuWorkflowFunc } from '#pikku/workflow/pikku-workflow-types.gen.js'\n\nexport const approval = pikkuWorkflowFunc({\n description: 'Submit a request and wait for approval',\n input: z.object({ requestId: z.string() }),\n output: z.object({ approved: z.boolean() }),\n func: async (services, data, { workflow }) => {\n await workflow.do('Submit request', 'submitRequest', data)\n await workflow.suspend('Awaiting approval') // pauses here until externally resumed\n const result = await workflow.do('Check result', 'getApprovalResult', data)\n return { approved: result.approved }\n },\n})\n```\n", "pikku-workflow/SKILL.md": "---\nname: pikku-workflow\ndescription: >-\n Use when building multi-step workflows, state machines, or orchestration pipelines with Pikku.\n Covers pikkuWorkflowFunc, workflow steps (do, sleep, suspend), graph workflows, and HTTP wiring.\n TRIGGER when: code uses pikkuWorkflowFunc/pikkuWorkflowGraph, user asks about workflows,\n multi-step processes, durable execution, suspend/resume, or DAG orchestration. DO NOT TRIGGER\n when: user asks about simple background jobs (use pikku-queue) or scheduled tasks (use\n pikku-cron).\ninstallGroups: [core]\n---\n\n# Pikku Workflow Wiring\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Capture baseline. Run `pikku-verify` (or `pikku all`) BEFORE writing code; note existing errors — only NEW errors are yours to fix.\n2. Discover before editing. Prefer `pikku-meta` / `pikku info functions --verbose` and `pikku info tags --verbose` to see functions usable as steps and project organization; inspect only the focused output you need.\n3. Identify the source files that own the behavior. Do not start from generated output, `.pikku`, `node_modules`, vendored packages, or build artifacts.\n4. Make the smallest source change. Keep generated files generated — never hand-edit SDKs, schema output, or typegen to paper over errors; fix the source cause.\n5. Validate with the narrowest relevant command, then re-run `pikku-verify`. If only files you did not touch still error, those are pre-existing — leave them unless asked.\n6. Call `pikku-workflow-view` only when `pikku-verify` fully passes (codegen AND type check both green) — never after a partial pass.\n\nSee `pikku-concepts` for the core mental model.\n\nBuild durable, multi-step workflows with automatic retry, sleep, suspend/resume, and parallel execution. Steps are cached for replay safety.\n\n## Choosing the right factory\n\n| Factory | When to use | Step-graph view? |\n|---|---|---|\n| `pikkuWorkflowFunc` | **Default for all new workflows.** Sequential + conditional logic; DSL mode (serialisable, replay-safe). ALL `const`/`let` declarations must be at the top level of the function body (not inside blocks). | ✅ Yes |\n| `pikkuWorkflowGraph` | DAG / fan-out with nodes and typed refs between them. | ✅ Yes |\n| `pikkuWorkflowComplexFunc` | Escape hatch only — arbitrary TypeScript, no top-level restriction (e.g. dynamic inline functions the DSL extractor cannot handle). | ❌ No (loses step-graph view) |\n\n**Default to `pikkuWorkflowFunc`.** Use `pikkuWorkflowGraph` ONLY with explicit user approval AND only for a genuine cyclic dependency or Node.js-only import DSL cannot express. Use `pikkuWorkflowComplexFunc` ONLY with explicit user approval — a last-resort escape hatch. Never switch to either just to dodge a PKU641 error; restructure the code instead.\n\n### PKU641 — DSL static analysis error\n\n`pikkuWorkflowFunc` statically analyzes the body: **every `const`/`let` must be top-level, not inside any block (`if`, `for`, `while`, …).** Assignments inside blocks are fine — only declarations trigger it.\n\n```typescript\n// ❌ PKU641 — declaration inside block\nif (priority === 'high') {\n const bugCard = await workflow.do(...)\n}\n\n// ✅ hoist the declaration, assign inside the block\nlet bugCard: Awaited<ReturnType<typeof workflow.do>>\nif (priority === 'high') {\n bugCard = await workflow.do(...)\n}\n```\n\n## Import path\n\n```typescript\n// CORRECT — workflow factories come from the generated types file\nimport { pikkuWorkflowFunc, pikkuWorkflowGraph, pikkuWorkflowComplexFunc } from '#pikku/workflow/pikku-workflow-types.gen.js'\n\n// WRONG — '#pikku' does not re-export them (TS2305)\nimport { pikkuWorkflowFunc } from '#pikku'\n```\n\n## Defining a workflow\n\nDeclare input/output as Zod schemas (like any function) — never TypeScript generic params (no `pikkuWorkflowFunc<In, Out>(...)`; that skips runtime validation). `data` is typed from the input schema.\n\n```typescript\nimport { z } from 'zod'\nimport { pikkuWorkflowFunc } from '#pikku/workflow/pikku-workflow-types.gen.js'\n\nconst ProcessOrderInput = z.object({ orderId: z.string(), amount: z.number() })\nconst ProcessOrderOutput = z.object({ status: z.string(), discount: z.number().optional() })\n\nexport const processOrder = pikkuWorkflowFunc({\n description: 'Process an order through payment and fulfillment',\n tags: ['orders'],\n input: ProcessOrderInput,\n output: ProcessOrderOutput,\n func: async (services, data, { workflow }) => {\n // Declare ALL variables at top level — even those only assigned in branches (PKU641)\n let discount: number | undefined\n let status: string\n\n if (data.amount > 1000) {\n const d = await workflow.do('Apply bulk discount', 'calcDiscount', { amount: data.amount })\n discount = d.discountPercent\n }\n\n const payment = await workflow.do('Charge', 'chargePayment', {\n orderId: data.orderId,\n amount: discount ? data.amount * (1 - discount / 100) : data.amount,\n })\n\n if (payment.success) {\n await workflow.do('Fulfill', 'fulfillOrder', { orderId: data.orderId })\n status = 'fulfilled'\n } else {\n status = 'payment-failed'\n }\n\n return { status, discount }\n },\n})\n```\n\n### Workflow step types\n\n```typescript\n// RPC step — run a registered Pikku function as a step (opts: retries, retryDelay, description)\nconst result = await workflow.do('Step name', 'rpcFunctionName', { ...data }, { retries: 3, retryDelay: '1s' })\n\n// Inline closure step — immediate execution, cached for replay\nconst msg = await workflow.do('Generate', async () => `Welcome, ${data.email}!`)\n\n// Sleep — durable pause (duration: '30s', '5min', '1h', '1d')\nawait workflow.sleep('Wait 5 minutes', '5min')\n\n// Suspend — pause until externally resumed (e.g. awaiting approval), then continue\nawait workflow.suspend('Awaiting approval')\n```\n\n### Parallel fan-out\n\n```typescript\nconst users = await Promise.all(\n data.userIds.map((userId) => workflow.do(`Fetch user ${userId}`, 'getUser', { userId }))\n)\n```\n\n### Graph workflow (DAG)\n\n`pikkuWorkflowGraph` derives types from the RPC map — no explicit `input`/`output`. Nodes map `nodeName → Pikku function name`; `config.<node>.next` lists nodes to run after it (in parallel); `config.<node>.input: (ref) => ...` transforms input using refs to prior node outputs.\n\n```typescript\nimport { pikkuWorkflowGraph } from '#pikku/workflow/pikku-workflow-types.gen.js'\n\nexport const userOnboarding = pikkuWorkflowGraph({\n description: 'Onboard a new user',\n nodes: {\n createProfile: 'createUserProfile',\n sendWelcome: 'sendEmail',\n setupDefaults: 'createDefaultTodos',\n },\n config: {\n createProfile: { next: ['sendWelcome', 'setupDefaults'] }, // run in parallel\n sendWelcome: {\n input: (ref) => ({ to: ref('createProfile', 'email'), subject: 'Welcome!' }),\n },\n },\n})\n```\n\n## File conventions\n\n- Place workflows in `packages/functions/src/wirings/*.workflow.ts`; export the variable so the inspector discovers it (no manual registration).\n- HTTP start/run/status routes are auto-scaffolded via `scaffold.workflow` in `pikku.config.json`.\n\n## Step dispatch & HTTP wiring\n\nFor per-step inline-vs-queue dispatch (`inline: false` and the `dispatchStep` rules), the manual `workflowStart`/`workflow`/`workflowStatus` HTTP wirings, and a suspend/resume example, read `references/workflow-reference.md`.\n\n## After writing\n\n1. `pikku-verify` (codegen + tsc).\n2. PKU641 → a `const`/`let` is inside a block; hoist it to the top of the function body.\n3. Import errors → use `#pikku/workflow/pikku-workflow-types.gen.js`, not `#pikku`.\n4. Type errors only in files you did not touch → pre-existing template errors; safe to ignore.\n5. Both green → call `pikku-workflow-view` with the workflow name.\n", "pikku-workflows-client/SKILL.md": "---\nname: pikku-workflows-client\ndescription: 'Run Pikku workflows from a React frontend and track their progress. Covers `useRunWorkflow` (run-and-wait), `useStartWorkflow` (fire-and-poll), and `useWorkflowStatus` (live status). TRIGGER when: a React component needs to invoke or display the status of a Pikku workflow, the user mentions long-running tasks / background jobs / progress UI tied to a workflow, or asks how to start/track a workflow from the client. DO NOT TRIGGER when: the user is wiring the workflow itself (use pikku-workflow) or only making regular RPC calls (use pikku-react-query).'\ninstallGroups: [core]\n---\n\n# Pikku Workflows — Client Hooks\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\nWhen a project has `pikkuWorkflowGraph` workflows, three React Query\nhooks are auto-generated alongside the standard RPC hooks. They handle\nthe two common shapes: **run-and-wait** (short workflows where the\nclient waits for the result) and **fire-and-poll** (long workflows where\nthe client gets a `runId` and polls status).\n\n## Discover what workflows exist\n\n```bash\nyarn pikku meta clients --json | jq '.workflows'\n```\n\nEach entry has `name`, `description`, `mode` (inline | distributed), plus\n`input` / `output` type names. Pass the workflow **name** to the hooks\nbelow.\n\n## Setup\n\nThese hooks are generated into the same `api.gen.ts` as `usePikkuQuery` —\nno extra setup beyond `PikkuProvider` + `QueryClientProvider` (see the\n**pikku-react** and **pikku-react-query** skills).\n\n## `useRunWorkflow(name, options?)` — run and wait\n\nFor short, synchronous-feeling workflows. Returns a mutation that\nresolves to the workflow's output.\n\n```tsx\nimport { useRunWorkflow } from './pikku/api.gen'\n\nfunction ChargeButton({ orderId }: { orderId: string }) {\n const run = useRunWorkflow('chargeOrder', {\n onSuccess: (output) => toast.success(`Charged: $${output.amount}`),\n })\n return (\n <button onClick={() => run.mutate({ orderId })} disabled={run.isPending}>\n {run.isPending ? 'Charging…' : 'Charge'}\n </button>\n )\n}\n```\n\nUse this when the workflow finishes in seconds and the UI can hold open\na loading state until done.\n\n## `useStartWorkflow(name, options?)` — fire-and-poll\n\nReturns a mutation that resolves to `{ runId: string }` immediately. The\nworkflow keeps running on the server. Pair with `useWorkflowStatus` to\nrender progress.\n\n```tsx\nconst start = useStartWorkflow('processVideo', {\n onSuccess: ({ runId }) => setActiveRunId(runId),\n})\n\nstart.mutate({ videoId: '123' })\n```\n\nUse this for long-running workflows (uploads, batch jobs, AI generation,\nanything you'd want a progress bar for).\n\n## `useWorkflowStatus(workflowName, runId, options?)` — observe\n\nPolls the workflow runtime for a run's status. Returns a typed status\nobject with `status`, optional `output`, and optional `error`.\n\n```tsx\nimport { useWorkflowStatus } from './pikku/api.gen'\n\nfunction VideoStatus({ runId }: { runId: string }) {\n const { data: status } = useWorkflowStatus('processVideo', runId, {\n refetchInterval: (query) =>\n query.state.data?.status === 'running' ? 1000 : false,\n })\n\n if (!status) return null\n if (status.status === 'running') return <Spinner />\n if (status.status === 'completed') return <Result {...status.output} />\n if (status.status === 'failed')\n return <Error message={status.error?.message} />\n return null\n}\n```\n\nStatus values: `'running' | 'suspended' | 'completed' | 'failed' | 'cancelled'`.\n\nThe hook stops auto-polling when the run reaches a terminal state (set\n`refetchInterval` to false in those cases — pattern shown above).\n\n## Putting it together — start + observe\n\n```tsx\nfunction ProcessVideoFlow({ videoId }: { videoId: string }) {\n const [runId, setRunId] = useState<string>()\n const start = useStartWorkflow('processVideo', {\n onSuccess: ({ runId }) => setRunId(runId),\n })\n const status = useWorkflowStatus('processVideo', runId)\n\n if (!runId) {\n return (\n <button\n onClick={() => start.mutate({ videoId })}\n disabled={start.isPending}\n >\n Start\n </button>\n )\n }\n return <ProgressBar status={status.data?.status} />\n}\n```\n\n## Backend: streaming richer progress\n\nThe status hook returns a coarse-grained state machine (`running`,\n`completed`, etc.). For step-by-step updates inside a long workflow,\npublish events from the workflow itself via `eventHub` or open a\nWebSocket channel — out of scope for this skill (see workflow + channel\ndocs).\n\n## What NOT to do\n\n- Don't poll status manually — use `useWorkflowStatus` with\n `refetchInterval`. It dedupes and stops on terminal states.\n- Don't call `useRunWorkflow` for workflows that take more than a few\n seconds. The user-facing component will hold a long-running pending\n state with no progress indication; use start + status instead.\n- Don't use these hooks for non-workflow RPCs — they only resolve\n workflow-shaped names. Regular RPCs go through `usePikkuQuery` /\n `usePikkuMutation`.\n", "pikku-ws/SKILL.md": "---\nname: pikku-ws\ndescription: >-\n Use when setting up a WebSocket server with the ws library in a Pikku app. Covers the ws runtime\n adapter for Pikku channels. TRIGGER when: code uses @pikku/ws, user asks about ws library\n WebSocket server, or Node.js WebSocket runtime. DO NOT TRIGGER when: user asks about WebSocket\n wiring/channels (use pikku-websocket) or uWebSockets (use pikku-deploy-uws).\n---\n\n# Pikku WS (WebSocket Server Runtime)\n\n## Agent Operating Procedure\n\nUse this skill as an execution checklist, not reference material.\n\n1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.\n2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.\n3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.\n4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.\n5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.\n\n`@pikku/ws` provides a WebSocket server runtime using the [ws](https://github.com/websockets/ws) library, connecting Pikku's channel system to a Node.js WebSocket server.\n\n## Installation\n\n```bash\nyarn add @pikku/ws ws\n```\n\n## Usage Patterns\n\n### Basic Setup\n\n```typescript\nimport { PikkuWSServer } from '@pikku/ws'\n\nconst wsServer = new PikkuWSServer({\n server: httpServer, // Node.js HTTP server\n singletonServices,\n createWireServices,\n channelStore,\n})\n\nawait wsServer.init()\n```\n\nThis runtime bridges the `ws` WebSocket library with Pikku's channel wiring. See `pikku-websocket` for channel wiring details and `pikku-deploy-fastify`/`pikku-deploy-express` for integrating with HTTP servers.\n" };