diff options
Diffstat (limited to 'admin-panel/src/server/agents')
| -rw-r--r-- | admin-panel/src/server/agents/base-agent.ts | 83 | ||||
| -rw-r--r-- | admin-panel/src/server/agents/builder.ts | 64 | ||||
| -rw-r--r-- | admin-panel/src/server/agents/critic.ts | 55 | ||||
| -rw-r--r-- | admin-panel/src/server/agents/inspector.ts | 54 | ||||
| -rw-r--r-- | admin-panel/src/server/agents/planner.ts | 45 | ||||
| -rw-r--r-- | admin-panel/src/server/agents/ranger.ts | 54 | ||||
| -rw-r--r-- | admin-panel/src/server/agents/shield.ts | 62 |
7 files changed, 417 insertions, 0 deletions
diff --git a/admin-panel/src/server/agents/base-agent.ts b/admin-panel/src/server/agents/base-agent.ts new file mode 100644 index 0000000..f9e49f7 --- /dev/null +++ b/admin-panel/src/server/agents/base-agent.ts @@ -0,0 +1,83 @@ +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { callClaude, type ClaudeResponse } from '../services/claude.service.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const TEMPLATES_DIR = join(__dirname, '..', 'templates'); + +export interface AgentContext { + taskId: number; + title: string; + description: string; + plan?: string; + decisionLog?: string; + doneWhen?: string; + implementationNotes?: string; + diff?: string; + testResults?: string; + reviewComments?: string; + branchName?: string; + repoPath?: string; + workspacePath?: string; + fileList?: string; + fileContents?: string; +} + +export interface AgentResult { + success: boolean; + content: string; + tokensUsed: number; + model: string; + updates: Record<string, unknown>; // Fields to update on the task + verdict?: 'approve' | 'reject'; // For review agents + message: string; // Log message +} + +export abstract class BaseAgent { + abstract name: string; + abstract templateFile: string; + abstract model: string; + + protected loadTemplate(): string { + return readFileSync(join(TEMPLATES_DIR, this.templateFile), 'utf-8'); + } + + protected fillTemplate(template: string, ctx: AgentContext): string { + return template + .replace(/\{\{taskId\}\}/g, String(ctx.taskId)) + .replace(/\{\{title\}\}/g, ctx.title || '') + .replace(/\{\{description\}\}/g, ctx.description || '') + .replace(/\{\{plan\}\}/g, ctx.plan || '') + .replace(/\{\{decision_log\}\}/g, ctx.decisionLog || '') + .replace(/\{\{done_when\}\}/g, ctx.doneWhen || '') + .replace(/\{\{implementation_notes\}\}/g, ctx.implementationNotes || '') + .replace(/\{\{diff\}\}/g, ctx.diff || '') + .replace(/\{\{test_results\}\}/g, ctx.testResults || '') + .replace(/\{\{review_comments\}\}/g, ctx.reviewComments || '') + .replace(/\{\{branch_name\}\}/g, ctx.branchName || '') + .replace(/\{\{file_list\}\}/g, ctx.fileList || '') + .replace(/\{\{file_contents\}\}/g, ctx.fileContents || ''); + } + + async execute(ctx: AgentContext): Promise<AgentResult> { + const template = this.loadTemplate(); + const prompt = this.fillTemplate(template, ctx); + + const response = await callClaude({ + model: this.model, + systemPrompt: this.getSystemPrompt(), + userMessage: prompt, + maxTokens: this.getMaxTokens(), + }); + + return this.parseResponse(response, ctx); + } + + protected abstract getSystemPrompt(): string; + protected abstract parseResponse(response: ClaudeResponse, ctx: AgentContext): AgentResult; + + protected getMaxTokens(): number { + return 8192; + } +} diff --git a/admin-panel/src/server/agents/builder.ts b/admin-panel/src/server/agents/builder.ts new file mode 100644 index 0000000..4e555be --- /dev/null +++ b/admin-panel/src/server/agents/builder.ts @@ -0,0 +1,64 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class BuilderAgent extends BaseAgent { + name = 'builder'; + templateFile = 'builder.md'; + model = 'claude-opus-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are an expert software engineer. Write clean, correct, production-quality code. Follow the plan precisely. Output complete file contents — never use placeholders or ellipses.'; + } + + protected getMaxTokens(): number { + return 16384; + } + + protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult { + const content = response.content; + + // Extract files from ```FILE: path``` blocks + const files = parseFileBlocks(content); + const summary = extractSection(content, 'Summary') || ''; + const implNotes = extractSection(content, 'Implementation Notes') || ''; + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates: { + implementation_notes: implNotes || summary, + status: 'impl_review', + // files are handled by pipeline.service.ts which reads them from content + }, + message: `Implemented ${files.length} file(s)`, + }; + } +} + +export interface FileChange { + path: string; + content: string; +} + +export function parseFileBlocks(text: string): FileChange[] { + const files: FileChange[] = []; + const regex = /```FILE:\s*(.+?)\n([\s\S]*?)```/g; + let match; + + while ((match = regex.exec(text)) !== null) { + files.push({ + path: match[1].trim(), + content: match[2], + }); + } + + return files; +} + +function extractSection(text: string, heading: string): string | null { + const regex = new RegExp(`###\\s*${heading}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i'); + const match = text.match(regex); + return match ? match[1].trim() : null; +} diff --git a/admin-panel/src/server/agents/critic.ts b/admin-panel/src/server/agents/critic.ts new file mode 100644 index 0000000..b0613fb --- /dev/null +++ b/admin-panel/src/server/agents/critic.ts @@ -0,0 +1,55 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class CriticAgent extends BaseAgent { + name = 'critic'; + templateFile = 'critic.md'; + model = 'claude-sonnet-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are a thorough plan reviewer. Be constructive but rigorous. Approve plans that are solid enough to implement, reject plans with significant gaps or flaws.'; + } + + protected parseResponse(response: ClaudeResponse, ctx: AgentContext): AgentResult { + const content = response.content; + const firstLine = content.split('\n')[0].trim().toUpperCase(); + const approved = firstLine.includes('APPROVE'); + const rejected = firstLine.includes('REJECT'); + const verdict: 'approve' | 'reject' = approved ? 'approve' : 'reject'; + + // Build review entry + const reviewEntry = { + agent: 'critic', + verdict, + content, + timestamp: new Date().toISOString(), + }; + + const updates: Record<string, unknown> = {}; + const currentReviews = ctx.reviewComments ? JSON.parse(ctx.reviewComments || '[]') : []; + currentReviews.push(reviewEntry); + updates.plan_review_comments = JSON.stringify(currentReviews); + + if (approved) { + updates.status = 'impl'; + updates.plan_review_count = (ctx as unknown as Record<string, number>).plan_review_count + ? (ctx as unknown as Record<string, number>).plan_review_count + 1 + : 1; + } else if (rejected) { + updates.status = 'plan'; // Send back to planning + updates.plan_review_count = (ctx as unknown as Record<string, number>).plan_review_count + ? (ctx as unknown as Record<string, number>).plan_review_count + 1 + : 1; + } + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates, + verdict, + message: `Plan review: ${verdict.toUpperCase()}`, + }; + } +} diff --git a/admin-panel/src/server/agents/inspector.ts b/admin-panel/src/server/agents/inspector.ts new file mode 100644 index 0000000..575c34e --- /dev/null +++ b/admin-panel/src/server/agents/inspector.ts @@ -0,0 +1,54 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class InspectorAgent extends BaseAgent { + name = 'inspector'; + templateFile = 'inspector.md'; + model = 'claude-sonnet-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are a senior code reviewer. Be thorough but pragmatic. Focus on correctness and security. Approve code that is production-ready, reject code with significant issues.'; + } + + protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult { + const content = response.content; + const firstLine = content.split('\n')[0].trim().toUpperCase(); + const approved = firstLine.includes('APPROVE'); + const verdict: 'approve' | 'reject' = approved ? 'approve' : 'reject'; + + const reviewEntry = { + agent: 'inspector', + verdict, + content, + timestamp: new Date().toISOString(), + }; + + const updates: Record<string, unknown> = {}; + + // Parse existing review comments + let currentReviews: unknown[]; + try { + currentReviews = JSON.parse(_ctx.reviewComments || '[]'); + } catch { + currentReviews = []; + } + currentReviews.push(reviewEntry); + updates.review_comments = JSON.stringify(currentReviews); + + if (approved) { + updates.status = 'test'; + } else { + updates.status = 'impl'; // Send back to builder + } + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates, + verdict, + message: `Code review: ${verdict.toUpperCase()}`, + }; + } +} diff --git a/admin-panel/src/server/agents/planner.ts b/admin-panel/src/server/agents/planner.ts new file mode 100644 index 0000000..eb004f9 --- /dev/null +++ b/admin-panel/src/server/agents/planner.ts @@ -0,0 +1,45 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class PlannerAgent extends BaseAgent { + name = 'planner'; + templateFile = 'planner.md'; + model = 'claude-opus-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are a meticulous software architect who creates clear, actionable implementation plans. Be thorough but concise. Focus on practical steps, not theory.'; + } + + protected getMaxTokens(): number { + return 16384; + } + + protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult { + const content = response.content; + + // Extract sections + const plan = content; + const doneWhen = extractSection(content, 'Done When') || ''; + const decisionLog = extractSection(content, 'Decisions') || ''; + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates: { + plan, + done_when: doneWhen, + decision_log: decisionLog, + status: 'plan_review', + }, + message: 'Generated implementation plan', + }; + } +} + +function extractSection(text: string, heading: string): string | null { + const regex = new RegExp(`###\\s*${heading}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i'); + const match = text.match(regex); + return match ? match[1].trim() : null; +} diff --git a/admin-panel/src/server/agents/ranger.ts b/admin-panel/src/server/agents/ranger.ts new file mode 100644 index 0000000..f1f3918 --- /dev/null +++ b/admin-panel/src/server/agents/ranger.ts @@ -0,0 +1,54 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class RangerAgent extends BaseAgent { + name = 'ranger'; + templateFile = 'ranger.md'; + model = 'claude-sonnet-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are a QA engineer making the final pass/fail decision. Be fair but thorough. Only pass tasks that genuinely meet their acceptance criteria.'; + } + + protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult { + const content = response.content; + const firstLine = content.split('\n')[0].trim().toUpperCase(); + const passed = firstLine.includes('PASS'); + const verdict: 'approve' | 'reject' = passed ? 'approve' : 'reject'; + + const testEntry = { + agent: 'ranger', + verdict: passed ? 'pass' : 'fail', + content, + timestamp: new Date().toISOString(), + }; + + const updates: Record<string, unknown> = {}; + + let currentResults: unknown[]; + try { + currentResults = JSON.parse(_ctx.testResults || '[]'); + } catch { + currentResults = []; + } + currentResults.push(testEntry); + updates.test_results = JSON.stringify(currentResults); + + if (passed) { + updates.status = 'done'; + updates.completed_at = new Date().toISOString(); + } else { + updates.status = 'impl'; // Send back to builder + } + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates, + verdict, + message: `Final verdict: ${passed ? 'PASS' : 'FAIL'}`, + }; + } +} diff --git a/admin-panel/src/server/agents/shield.ts b/admin-panel/src/server/agents/shield.ts new file mode 100644 index 0000000..2ce0bc1 --- /dev/null +++ b/admin-panel/src/server/agents/shield.ts @@ -0,0 +1,62 @@ +import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js'; +import type { ClaudeResponse } from '../services/claude.service.js'; + +export class ShieldAgent extends BaseAgent { + name = 'shield'; + templateFile = 'shield.md'; + model = 'claude-sonnet-4-20250514'; + + protected getSystemPrompt(): string { + return 'You are a test engineer who writes thorough, practical tests. Focus on verifying requirements and catching regressions. Use appropriate testing frameworks.'; + } + + protected getMaxTokens(): number { + return 12288; + } + + protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult { + const content = response.content; + + // Extract test files + const files = parseTestFiles(content); + const strategy = extractSection(content, 'Test Strategy') || ''; + + return { + success: true, + content, + tokensUsed: response.tokensUsed, + model: response.model, + updates: { + // Test files are written by pipeline service + // Test results will be populated after running + }, + message: `Generated ${files.length} test file(s)`, + }; + } +} + +interface TestFile { + path: string; + content: string; +} + +function parseTestFiles(text: string): TestFile[] { + const files: TestFile[] = []; + const regex = /```FILE:\s*(.+?)\n([\s\S]*?)```/g; + let match; + + while ((match = regex.exec(text)) !== null) { + files.push({ + path: match[1].trim(), + content: match[2], + }); + } + + return files; +} + +function extractSection(text: string, heading: string): string | null { + const regex = new RegExp(`###\\s*${heading}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i'); + const match = text.match(regex); + return match ? match[1].trim() : null; +} |
