# emdash-core-ts Design Document

A TypeScript implementation of the emdash-core agent system, providing AI-powered code exploration and task execution via a REST API.

## Overview

This package provides:
- **Multi-agent system** with specialized sub-agents (Main, Explore, Plan, Bash)
- **Tool framework** with Zod-validated schemas
- **Hono-based API** with SSE streaming
- **Git worktree management** for parallel task execution
- **Configurable LLM providers** via AI SDK

## Project Structure

```
packages/core-ts/
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── DESIGN.md
└── src/
    ├── index.ts                    # Main exports
    ├── config.ts                   # Zod-validated configuration
    ├── server.ts                   # Hono server entry point
    │
    ├── agent/
    │   ├── index.ts                # Agent exports
    │   ├── client.ts               # AI SDK provider setup
    │   ├── runner.ts               # AgentRunner with tool loop
    │   ├── subagent.ts             # SubAgentRunner for spawning
    │   ├── toolkit.ts              # Tool registry and execution
    │   │
    │   ├── tools/
    │   │   ├── index.ts            # Tool exports
    │   │   ├── types.ts            # Tool types and interfaces
    │   │   ├── registry.ts         # Tool registration with Zod schemas
    │   │   ├── coding.ts           # readFile, writeFile, editFile, listFiles
    │   │   ├── search.ts           # grep, glob, semanticSearch
    │   │   ├── shell.ts            # executeCommand, gitCommand
    │   │   └── task.ts             # Task tool for sub-agent spawning
    │   │
    │   ├── agents/
    │   │   ├── index.ts            # Agent type exports
    │   │   ├── main.ts             # Main coding agent config
    │   │   ├── explore.ts          # Read-only exploration agent
    │   │   ├── plan.ts             # Planning agent
    │   │   └── bash.ts             # Shell execution agent
    │   │
    │   └── prompts/
    │       ├── index.ts            # Prompt exports
    │       ├── system.ts           # Base system prompt builder
    │       ├── main.ts             # Main agent prompt
    │       ├── explore.ts          # Explore agent prompt
    │       └── plan.ts             # Plan agent prompt
    │
    ├── api/
    │   ├── index.ts                # API exports
    │   ├── router.ts               # Main Hono router
    │   ├── agent.ts                # POST /agent/chat (SSE)
    │   ├── health.ts               # GET /health
    │   └── middleware/
    │       ├── error.ts            # Error handling middleware
    │       └── cors.ts             # CORS configuration
    │
    ├── swarm/
    │   ├── index.ts                # Swarm exports
    │   ├── worktree-manager.ts     # Git worktree lifecycle
    │   ├── task-runner.ts          # Parallel task execution
    │   └── types.ts                # Swarm task types
    │
    └── utils/
        ├── index.ts                # Utility exports
        ├── logger.ts               # Pino logger setup
        ├── git.ts                  # Git operations helper
        └── file.ts                 # File path utilities
```

---

## Dependencies

```json
{
  "dependencies": {
    "@ai-sdk/anthropic": "^1.0.0",
    "@ai-sdk/openai": "^1.0.0",
    "ai": "^4.0.0",
    "hono": "^4.0.0",
    "zod": "^3.23.0",
    "pino": "^9.0.0",
    "pino-pretty": "^11.0.0",
    "simple-git": "^3.27.0",
    "glob": "^11.0.0",
    "execa": "^9.0.0",
    "dotenv": "^16.0.0"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.6.0",
    "vitest": "^2.0.0",
    "tsx": "^4.0.0"
  }
}
```

---

## Module Designs

### 1. Configuration (`src/config.ts`)

Zod-validated configuration with environment variable support.

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

export const ConfigSchema = z.object({
  // Server
  host: z.string().default('127.0.0.1'),
  port: z.number().default(8765),

  // Repository
  repoRoot: z.string().optional(),

  // LLM
  defaultModel: z.string().default('claude-sonnet-4-20250514'),
  maxIterations: z.number().default(100),
  maxTokens: z.number().default(16384),

  // Providers
  anthropicApiKey: z.string().optional(),
  openaiApiKey: z.string().optional(),
  fireworksApiKey: z.string().optional(),

  // Features
  enableSwarm: z.boolean().default(false),
  enableEmbeddings: z.boolean().default(false),
});

export type Config = z.infer<typeof ConfigSchema>;

export function loadConfig(overrides?: Partial<Config>): Config {
  // Load from env with EMDASH_ prefix, then apply overrides
}
```

### 2. AI Client (`src/agent/client.ts`)

Multi-provider AI client using Vercel AI SDK.

```typescript
import { anthropic } from '@ai-sdk/anthropic';
import { openai } from '@ai-sdk/openai';
import { createOpenAI } from '@ai-sdk/openai';
import type { LanguageModel } from 'ai';

export type ModelProvider = 'anthropic' | 'openai' | 'fireworks';

export interface ModelConfig {
  provider: ModelProvider;
  modelId: string;
  maxTokens?: number;
  temperature?: number;
}

const MODEL_MAP: Record<string, ModelConfig> = {
  // Anthropic
  'claude-sonnet-4-20250514': { provider: 'anthropic', modelId: 'claude-sonnet-4-20250514' },
  'claude-opus-4-20250514': { provider: 'anthropic', modelId: 'claude-opus-4-20250514' },
  'claude-3-5-haiku-20241022': { provider: 'anthropic', modelId: 'claude-3-5-haiku-20241022' },

  // OpenAI
  'gpt-4o': { provider: 'openai', modelId: 'gpt-4o' },
  'gpt-4o-mini': { provider: 'openai', modelId: 'gpt-4o-mini' },

  // Fireworks (Llama, etc.)
  'llama-v3p1-405b-instruct': { provider: 'fireworks', modelId: 'accounts/fireworks/models/llama-v3p1-405b-instruct' },
};

export function getModel(modelName: string): LanguageModel {
  const config = MODEL_MAP[modelName];
  if (!config) throw new Error(`Unknown model: ${modelName}`);

  switch (config.provider) {
    case 'anthropic':
      return anthropic(config.modelId);
    case 'openai':
      return openai(config.modelId);
    case 'fireworks':
      const fireworks = createOpenAI({
        apiKey: process.env.FIREWORKS_API_KEY,
        baseURL: 'https://api.fireworks.ai/inference/v1',
      });
      return fireworks(config.modelId);
  }
}
```

### 3. Tool Types (`src/agent/tools/types.ts`)

Core tool interfaces with Zod validation.

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

export type ToolCategory = 'search' | 'coding' | 'shell' | 'task' | 'planning';

export interface ToolResult<T = unknown> {
  success: boolean;
  data?: T;
  error?: string;
  suggestions?: string[];
  metadata?: Record<string, unknown>;
}

export interface ToolDefinition<TInput extends z.ZodType = z.ZodType, TOutput = unknown> {
  name: string;
  description: string;
  category: ToolCategory;
  schema: TInput;
  execute: (input: z.infer<TInput>, context: ToolContext) => Promise<ToolResult<TOutput>>;
}

export interface ToolContext {
  repoRoot: string;
  workingDir: string;
  logger: Logger;
  abortSignal?: AbortSignal;
}

// Helper to create tools with full type inference
export function defineTool<TInput extends z.ZodType, TOutput>(
  definition: ToolDefinition<TInput, TOutput>
): ToolDefinition<TInput, TOutput> {
  return definition;
}
```

### 4. Tool Registry (`src/agent/tools/registry.ts`)

Central registry for tool management.

```typescript
import { tool } from 'ai';
import type { ToolDefinition, ToolContext } from './types';

export class ToolRegistry {
  private tools = new Map<string, ToolDefinition>();

  register(definition: ToolDefinition): void {
    this.tools.set(definition.name, definition);
  }

  get(name: string): ToolDefinition | undefined {
    return this.tools.get(name);
  }

  getAll(): ToolDefinition[] {
    return Array.from(this.tools.values());
  }

  getByCategory(category: ToolCategory): ToolDefinition[] {
    return this.getAll().filter(t => t.category === category);
  }

  // Convert to AI SDK tools format
  toAITools(context: ToolContext): Record<string, ReturnType<typeof tool>> {
    const result: Record<string, ReturnType<typeof tool>> = {};

    for (const def of this.tools.values()) {
      result[def.name] = tool({
        description: def.description,
        parameters: def.schema,
        execute: async (input) => {
          const result = await def.execute(input, context);
          return result;
        },
      });
    }

    return result;
  }
}

// Default registry with all built-in tools
export function createDefaultRegistry(): ToolRegistry {
  const registry = new ToolRegistry();

  // Register all default tools
  registry.register(readFileTool);
  registry.register(writeFileTool);
  registry.register(editFileTool);
  registry.register(listFilesTool);
  registry.register(grepTool);
  registry.register(globTool);
  registry.register(executeCommandTool);
  registry.register(taskTool);

  return registry;
}
```

### 5. Coding Tools (`src/agent/tools/coding.ts`)

File operation tools with path validation.

```typescript
import { z } from 'zod';
import { defineTool, type ToolResult } from './types';
import { readFile, writeFile } from 'fs/promises';
import { join, resolve, relative } from 'path';

export const readFileTool = defineTool({
  name: 'read_file',
  description: 'Read contents of a file. Returns file content with line numbers.',
  category: 'coding',
  schema: z.object({
    path: z.string().describe('Relative path to the file'),
    startLine: z.number().optional().describe('Start line (1-indexed)'),
    endLine: z.number().optional().describe('End line (1-indexed, inclusive)'),
  }),

  async execute(input, context): Promise<ToolResult<{ content: string; lines: number }>> {
    const fullPath = resolve(context.repoRoot, input.path);

    // Validate path is within repo
    if (!fullPath.startsWith(context.repoRoot)) {
      return { success: false, error: 'Path is outside repository' };
    }

    try {
      let content = await readFile(fullPath, 'utf-8');
      const allLines = content.split('\n');

      // Apply line range if specified
      if (input.startLine !== undefined || input.endLine !== undefined) {
        const start = (input.startLine ?? 1) - 1;
        const end = input.endLine ?? allLines.length;
        content = allLines.slice(start, end).join('\n');
      }

      // Add line numbers
      const numbered = content.split('\n')
        .map((line, i) => `${(input.startLine ?? 1) + i}: ${line}`)
        .join('\n');

      return {
        success: true,
        data: { content: numbered, lines: allLines.length },
      };
    } catch (err) {
      return { success: false, error: `Failed to read file: ${err}` };
    }
  },
});

export const writeFileTool = defineTool({
  name: 'write_file',
  description: 'Write content to a file. Creates parent directories if needed.',
  category: 'coding',
  schema: z.object({
    path: z.string().describe('Relative path to the file'),
    content: z.string().describe('Content to write'),
  }),

  async execute(input, context): Promise<ToolResult<{ path: string }>> {
    const fullPath = resolve(context.repoRoot, input.path);

    if (!fullPath.startsWith(context.repoRoot)) {
      return { success: false, error: 'Path is outside repository' };
    }

    try {
      await mkdir(dirname(fullPath), { recursive: true });
      await writeFile(fullPath, input.content, 'utf-8');
      return { success: true, data: { path: input.path } };
    } catch (err) {
      return { success: false, error: `Failed to write file: ${err}` };
    }
  },
});

export const editFileTool = defineTool({
  name: 'edit_file',
  description: 'Replace a specific string in a file with new content.',
  category: 'coding',
  schema: z.object({
    path: z.string().describe('Relative path to the file'),
    oldString: z.string().describe('Exact string to find and replace'),
    newString: z.string().describe('Replacement string'),
    replaceAll: z.boolean().optional().describe('Replace all occurrences'),
  }),

  async execute(input, context): Promise<ToolResult<{ path: string; replacements: number }>> {
    const fullPath = resolve(context.repoRoot, input.path);

    if (!fullPath.startsWith(context.repoRoot)) {
      return { success: false, error: 'Path is outside repository' };
    }

    try {
      let content = await readFile(fullPath, 'utf-8');

      if (!content.includes(input.oldString)) {
        return { success: false, error: 'Old string not found in file' };
      }

      const count = (content.match(new RegExp(escapeRegex(input.oldString), 'g')) || []).length;

      if (input.replaceAll) {
        content = content.replaceAll(input.oldString, input.newString);
      } else {
        content = content.replace(input.oldString, input.newString);
      }

      await writeFile(fullPath, content, 'utf-8');
      return {
        success: true,
        data: { path: input.path, replacements: input.replaceAll ? count : 1 },
      };
    } catch (err) {
      return { success: false, error: `Failed to edit file: ${err}` };
    }
  },
});

export const listFilesTool = defineTool({
  name: 'list_files',
  description: 'List files in a directory recursively.',
  category: 'coding',
  schema: z.object({
    path: z.string().optional().describe('Directory path (defaults to repo root)'),
    pattern: z.string().optional().describe('Glob pattern filter'),
    maxDepth: z.number().optional().describe('Maximum directory depth'),
  }),

  async execute(input, context): Promise<ToolResult<{ files: string[] }>> {
    // Implementation using glob
  },
});
```

### 6. Search Tools (`src/agent/tools/search.ts`)

Code search tools using ripgrep and glob.

```typescript
import { z } from 'zod';
import { defineTool } from './types';
import { execa } from 'execa';
import { glob } from 'glob';

export const grepTool = defineTool({
  name: 'grep',
  description: 'Search for a pattern in files using ripgrep. Returns matching lines with context.',
  category: 'search',
  schema: z.object({
    pattern: z.string().describe('Regex pattern to search for'),
    path: z.string().optional().describe('Directory or file to search'),
    glob: z.string().optional().describe('File pattern filter (e.g., "*.ts")'),
    contextLines: z.number().optional().describe('Lines of context around matches'),
    caseSensitive: z.boolean().optional().describe('Case-sensitive search'),
    maxResults: z.number().optional().default(50).describe('Maximum results to return'),
  }),

  async execute(input, context) {
    const args = [
      '--json',
      input.caseSensitive === false ? '-i' : '',
      input.contextLines ? `-C${input.contextLines}` : '',
      input.glob ? `--glob=${input.glob}` : '',
      input.pattern,
      input.path ?? '.',
    ].filter(Boolean);

    try {
      const { stdout } = await execa('rg', args, {
        cwd: context.repoRoot,
        reject: false,
      });

      const matches = parseRipgrepJson(stdout, input.maxResults);
      return { success: true, data: { matches } };
    } catch (err) {
      return { success: false, error: `Grep failed: ${err}` };
    }
  },
});

export const globTool = defineTool({
  name: 'glob',
  description: 'Find files matching a glob pattern.',
  category: 'search',
  schema: z.object({
    pattern: z.string().describe('Glob pattern (e.g., "**/*.ts")'),
    path: z.string().optional().describe('Base directory'),
    ignore: z.array(z.string()).optional().describe('Patterns to ignore'),
  }),

  async execute(input, context) {
    const basePath = input.path
      ? resolve(context.repoRoot, input.path)
      : context.repoRoot;

    const files = await glob(input.pattern, {
      cwd: basePath,
      ignore: input.ignore ?? ['node_modules/**', '.git/**'],
      nodir: true,
    });

    return {
      success: true,
      data: { files: files.slice(0, 100) },
    };
  },
});
```

### 7. Shell Tools (`src/agent/tools/shell.ts`)

Command execution with timeout and safety checks.

```typescript
import { z } from 'zod';
import { defineTool } from './types';
import { execa, type ExecaError } from 'execa';

// Commands that should never be allowed
const BLOCKED_COMMANDS = [
  /rm\s+-rf\s+[\/~]/,     // rm -rf / or ~
  /:\s*\(\)\s*\{\s*:\|:/,  // Fork bomb
  />\s*\/dev\/sd/,         // Writing to block devices
];

export const executeCommandTool = defineTool({
  name: 'execute_command',
  description: 'Execute a shell command. Use for build tools, tests, git operations.',
  category: 'shell',
  schema: z.object({
    command: z.string().describe('The command to execute'),
    cwd: z.string().optional().describe('Working directory'),
    timeout: z.number().optional().default(120000).describe('Timeout in milliseconds'),
    env: z.record(z.string()).optional().describe('Additional environment variables'),
  }),

  async execute(input, context) {
    // Safety check
    for (const blocked of BLOCKED_COMMANDS) {
      if (blocked.test(input.command)) {
        return { success: false, error: 'Command blocked for safety reasons' };
      }
    }

    const cwd = input.cwd
      ? resolve(context.repoRoot, input.cwd)
      : context.repoRoot;

    try {
      const { stdout, stderr, exitCode } = await execa('bash', ['-c', input.command], {
        cwd,
        timeout: input.timeout,
        env: { ...process.env, ...input.env },
        reject: false,
        signal: context.abortSignal,
      });

      return {
        success: exitCode === 0,
        data: {
          stdout: stdout.slice(0, 50000),  // Truncate long output
          stderr: stderr.slice(0, 10000),
          exitCode,
        },
      };
    } catch (err) {
      const execaErr = err as ExecaError;
      if (execaErr.timedOut) {
        return { success: false, error: `Command timed out after ${input.timeout}ms` };
      }
      return { success: false, error: `Command failed: ${err}` };
    }
  },
});

export const gitCommandTool = defineTool({
  name: 'git_command',
  description: 'Execute a git command. Safer than general shell for git operations.',
  category: 'shell',
  schema: z.object({
    args: z.array(z.string()).describe('Git command arguments (e.g., ["status", "-s"])'),
  }),

  async execute(input, context) {
    // Block dangerous git operations
    const dangerous = ['push --force', 'reset --hard', 'clean -fd'];
    const argStr = input.args.join(' ');
    if (dangerous.some(d => argStr.includes(d))) {
      return { success: false, error: 'Dangerous git operation blocked' };
    }

    try {
      const { stdout, stderr } = await execa('git', input.args, {
        cwd: context.repoRoot,
        reject: false,
      });

      return { success: true, data: { stdout, stderr } };
    } catch (err) {
      return { success: false, error: `Git command failed: ${err}` };
    }
  },
});
```

### 8. Task Tool (`src/agent/tools/task.ts`)

Sub-agent spawning tool.

```typescript
import { z } from 'zod';
import { defineTool } from './types';
import { SubAgentRunner } from '../subagent';

export const SubAgentType = z.enum(['explore', 'plan', 'bash']);
export type SubAgentType = z.infer<typeof SubAgentType>;

export const taskTool = defineTool({
  name: 'task',
  description: 'Spawn a specialized sub-agent for focused tasks. Explore: read-only codebase exploration. Plan: design implementation plans. Bash: execute shell commands.',
  category: 'task',
  schema: z.object({
    type: SubAgentType.describe('Type of sub-agent to spawn'),
    prompt: z.string().describe('Task description for the sub-agent'),
    model: z.enum(['fast', 'standard', 'powerful']).optional()
      .describe('Model tier: fast (haiku), standard (sonnet), powerful (opus)'),
    background: z.boolean().optional()
      .describe('Run in background and return immediately'),
  }),

  async execute(input, context) {
    const runner = new SubAgentRunner({
      type: input.type,
      repoRoot: context.repoRoot,
      model: input.model ?? 'standard',
    });

    if (input.background) {
      const taskId = await runner.startBackground(input.prompt);
      return {
        success: true,
        data: { taskId, status: 'running' },
        suggestions: ['Use task status to check progress'],
      };
    }

    const result = await runner.run(input.prompt);
    return {
      success: result.success,
      data: {
        response: result.response,
        findings: result.findings,
        filesExplored: result.filesExplored,
      },
    };
  },
});
```

### 9. Agent Runner (`src/agent/runner.ts`)

Main agent orchestration using AI SDK.

```typescript
import { generateText, streamText, type CoreMessage, type Tool } from 'ai';
import { getModel } from './client';
import { ToolRegistry, createDefaultRegistry } from './tools/registry';
import { buildSystemPrompt } from './prompts/system';
import type { Config } from '../config';
import type { Logger } from '../utils/logger';

export interface AgentRunnerOptions {
  config: Config;
  logger: Logger;
  repoRoot: string;
  model?: string;
  mode?: 'code' | 'plan' | 'explore';
  maxIterations?: number;
  tools?: ToolRegistry;
  onToolCall?: (name: string, input: unknown) => void;
  onToolResult?: (name: string, result: unknown) => void;
  onText?: (text: string) => void;
}

export interface AgentResult {
  response: string;
  toolCalls: Array<{ name: string; input: unknown; result: unknown }>;
  usage: { promptTokens: number; completionTokens: number };
  iterations: number;
}

export class AgentRunner {
  private config: Config;
  private logger: Logger;
  private repoRoot: string;
  private model: string;
  private mode: 'code' | 'plan' | 'explore';
  private maxIterations: number;
  private tools: ToolRegistry;
  private messages: CoreMessage[] = [];

  // Callbacks
  private onToolCall?: (name: string, input: unknown) => void;
  private onToolResult?: (name: string, result: unknown) => void;
  private onText?: (text: string) => void;

  constructor(options: AgentRunnerOptions) {
    this.config = options.config;
    this.logger = options.logger;
    this.repoRoot = options.repoRoot;
    this.model = options.model ?? options.config.defaultModel;
    this.mode = options.mode ?? 'code';
    this.maxIterations = options.maxIterations ?? options.config.maxIterations;
    this.tools = options.tools ?? createDefaultRegistry();
    this.onToolCall = options.onToolCall;
    this.onToolResult = options.onToolResult;
    this.onText = options.onText;
  }

  async run(userMessage: string): Promise<AgentResult> {
    this.messages.push({ role: 'user', content: userMessage });

    const systemPrompt = buildSystemPrompt({
      mode: this.mode,
      repoRoot: this.repoRoot,
      availableTools: this.tools.getAll(),
    });

    const model = getModel(this.model);
    const toolContext = {
      repoRoot: this.repoRoot,
      workingDir: this.repoRoot,
      logger: this.logger,
    };

    const toolCalls: AgentResult['toolCalls'] = [];
    let totalUsage = { promptTokens: 0, completionTokens: 0 };
    let iterations = 0;

    const result = await generateText({
      model,
      system: systemPrompt,
      messages: this.messages,
      tools: this.tools.toAITools(toolContext),
      maxSteps: this.maxIterations,
      onStepFinish: ({ stepType, text, toolCalls: calls, toolResults, usage }) => {
        iterations++;
        totalUsage.promptTokens += usage?.promptTokens ?? 0;
        totalUsage.completionTokens += usage?.completionTokens ?? 0;

        if (text && this.onText) {
          this.onText(text);
        }

        if (calls) {
          for (const call of calls) {
            this.onToolCall?.(call.toolName, call.args);
            const result = toolResults?.find(r => r.toolCallId === call.toolCallId);
            if (result) {
              this.onToolResult?.(call.toolName, result.result);
              toolCalls.push({
                name: call.toolName,
                input: call.args,
                result: result.result,
              });
            }
          }
        }
      },
    });

    // Add assistant response to history
    this.messages.push({ role: 'assistant', content: result.text });

    return {
      response: result.text,
      toolCalls,
      usage: totalUsage,
      iterations,
    };
  }

  async *runStream(userMessage: string): AsyncGenerator<AgentStreamEvent> {
    this.messages.push({ role: 'user', content: userMessage });

    const systemPrompt = buildSystemPrompt({
      mode: this.mode,
      repoRoot: this.repoRoot,
      availableTools: this.tools.getAll(),
    });

    const model = getModel(this.model);
    const toolContext = {
      repoRoot: this.repoRoot,
      workingDir: this.repoRoot,
      logger: this.logger,
    };

    const result = streamText({
      model,
      system: systemPrompt,
      messages: this.messages,
      tools: this.tools.toAITools(toolContext),
      maxSteps: this.maxIterations,
    });

    for await (const event of result.fullStream) {
      yield event;
    }
  }

  getMessages(): CoreMessage[] {
    return [...this.messages];
  }

  clearHistory(): void {
    this.messages = [];
  }
}

export type AgentStreamEvent =
  | { type: 'text-delta'; textDelta: string }
  | { type: 'tool-call'; toolName: string; args: unknown }
  | { type: 'tool-result'; toolName: string; result: unknown }
  | { type: 'finish'; usage: { promptTokens: number; completionTokens: number } };
```

### 10. Sub-Agent Runner (`src/agent/subagent.ts`)

Lightweight sub-agent execution.

```typescript
import { AgentRunner, type AgentResult } from './runner';
import { ToolRegistry } from './tools/registry';
import { loadConfig } from '../config';
import { createLogger } from '../utils/logger';

// Tool allowlists per agent type
const TOOL_ALLOWLIST: Record<SubAgentType, string[]> = {
  explore: ['read_file', 'list_files', 'grep', 'glob'],
  plan: ['read_file', 'list_files', 'grep', 'glob', 'write_file'],
  bash: ['execute_command', 'git_command', 'read_file'],
};

const MODEL_TIERS = {
  fast: 'claude-3-5-haiku-20241022',
  standard: 'claude-sonnet-4-20250514',
  powerful: 'claude-opus-4-20250514',
};

export interface SubAgentResult {
  success: boolean;
  response: string;
  findings?: string[];
  filesExplored?: string[];
  error?: string;
}

export class SubAgentRunner {
  private type: SubAgentType;
  private repoRoot: string;
  private model: string;

  constructor(options: { type: SubAgentType; repoRoot: string; model?: string }) {
    this.type = options.type;
    this.repoRoot = options.repoRoot;
    this.model = MODEL_TIERS[options.model ?? 'standard'];
  }

  async run(prompt: string): Promise<SubAgentResult> {
    const config = loadConfig();
    const logger = createLogger({ name: `subagent-${this.type}` });

    // Create restricted tool registry
    const registry = new ToolRegistry();
    const allowedTools = TOOL_ALLOWLIST[this.type];
    const defaultRegistry = createDefaultRegistry();

    for (const name of allowedTools) {
      const tool = defaultRegistry.get(name);
      if (tool) registry.register(tool);
    }

    const runner = new AgentRunner({
      config,
      logger,
      repoRoot: this.repoRoot,
      model: this.model,
      mode: this.type === 'explore' ? 'explore' : 'code',
      maxIterations: 20, // Sub-agents have lower limits
      tools: registry,
    });

    try {
      const result = await runner.run(prompt);

      return {
        success: true,
        response: result.response,
        filesExplored: this.extractFilesExplored(result),
        findings: this.extractFindings(result),
      };
    } catch (err) {
      return {
        success: false,
        response: '',
        error: String(err),
      };
    }
  }

  async startBackground(prompt: string): Promise<string> {
    // Implementation for background execution
    // Returns task ID for status checking
  }

  private extractFilesExplored(result: AgentResult): string[] {
    // Extract file paths from read_file and list_files tool calls
    return result.toolCalls
      .filter(c => c.name === 'read_file' || c.name === 'list_files')
      .map(c => (c.input as any).path)
      .filter(Boolean);
  }

  private extractFindings(result: AgentResult): string[] {
    // Parse findings from response (implementation-specific)
    return [];
  }
}
```

### 11. Agent Configurations (`src/agent/agents/`)

Pre-configured agent types.

```typescript
// src/agent/agents/main.ts
export const mainAgentConfig = {
  name: 'main',
  description: 'Full-featured coding agent with all tools',
  model: 'claude-sonnet-4-20250514',
  tools: ['all'], // All tools enabled
  mode: 'code' as const,
  maxIterations: 100,
  systemPromptTemplate: 'main',
};

// src/agent/agents/explore.ts
export const exploreAgentConfig = {
  name: 'explore',
  description: 'Fast read-only codebase exploration',
  model: 'claude-3-5-haiku-20241022',
  tools: ['read_file', 'list_files', 'grep', 'glob'],
  mode: 'explore' as const,
  maxIterations: 20,
  systemPromptTemplate: 'explore',
};

// src/agent/agents/plan.ts
export const planAgentConfig = {
  name: 'plan',
  description: 'Implementation planning and design',
  model: 'claude-sonnet-4-20250514',
  tools: ['read_file', 'list_files', 'grep', 'glob', 'write_file'],
  mode: 'plan' as const,
  maxIterations: 30,
  systemPromptTemplate: 'plan',
};

// src/agent/agents/bash.ts
export const bashAgentConfig = {
  name: 'bash',
  description: 'Shell command execution specialist',
  model: 'claude-3-5-haiku-20241022',
  tools: ['execute_command', 'git_command', 'read_file'],
  mode: 'code' as const,
  maxIterations: 10,
  systemPromptTemplate: 'bash',
};
```

### 12. Prompts (`src/agent/prompts/`)

System prompt templates.

```typescript
// src/agent/prompts/system.ts
import type { ToolDefinition } from '../tools/types';

export interface PromptContext {
  mode: 'code' | 'plan' | 'explore';
  repoRoot: string;
  availableTools: ToolDefinition[];
  customInstructions?: string;
}

export function buildSystemPrompt(context: PromptContext): string {
  const toolList = context.availableTools
    .map(t => `- ${t.name}: ${t.description}`)
    .join('\n');

  const basePrompt = `You are an AI coding assistant working in the repository at ${context.repoRoot}.

## Available Tools
${toolList}

## Guidelines
- Use tools to explore and understand the codebase before making changes
- Prefer reading existing code over making assumptions
- Keep changes minimal and focused
- Validate paths before file operations

`;

  const modeInstructions = getModeInstructions(context.mode);

  return basePrompt + modeInstructions + (context.customInstructions ?? '');
}

function getModeInstructions(mode: string): string {
  switch (mode) {
    case 'explore':
      return `## Mode: Explore
You are in read-only exploration mode. Focus on understanding and explaining code.
Do NOT make any file modifications.
`;
    case 'plan':
      return `## Mode: Plan
You are in planning mode. Create detailed implementation plans.
You may write plan documents to .emdash/ directory.
`;
    case 'code':
    default:
      return `## Mode: Code
You can read, write, and execute commands to complete coding tasks.
`;
  }
}
```

### 13. API Router (`src/api/router.ts`)

Main Hono router setup.

```typescript
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { healthRoutes } from './health';
import { agentRoutes } from './agent';
import { errorHandler } from './middleware/error';
import type { Config } from '../config';

export interface AppContext {
  config: Config;
  repoRoot: string;
}

export function createRouter(context: AppContext): Hono {
  const app = new Hono();

  // Middleware
  app.use('*', cors());
  app.use('*', logger());
  app.onError(errorHandler);

  // Store context
  app.use('*', async (c, next) => {
    c.set('config', context.config);
    c.set('repoRoot', context.repoRoot);
    await next();
  });

  // Routes
  app.route('/health', healthRoutes);
  app.route('/agent', agentRoutes);

  return app;
}
```

### 14. Agent API (`src/api/agent.ts`)

Chat endpoint with SSE streaming.

```typescript
import { Hono } from 'hono';
import { streamSSE } from 'hono/streaming';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
import { AgentRunner } from '../agent/runner';
import { createLogger } from '../utils/logger';

const ChatRequestSchema = z.object({
  message: z.string().min(1),
  model: z.string().optional(),
  mode: z.enum(['code', 'plan', 'explore']).optional(),
  sessionId: z.string().optional(),
});

export const agentRoutes = new Hono();

agentRoutes.post('/chat', zValidator('json', ChatRequestSchema), async (c) => {
  const body = c.req.valid('json');
  const config = c.get('config');
  const repoRoot = c.get('repoRoot');
  const logger = createLogger({ name: 'agent-api' });

  return streamSSE(c, async (stream) => {
    const runner = new AgentRunner({
      config,
      logger,
      repoRoot,
      model: body.model,
      mode: body.mode,
      onToolCall: (name, input) => {
        stream.writeSSE({
          event: 'tool_call',
          data: JSON.stringify({ name, input }),
        });
      },
      onToolResult: (name, result) => {
        stream.writeSSE({
          event: 'tool_result',
          data: JSON.stringify({ name, result }),
        });
      },
      onText: (text) => {
        stream.writeSSE({
          event: 'message',
          data: JSON.stringify({ text }),
        });
      },
    });

    try {
      await stream.writeSSE({ event: 'start', data: '{}' });

      const result = await runner.run(body.message);

      await stream.writeSSE({
        event: 'done',
        data: JSON.stringify({
          response: result.response,
          usage: result.usage,
          iterations: result.iterations,
        }),
      });
    } catch (err) {
      await stream.writeSSE({
        event: 'error',
        data: JSON.stringify({ error: String(err) }),
      });
    }
  });
});

// Non-streaming endpoint for simple cases
agentRoutes.post('/run', zValidator('json', ChatRequestSchema), async (c) => {
  const body = c.req.valid('json');
  const config = c.get('config');
  const repoRoot = c.get('repoRoot');
  const logger = createLogger({ name: 'agent-api' });

  const runner = new AgentRunner({
    config,
    logger,
    repoRoot,
    model: body.model,
    mode: body.mode,
  });

  const result = await runner.run(body.message);

  return c.json({
    response: result.response,
    toolCalls: result.toolCalls,
    usage: result.usage,
  });
});
```

### 15. Health Endpoint (`src/api/health.ts`)

```typescript
import { Hono } from 'hono';

export const healthRoutes = new Hono();

healthRoutes.get('/', (c) => {
  const config = c.get('config');

  return c.json({
    status: 'healthy',
    version: '0.1.0',
    repoRoot: c.get('repoRoot'),
    defaultModel: config.defaultModel,
    timestamp: new Date().toISOString(),
  });
});
```

### 16. Server Entry (`src/server.ts`)

```typescript
import { serve } from '@hono/node-server';
import { loadConfig, type Config } from './config';
import { createRouter } from './api/router';
import { createLogger } from './utils/logger';
import { resolve } from 'path';

export interface ServerOptions {
  host?: string;
  port?: number;
  repoRoot?: string;
}

export async function startServer(options: ServerOptions = {}) {
  const config = loadConfig({
    host: options.host,
    port: options.port,
    repoRoot: options.repoRoot,
  });

  const logger = createLogger({ name: 'server' });
  const repoRoot = resolve(config.repoRoot ?? process.cwd());

  const app = createRouter({ config, repoRoot });

  logger.info({ host: config.host, port: config.port, repoRoot }, 'Starting server');

  serve({
    fetch: app.fetch,
    hostname: config.host,
    port: config.port,
  });

  logger.info(`Server running at http://${config.host}:${config.port}`);

  return app;
}

// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
  startServer();
}
```

### 17. Worktree Manager (`src/swarm/worktree-manager.ts`)

Git worktree lifecycle management.

```typescript
import { simpleGit, type SimpleGit } from 'simple-git';
import { mkdir, rm, writeFile, readFile } from 'fs/promises';
import { join, resolve } from 'path';
import { existsSync } from 'fs';

export interface WorktreeTask {
  id: string;
  slug: string;
  title: string;
  branch: string;
  worktreePath: string;
  status: 'pending' | 'running' | 'completed' | 'failed' | 'merged';
  filesModified: string[];
  summary?: string;
}

export class WorktreeManager {
  private git: SimpleGit;
  private repoRoot: string;
  private worktreesDir: string;

  constructor(repoRoot: string) {
    this.repoRoot = resolve(repoRoot);
    this.git = simpleGit(this.repoRoot);
    this.worktreesDir = join(this.repoRoot, '.emdash-worktrees');
  }

  async init(): Promise<void> {
    await mkdir(this.worktreesDir, { recursive: true });
  }

  async createWorktree(task: { id: string; title: string }): Promise<WorktreeTask> {
    const slug = this.slugify(task.title);
    const branch = `swarm/${task.id}-${slug}`;
    const worktreePath = join(this.worktreesDir, `${task.id}-${slug}`);

    // Get current branch as base
    const currentBranch = await this.git.revparse(['--abbrev-ref', 'HEAD']);

    // Create worktree with new branch
    await this.git.raw(['worktree', 'add', '-b', branch, worktreePath, currentBranch.trim()]);

    const worktreeTask: WorktreeTask = {
      id: task.id,
      slug,
      title: task.title,
      branch,
      worktreePath,
      status: 'pending',
      filesModified: [],
    };

    // Save task metadata
    await this.saveTaskMeta(worktreeTask);

    return worktreeTask;
  }

  async removeWorktree(taskId: string): Promise<void> {
    const task = await this.getTask(taskId);
    if (!task) return;

    // Remove worktree
    await this.git.raw(['worktree', 'remove', '--force', task.worktreePath]);

    // Delete branch if not merged
    if (task.status !== 'merged') {
      await this.git.branch(['-D', task.branch]).catch(() => {});
    }
  }

  async mergeWorktree(taskId: string): Promise<{ success: boolean; conflicts?: string[] }> {
    const task = await this.getTask(taskId);
    if (!task || task.status !== 'completed') {
      throw new Error(`Task ${taskId} is not ready for merge`);
    }

    try {
      // Merge the branch
      await this.git.merge([task.branch, '--no-ff', '-m', `Merge ${task.title}`]);

      task.status = 'merged';
      await this.saveTaskMeta(task);

      return { success: true };
    } catch (err) {
      // Handle merge conflicts
      const status = await this.git.status();
      const conflicts = status.conflicted;

      if (conflicts.length > 0) {
        // Abort merge
        await this.git.merge(['--abort']);
        return { success: false, conflicts };
      }

      throw err;
    }
  }

  async listWorktrees(): Promise<WorktreeTask[]> {
    const worktrees = await this.git.raw(['worktree', 'list', '--porcelain']);
    const tasks: WorktreeTask[] = [];

    // Parse worktree list and load metadata
    const lines = worktrees.split('\n');
    for (let i = 0; i < lines.length; i++) {
      if (lines[i].startsWith('worktree ') && lines[i].includes('.emdash-worktrees')) {
        const path = lines[i].replace('worktree ', '');
        const metaPath = join(path, '.emdash-task', 'task.json');

        if (existsSync(metaPath)) {
          const meta = JSON.parse(await readFile(metaPath, 'utf-8'));
          tasks.push(meta);
        }
      }
    }

    return tasks;
  }

  async getTask(taskId: string): Promise<WorktreeTask | null> {
    const tasks = await this.listWorktrees();
    return tasks.find(t => t.id === taskId) ?? null;
  }

  async updateTaskStatus(
    taskId: string,
    status: WorktreeTask['status'],
    updates?: Partial<WorktreeTask>
  ): Promise<void> {
    const task = await this.getTask(taskId);
    if (!task) throw new Error(`Task ${taskId} not found`);

    task.status = status;
    Object.assign(task, updates);
    await this.saveTaskMeta(task);
  }

  private async saveTaskMeta(task: WorktreeTask): Promise<void> {
    const metaDir = join(task.worktreePath, '.emdash-task');
    await mkdir(metaDir, { recursive: true });
    await writeFile(
      join(metaDir, 'task.json'),
      JSON.stringify(task, null, 2)
    );
  }

  private slugify(text: string): string {
    return text
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-|-$/g, '')
      .slice(0, 30);
  }
}
```

### 18. Logger (`src/utils/logger.ts`)

```typescript
import pino, { type Logger as PinoLogger } from 'pino';

export type Logger = PinoLogger;

export interface LoggerOptions {
  name?: string;
  level?: string;
  pretty?: boolean;
}

export function createLogger(options: LoggerOptions = {}): Logger {
  const transport = options.pretty ?? process.env.NODE_ENV !== 'production'
    ? {
        target: 'pino-pretty',
        options: {
          colorize: true,
          translateTime: 'HH:MM:ss',
          ignore: 'pid,hostname',
        },
      }
    : undefined;

  return pino({
    name: options.name ?? 'emdash-core',
    level: options.level ?? process.env.LOG_LEVEL ?? 'info',
    transport,
  });
}
```

---

## API Specification

### Endpoints

| Method | Path | Description |
|--------|------|-------------|
| GET | `/health` | Server health check |
| POST | `/agent/chat` | SSE streaming chat |
| POST | `/agent/run` | Non-streaming chat |

### SSE Events (for `/agent/chat`)

| Event | Data | Description |
|-------|------|-------------|
| `start` | `{}` | Session started |
| `message` | `{ text: string }` | Text chunk from LLM |
| `tool_call` | `{ name: string, input: object }` | Tool invocation |
| `tool_result` | `{ name: string, result: object }` | Tool result |
| `done` | `{ response, usage, iterations }` | Completion |
| `error` | `{ error: string }` | Error occurred |

### Request Schema

```typescript
// POST /agent/chat, /agent/run
{
  message: string;       // Required: User message
  model?: string;        // Optional: Model override
  mode?: 'code' | 'plan' | 'explore';  // Optional: Agent mode
  sessionId?: string;    // Optional: Session ID for continuity
}
```

---

## Configuration

### Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `EMDASH_HOST` | `127.0.0.1` | Server bind host |
| `EMDASH_PORT` | `8765` | Server port |
| `EMDASH_DEFAULT_MODEL` | `claude-sonnet-4-20250514` | Default LLM model |
| `EMDASH_MAX_ITERATIONS` | `100` | Max tool loop iterations |
| `ANTHROPIC_API_KEY` | - | Anthropic API key |
| `OPENAI_API_KEY` | - | OpenAI API key |
| `FIREWORKS_API_KEY` | - | Fireworks API key |
| `LOG_LEVEL` | `info` | Logging level |

---

## Implementation Phases

### Phase 1: Core Foundation ✅
- [x] Project setup (package.json, tsconfig)
- [x] Configuration module with Zod validation
- [x] Logger utility
- [x] AI client with multi-provider support
- [x] Basic tool types and registry

### Phase 2: Tools ✅
- [x] Coding tools (read, write, edit, list)
- [x] Search tools (grep, glob)
- [x] Shell tools (execute, git)
- [x] Task tool (sub-agent spawning)
- [x] Semantic search tools (index_repository, semantic_search)

### Phase 3: Agent System ✅
- [x] AgentRunner with tool loop
- [x] SubAgentRunner for specialized agents
- [x] System prompt builder
- [x] Agent configurations (main, explore, plan, bash)
- [x] Custom agent loading from markdown

### Phase 4: API Layer ✅
- [x] Hono router setup
- [x] Health endpoint
- [x] Agent chat endpoint with SSE
- [x] Error handling middleware

### Phase 5: Swarm ✅
- [x] WorktreeManager
- [x] Parallel task execution (SwarmRunner)
- [x] Merge handling

### Phase 6: Embeddings ✅
- [x] OpenAI embedding provider
- [x] Fireworks embedding provider
- [x] In-memory embedding store
- [x] Repository indexing service

### Phase 7: MCP Support ✅
- [x] MCP client with JSON-RPC over stdio
- [x] MCP server manager (lifecycle, tool discovery)
- [x] MCP config loader (.emdash/mcp.json)
- [x] MCP tool factory (convert to internal tool format)
- [x] Tool name collision handling with prefixing

### Phase 8: Checkpointing ✅
- [x] Checkpoint manager
- [x] Git-based session persistence
- [x] Checkpoint create/load/restore/delete
- [x] Session history tracking
- [x] Cleanup utilities

### Phase 9: Research Mode ✅
- [x] Planner agent (breaks down goals into questions)
- [x] Researcher agent (investigates questions)
- [x] Synthesizer agent (creates reports)
- [x] Critic agent (validates findings)
- [x] ResearchController (orchestrates workflow)
- [x] Revision loop with critic feedback

### Phase 10: Polish
- [ ] Comprehensive tests
- [ ] Documentation
- [ ] CLI entry point

---

## Differences from Python Implementation

| Aspect | Python | TypeScript |
|--------|--------|------------|
| Framework | FastAPI | Hono |
| LLM SDK | OpenAI client + claude-agent-sdk | Vercel AI SDK |
| Validation | Pydantic | Zod |
| Logging | Loguru | Pino |
| Git | GitPython | simple-git |
| Process | subprocess | execa |
| Graph DB | Kuzu (optional) | Not included |
| Embeddings | OpenAI/Fireworks | OpenAI/Fireworks |
| MCP | MCP SDK | Custom JSON-RPC client |
| Checkpointing | Git-based | Git-based |
| Research Mode | Multi-agent | Multi-agent |

## Future Considerations

1. **Graph Database** - Consider adding Kuzu for code structure analysis
2. **Code Review** - PR review and code quality analysis
3. **Verifiers** - Post-task verification with retry logic
