summaryrefslogtreecommitdiff
path: root/admin-panel/src/server/agents
diff options
context:
space:
mode:
Diffstat (limited to 'admin-panel/src/server/agents')
-rw-r--r--admin-panel/src/server/agents/base-agent.ts83
-rw-r--r--admin-panel/src/server/agents/builder.ts64
-rw-r--r--admin-panel/src/server/agents/critic.ts55
-rw-r--r--admin-panel/src/server/agents/inspector.ts54
-rw-r--r--admin-panel/src/server/agents/planner.ts45
-rw-r--r--admin-panel/src/server/agents/ranger.ts54
-rw-r--r--admin-panel/src/server/agents/shield.ts62
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;
+}