/** * Agent execution runners */ import { execSync } from 'node:child_process'; import { existsSync, readFileSync } from 'node:fs'; import { basename, dirname } from 'node:path'; import { callClaude, callClaudeCustom, callClaudeAgent, callClaudeSkill, ClaudeCallOptions, } from '../claude/client.js'; import { type StreamCallback, type PermissionHandler, type AskUserQuestionHandler } from '../claude/process.js'; import { loadCustomAgents, loadAgentPrompt } from '../config/loader.js'; import type { AgentResponse, CustomAgentConfig } from '../models/types.js'; export type { StreamCallback }; /** Common options for running agents */ export interface RunAgentOptions { cwd: string; sessionId?: string; model?: string; /** Resolved path to agent prompt file */ agentPath?: string; onStream?: StreamCallback; onPermissionRequest?: PermissionHandler; onAskUserQuestion?: AskUserQuestionHandler; /** Bypass all permission checks (sacrifice-my-pc mode) */ bypassPermissions?: boolean; } /** Default tools for each built-in agent type */ const DEFAULT_AGENT_TOOLS: Record = { coder: ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash', 'WebSearch', 'WebFetch'], architect: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch'], supervisor: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'], }; /** Get git diff for review context */ export function getGitDiff(cwd: string): string { try { // First check if HEAD exists (new repos may not have any commits) try { execSync('git rev-parse HEAD', { cwd, encoding: 'utf-8', stdio: 'pipe' }); } catch { // No commits yet, return empty diff return ''; } const diff = execSync('git diff HEAD', { cwd, encoding: 'utf-8', maxBuffer: 1024 * 1024 * 10, // 10MB stdio: 'pipe', }); return diff.trim(); } catch { return ''; } } /** Run a custom agent */ export async function runCustomAgent( agentConfig: CustomAgentConfig, task: string, options: RunAgentOptions ): Promise { // If agent references a Claude Code agent if (agentConfig.claudeAgent) { const callOptions: ClaudeCallOptions = { cwd: options.cwd, sessionId: options.sessionId, allowedTools: agentConfig.allowedTools, model: options.model || agentConfig.model, onStream: options.onStream, onPermissionRequest: options.onPermissionRequest, onAskUserQuestion: options.onAskUserQuestion, bypassPermissions: options.bypassPermissions, }; return callClaudeAgent(agentConfig.claudeAgent, task, callOptions); } // If agent references a Claude Code skill if (agentConfig.claudeSkill) { const callOptions: ClaudeCallOptions = { cwd: options.cwd, sessionId: options.sessionId, allowedTools: agentConfig.allowedTools, model: options.model || agentConfig.model, onStream: options.onStream, onPermissionRequest: options.onPermissionRequest, onAskUserQuestion: options.onAskUserQuestion, bypassPermissions: options.bypassPermissions, }; return callClaudeSkill(agentConfig.claudeSkill, task, callOptions); } // Custom agent with prompt const systemPrompt = loadAgentPrompt(agentConfig); const callOptions: ClaudeCallOptions = { cwd: options.cwd, sessionId: options.sessionId, allowedTools: agentConfig.allowedTools || ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch'], model: options.model || agentConfig.model, statusPatterns: agentConfig.statusPatterns, onStream: options.onStream, onPermissionRequest: options.onPermissionRequest, onAskUserQuestion: options.onAskUserQuestion, bypassPermissions: options.bypassPermissions, }; return callClaudeCustom(agentConfig.name, task, systemPrompt, callOptions); } /** * Load agent prompt from file path. */ function loadAgentPromptFromPath(agentPath: string): string { if (!existsSync(agentPath)) { throw new Error(`Agent file not found: ${agentPath}`); } return readFileSync(agentPath, 'utf-8'); } /** * Get agent name from path or spec. * For agents in subdirectories, includes parent dir for pattern matching. * - "~/.takt/agents/default/coder.md" -> "coder" * - "~/.takt/agents/research/supervisor.md" -> "research/supervisor" * - "./coder.md" -> "coder" * - "coder" -> "coder" */ function extractAgentName(agentSpec: string): string { if (!agentSpec.endsWith('.md')) { return agentSpec; } const name = basename(agentSpec, '.md'); const dir = basename(dirname(agentSpec)); // If in 'default' directory, just use the agent name // Otherwise, include the directory for disambiguation (e.g., 'research/supervisor') if (dir === 'default' || dir === 'agents' || dir === '.') { return name; } return `${dir}/${name}`; } /** Run an agent by name or path */ export async function runAgent( agentSpec: string, task: string, options: RunAgentOptions ): Promise { const agentName = extractAgentName(agentSpec); // If agentPath is provided (from workflow), use it to load prompt if (options.agentPath) { if (!existsSync(options.agentPath)) { throw new Error(`Agent file not found: ${options.agentPath}`); } const systemPrompt = loadAgentPromptFromPath(options.agentPath); const tools = DEFAULT_AGENT_TOOLS[agentName] || ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch']; const callOptions: ClaudeCallOptions = { cwd: options.cwd, sessionId: options.sessionId, allowedTools: tools, model: options.model, systemPrompt, onStream: options.onStream, onPermissionRequest: options.onPermissionRequest, onAskUserQuestion: options.onAskUserQuestion, bypassPermissions: options.bypassPermissions, }; return callClaude(agentName, task, callOptions); } // Fallback: Look for custom agent by name const customAgents = loadCustomAgents(); const agentConfig = customAgents.get(agentName); if (agentConfig) { return runCustomAgent(agentConfig, task, options); } throw new Error(`Unknown agent: ${agentSpec}`); }