RF-25: Code cleanup - remove dead code, add hooks barrel export

- Removed dead agent-service.ts (replaced by clawdbot-agent-service.ts)
- Moved AgentOutput type to clawdbot-agent-service.ts
- Added web/src/hooks/index.ts barrel export for cleaner imports
- Noted: useToast is correct shadcn pattern, not a reimplementation
- Noted: UI components are shadcn standard, no docs needed
- Noted: Monorepo scripts already complete
This commit is contained in:
Brad Groux 2026-01-28 06:29:20 -06:00
parent 75cee87a3d
commit f598ec186b
6 changed files with 58 additions and 436 deletions

View file

@ -595,3 +595,4 @@
{"type":"task.status_changed","taskId":"task_20260128_80Bww2","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_lhxfDcirN7Nm","timestamp":"2026-01-28T11:03:10.942Z"}
{"type":"task.status_changed","taskId":"task_20260128_bIPwKK","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_qF5oHf8YG7Ha","timestamp":"2026-01-28T11:07:35.701Z"}
{"type":"task.status_changed","taskId":"task_20260128_UMDZyh","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_DHjWHkRDV-eW","timestamp":"2026-01-28T12:09:08.261Z"}
{"type":"task.status_changed","taskId":"task_20260128_oeprdJ","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_f00PlGFiT8-I","timestamp":"2026-01-28T12:25:50.647Z"}

View file

@ -1,4 +1,15 @@
[
{
"id": "activity_1769603150649_gbh68ockj",
"type": "status_changed",
"taskId": "task_20260128_oeprdJ",
"taskTitle": "RF-25: Code Cleanup & Organization",
"details": {
"from": "todo",
"status": "in-progress"
},
"timestamp": "2026-01-28T12:25:50.649Z"
},
{
"id": "activity_1769602148289_c072zwzck",
"type": "comment_added",

View file

@ -26,7 +26,7 @@ import { getTelemetryService } from './services/telemetry-service.js';
import { ConfigService } from './services/config-service.js';
import { initBroadcast } from './services/broadcast-service.js';
import { errorHandler } from './middleware/error-handler.js';
import type { AgentOutput } from './services/agent-service.js';
import type { AgentOutput } from './services/clawdbot-agent-service.js';
const app = express();
const PORT = process.env.PORT || 3001;

View file

@ -1,435 +0,0 @@
import { spawn, ChildProcess } from 'child_process';
import fs from 'fs/promises';
import path from 'path';
import { EventEmitter } from 'events';
import { nanoid } from 'nanoid';
import { ConfigService } from './config-service.js';
import { TaskService } from './task-service.js';
import { getTelemetryService, type TelemetryService } from './telemetry-service.js';
import { getTraceService, type TraceService } from './trace-service.js';
import type { Task, AgentType, TaskAttempt, AttemptStatus, RunTelemetryEvent } from '@veritas-kanban/shared';
import { expandPath } from '@veritas-kanban/shared';
const PROJECT_ROOT = path.resolve(process.cwd(), '..');
const LOGS_DIR = path.join(PROJECT_ROOT, '.veritas-kanban', 'logs');
export interface AgentOutput {
type: 'stdout' | 'stderr' | 'stdin' | 'system';
content: string;
timestamp: string;
}
export interface AgentStatus {
taskId: string;
attemptId: string;
agent: AgentType;
status: AttemptStatus;
pid?: number;
startedAt?: string;
endedAt?: string;
}
// Global map of running agents
const runningAgents = new Map<string, {
process: ChildProcess;
taskId: string;
attemptId: string;
agent: AgentType;
logPath: string;
emitter: EventEmitter;
startedAt: string;
project?: string;
}>();
export class AgentService {
private configService: ConfigService;
private taskService: TaskService;
private telemetry: TelemetryService;
private traceService: TraceService;
private logsDir: string;
constructor() {
this.configService = new ConfigService();
this.taskService = new TaskService();
this.telemetry = getTelemetryService();
this.traceService = getTraceService();
this.logsDir = LOGS_DIR;
this.ensureLogsDir();
}
private async ensureLogsDir(): Promise<void> {
await fs.mkdir(this.logsDir, { recursive: true });
}
async startAgent(taskId: string, agentType?: AgentType): Promise<AgentStatus> {
// Get task
const task = await this.taskService.getTask(taskId);
if (!task) {
throw new Error(`Task "${taskId}" not found`);
}
if (task.type !== 'code') {
throw new Error('Agents can only be started on code tasks');
}
if (!task.git?.worktreePath) {
throw new Error('Task must have an active worktree to start an agent');
}
// Check if agent already running for this task
if (runningAgents.has(taskId)) {
throw new Error('An agent is already running for this task');
}
// Get agent config
const config = await this.configService.getConfig();
const agent = agentType || config.defaultAgent;
const agentConfig = config.agents.find(a => a.type === agent);
if (!agentConfig) {
throw new Error(`Agent "${agent}" not found in config`);
}
if (!agentConfig.enabled) {
throw new Error(`Agent "${agent}" is not enabled`);
}
// Create attempt
const attemptId = `attempt_${nanoid(8)}`;
const startedAt = new Date().toISOString();
const logPath = path.join(this.logsDir, `${taskId}_${attemptId}.md`);
// Start trace (if enabled)
this.traceService.startTrace(attemptId, taskId, agent, task.project);
this.traceService.startStep(attemptId, 'init', { worktreePath: task.git.worktreePath });
// Build prompt from task
const prompt = this.buildPrompt(task);
// Create event emitter for this agent
const emitter = new EventEmitter();
// Spawn agent process
const worktreePath = expandPath(task.git.worktreePath);
const args = [...agentConfig.args];
const childProcess = spawn(agentConfig.command, args, {
cwd: worktreePath,
shell: false,
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
FORCE_COLOR: '1',
TERM: 'xterm-256color',
},
});
// Debug: check if streams exist
console.log(`[Agent ${attemptId}] stdout: ${!!childProcess.stdout}, stderr: ${!!childProcess.stderr}, stdin: ${!!childProcess.stdin}`);
// Send prompt via stdin for agents that expect it (claude-code, amp)
if (agent === 'claude-code' || agent === 'amp') {
console.log(`[Agent ${attemptId}] Sending prompt via stdin (${prompt.length} chars)`);
childProcess.stdin?.write(prompt);
childProcess.stdin?.end();
console.log(`[Agent ${attemptId}] stdin closed`);
}
// Store in running agents
runningAgents.set(taskId, {
process: childProcess,
taskId,
attemptId,
agent,
logPath,
emitter,
startedAt,
project: task.project,
});
// Initialize log file
await this.initLogFile(logPath, task, agent, prompt);
// Emit telemetry event for run started
await this.telemetry.emit<RunTelemetryEvent>({
type: 'run.started',
taskId,
attemptId,
agent,
project: task.project,
});
// End init step, start execute step (tracing)
this.traceService.endStep(attemptId, 'init');
this.traceService.startStep(attemptId, 'execute', { pid: childProcess.pid });
// Handle stdout
childProcess.stdout?.on('data', async (data: Buffer) => {
const content = data.toString();
console.log(`[Agent ${attemptId}] stdout:`, content.slice(0, 100));
const output: AgentOutput = {
type: 'stdout',
content,
timestamp: new Date().toISOString(),
};
emitter.emit('output', output);
await this.appendToLog(logPath, 'stdout', content);
});
// Handle stderr
childProcess.stderr?.on('data', async (data: Buffer) => {
const content = data.toString();
console.log(`[Agent ${attemptId}] stderr:`, content.slice(0, 100));
const output: AgentOutput = {
type: 'stderr',
content,
timestamp: new Date().toISOString(),
};
emitter.emit('output', output);
await this.appendToLog(logPath, 'stderr', content);
});
// Handle process exit
childProcess.on('exit', async (code, signal) => {
const endedAt = new Date().toISOString();
const status: AttemptStatus = code === 0 ? 'complete' : 'failed';
const success = code === 0;
// Calculate duration
const running = runningAgents.get(taskId);
const durationMs = running
? new Date(endedAt).getTime() - new Date(running.startedAt).getTime()
: 0;
// Update task attempt
await this.updateTaskAttempt(taskId, attemptId, {
status,
ended: endedAt,
});
// Update task status
await this.taskService.updateTask(taskId, {
status: 'review',
});
// Emit telemetry event for run completed
await this.telemetry.emit<RunTelemetryEvent>({
type: 'run.completed',
taskId,
attemptId,
agent,
project: task.project,
durationMs,
exitCode: code ?? undefined,
success,
});
// End execute step, start complete step, then complete trace
this.traceService.endStep(attemptId, 'execute');
this.traceService.startStep(attemptId, 'complete', { exitCode: code });
this.traceService.endStep(attemptId, 'complete');
await this.traceService.completeTrace(attemptId, success ? 'completed' : 'failed');
// Emit completion
emitter.emit('complete', { code, signal, status });
// Finalize log
await this.appendToLog(logPath, 'system', `\n---\nAgent exited with code ${code}${signal ? ` (signal: ${signal})` : ''}`);
// Remove from running agents
runningAgents.delete(taskId);
});
// Handle errors
childProcess.on('error', async (error) => {
// Emit telemetry event for run error
await this.telemetry.emit<RunTelemetryEvent>({
type: 'run.error',
taskId,
attemptId,
agent,
project: task.project,
error: error.message,
success: false,
});
// Record error step and complete trace with error status
this.traceService.startStep(attemptId, 'error', { error: error.message });
this.traceService.endStep(attemptId, 'error');
await this.traceService.completeTrace(attemptId, 'error');
emitter.emit('error', error);
await this.appendToLog(logPath, 'system', `\n---\nAgent error: ${error.message}`);
runningAgents.delete(taskId);
});
// Update task with attempt info
const attempt: TaskAttempt = {
id: attemptId,
agent,
status: 'running',
started: startedAt,
};
await this.taskService.updateTask(taskId, {
status: 'in-progress',
attempt,
// Add to attempts history
});
return {
taskId,
attemptId,
agent,
status: 'running',
pid: childProcess.pid,
startedAt,
};
}
async sendMessage(taskId: string, message: string): Promise<void> {
const running = runningAgents.get(taskId);
if (!running) {
throw new Error('No agent running for this task');
}
const { process: childProcess, logPath, emitter } = running;
if (!childProcess.stdin?.writable) {
throw new Error('Agent stdin is not writable');
}
// Send to agent stdin
childProcess.stdin.write(message + '\n');
// Log the input
await this.appendToLog(logPath, 'stdin', message);
// Emit for UI
emitter.emit('output', {
type: 'stdin',
content: message,
timestamp: new Date().toISOString(),
});
}
async stopAgent(taskId: string): Promise<void> {
const running = runningAgents.get(taskId);
if (!running) {
throw new Error('No agent running for this task');
}
const { process: childProcess, attemptId, logPath } = running;
// Try graceful shutdown first
childProcess.kill('SIGTERM');
// Force kill after 5 seconds
setTimeout(() => {
if (runningAgents.has(taskId)) {
childProcess.kill('SIGKILL');
}
}, 5000);
// Update attempt status
await this.updateTaskAttempt(taskId, attemptId, {
status: 'failed',
ended: new Date().toISOString(),
});
await this.appendToLog(logPath, 'system', '\n---\nAgent stopped by user');
}
getAgentStatus(taskId: string): AgentStatus | null {
const running = runningAgents.get(taskId);
if (!running) {
return null;
}
return {
taskId,
attemptId: running.attemptId,
agent: running.agent,
status: 'running',
pid: running.process.pid,
};
}
getAgentEmitter(taskId: string): EventEmitter | null {
return runningAgents.get(taskId)?.emitter || null;
}
async getAttemptLog(taskId: string, attemptId: string): Promise<string> {
const logPath = path.join(this.logsDir, `${taskId}_${attemptId}.md`);
try {
return await fs.readFile(logPath, 'utf-8');
} catch {
throw new Error('Log file not found');
}
}
async listAttempts(taskId: string): Promise<string[]> {
const files = await fs.readdir(this.logsDir);
return files
.filter(f => f.startsWith(`${taskId}_`) && f.endsWith('.md'))
.map(f => f.replace(`${taskId}_`, '').replace('.md', ''));
}
private buildPrompt(task: Task): string {
let prompt = `# Task: ${task.title}\n\n`;
if (task.description) {
prompt += `## Description\n\n${task.description}\n\n`;
}
prompt += `## Instructions\n\n`;
prompt += `Work in this directory to complete the task above.\n`;
prompt += `When you're done, commit your changes with a descriptive message.\n`;
return prompt;
}
private async initLogFile(logPath: string, task: Task, agent: AgentType, prompt: string): Promise<void> {
const header = `# Agent Log: ${task.title}
**Task ID:** ${task.id}
**Agent:** ${agent}
**Started:** ${new Date().toISOString()}
**Worktree:** ${task.git?.worktreePath}
## Prompt
\`\`\`
${prompt}
\`\`\`
## Output
`;
await fs.writeFile(logPath, header, 'utf-8');
}
private async appendToLog(logPath: string, type: string, content: string): Promise<void> {
const prefix = type === 'stdin' ? '\n**You:**\n' : '';
const formatted = type === 'stdin'
? `${prefix}${content}\n`
: content;
await fs.appendFile(logPath, formatted, 'utf-8');
}
private async updateTaskAttempt(
taskId: string,
attemptId: string,
updates: Partial<TaskAttempt>
): Promise<void> {
const task = await this.taskService.getTask(taskId);
if (!task?.attempt || task.attempt.id !== attemptId) return;
await this.taskService.updateTask(taskId, {
attempt: {
...task.attempt,
...updates,
},
});
}
}

View file

@ -31,6 +31,12 @@ export interface AgentStatus {
endedAt?: string;
}
export interface AgentOutput {
type: 'stdout' | 'stderr' | 'stdin' | 'system';
content: string;
timestamp: string;
}
// Track pending agent requests
const pendingAgents = new Map<string, {
taskId: string;

39
web/src/hooks/index.ts Normal file
View file

@ -0,0 +1,39 @@
/**
* Hooks barrel export
* Import hooks from '@/hooks' for cleaner imports
*/
export * from './useActivity';
export * from './useAgent';
export * from './useAttachments';
export * from './useBulkActions';
export * from './useConfig';
export * from './useConflicts';
export * from './useDebouncedSave';
export * from './useDiff';
export * from './useFeatureSettings';
export * from './useGitHub';
export * from './useKeyboard';
export * from './useManagedList';
export * from './useMetrics';
export * from './usePreview';
export * from './useProjects';
export * from './useSprints';
export * from './useTaskSync';
export * from './useTaskTypes';
export * from './useTasks';
export * from './useTemplateForm';
export * from './useTemplates';
// Note: useTimeTracking also exports formatDuration - import directly to avoid conflict with useMetrics
export {
useTimeSummary,
useStartTimer,
useStopTimer,
useAddTimeEntry,
useDeleteTimeEntry,
parseDuration,
type TimeSummary,
} from './useTimeTracking';
// formatDuration from useTimeTracking takes seconds; use useMetrics.formatDuration (takes ms) via barrel
export * from './useToast';
export * from './useWorktree';