refactor: simplify ralph wizard from 9 agents to 2

Remove execution layer that never actually controlled execution:
- execution-bridge.ts (model param was ignored)
- group-scheduler.ts (task-tool mode never used)
- model-selector.ts (recommendations were display-only)
- context-manager.ts (never invoked)
- execution-limits.ts

Remove redundant agent prompts (overlapping outputs):
- requirements-analyst, architecture-planner, risk-analyst
- testing-specialist, verification (merged into planner.ts)
- execution-optimizer (output was ignored)
- final-review (scores were cosmetic)

Simplify plan-orchestrator.ts from 2400 LOC to 520 LOC:
- Before: 9 agents, 6 phases, ~40-60 minutes
- After: 2 agents (research + planner), ~18 minutes

Remove /api/execution/* endpoints and related server code.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-30 12:15:09 +01:00
co-authored by Claude Opus 4.5
parent 0020ae5a3e
commit a92826b66d
18 changed files with 329 additions and 5468 deletions
-93
View File
@@ -1,93 +0,0 @@
/**
* @fileoverview Centralized limits for the Execution Bridge system.
*
* These constants define resource limits for parallel task execution,
* group scheduling, and model selection.
*
* @module config/execution-limits
*/
// ============================================================================
// Parallel Execution Limits
// ============================================================================
/**
* Maximum tasks to execute in parallel within a single group.
* Higher values increase throughput but may strain system resources.
*/
export const MAX_PARALLEL_TASKS_PER_GROUP = 5;
/**
* Default timeout for an execution group in milliseconds (30 minutes).
* If all tasks in a group don't complete within this time, stragglers are cancelled.
*/
export const GROUP_TIMEOUT_MS = 30 * 60 * 1000;
/**
* Maximum retry attempts for a failed task before marking as permanently failed.
*/
export const MAX_TASK_RETRIES = 2;
/**
* Delay between task retries in milliseconds (10 seconds).
*/
export const TASK_RETRY_DELAY_MS = 10 * 1000;
// ============================================================================
// Model Selection Limits
// ============================================================================
/**
* Default model when no recommendation is provided.
*/
export const DEFAULT_MODEL: 'opus' | 'sonnet' | 'haiku' = 'sonnet';
/**
* Token threshold for switching execution modes.
* Tasks with estimated tokens below this use task-tool mode.
* Tasks with estimated tokens above this use session mode.
*/
export const TOKEN_THRESHOLD_FOR_SESSION_MODE = 50000;
/**
* Token threshold for low-complexity tasks (haiku-appropriate).
*/
export const TOKEN_THRESHOLD_HAIKU = 15000;
// ============================================================================
// Context Management Limits
// ============================================================================
/**
* Delay between /clear and /init commands in milliseconds.
*/
export const CONTEXT_REFRESH_DELAY_MS = 2000;
/**
* Maximum pending context refresh operations.
*/
export const MAX_PENDING_CONTEXT_REFRESHES = 10;
// ============================================================================
// Execution Bridge Limits
// ============================================================================
/**
* Maximum execution groups to track in history.
*/
export const MAX_EXECUTION_HISTORY = 50;
/**
* Polling interval for execution progress in milliseconds.
*/
export const EXECUTION_POLL_INTERVAL_MS = 1000;
/**
* Maximum total tasks in a single execution plan.
*/
export const MAX_TASKS_PER_PLAN = 100;
/**
* Grace period after group completion before cleanup in milliseconds.
*/
export const GROUP_CLEANUP_DELAY_MS = 5000;
-333
View File
@@ -1,333 +0,0 @@
/**
* @fileoverview Context Manager - Handles fresh context requirements.
*
* Manages context refresh operations for tasks that require starting
* with a clean context window. Supports both /clear + /init sequences
* and new session spawning.
*
* @module context-manager
*/
import { EventEmitter } from 'node:events';
import {
CONTEXT_REFRESH_DELAY_MS,
MAX_PENDING_CONTEXT_REFRESHES,
} from './config/execution-limits.js';
// ========== Types ==========
/** Methods for refreshing context */
export type ContextRefreshMethod = 'clear-init' | 'new-session';
/** Status of a context refresh operation */
export type ContextRefreshStatus = 'pending' | 'clearing' | 'initializing' | 'completed' | 'failed';
/**
* Request for a context refresh.
*/
export interface ContextRefreshRequest {
/** Task ID requesting refresh */
taskId: string;
/** Session ID to refresh */
sessionId: string;
/** Preferred refresh method */
method: ContextRefreshMethod;
/** Working directory (for new-session method) */
workingDir?: string;
/** Optional init prompt to use */
initPrompt?: string;
}
/**
* Result of a context refresh operation.
*/
export interface ContextRefreshResult {
/** Request that was processed */
request: ContextRefreshRequest;
/** Final status */
status: ContextRefreshStatus;
/** New session ID (if method was new-session) */
newSessionId?: string;
/** Error message if failed */
error?: string;
/** Duration in milliseconds */
durationMs: number;
}
/**
* Tracked refresh operation.
*/
interface TrackedRefresh {
request: ContextRefreshRequest;
status: ContextRefreshStatus;
startedAt: number;
completedAt?: number;
error?: string;
newSessionId?: string;
}
// ========== Events ==========
export interface ContextManagerEvents {
/** Context refresh started */
refreshStarted: (data: { taskId: string; sessionId: string; method: ContextRefreshMethod }) => void;
/** Context refresh completed */
refreshCompleted: (result: ContextRefreshResult) => void;
/** Context refresh failed */
refreshFailed: (data: { taskId: string; sessionId: string; error: string }) => void;
}
// ========== Session Writer Interface ==========
/**
* Interface for writing to sessions.
* Injected to avoid circular dependencies.
*/
export interface SessionWriter {
/** Write text to session */
writeToSession(sessionId: string, text: string): void;
/** Create a new session */
createSession?(workingDir: string, name?: string): Promise<{ sessionId: string }>;
}
// ========== Context Manager ==========
/**
* ContextManager - Handles context refresh operations.
*
* When a task requires fresh context (requiresFreshContext=true),
* this manager coordinates the refresh using one of two methods:
*
* 1. clear-init: Send /clear followed by /init to existing session
* 2. new-session: Spawn a completely new session (more expensive)
*/
export class ContextManager extends EventEmitter {
private _pending: Map<string, TrackedRefresh> = new Map();
private _sessionWriter: SessionWriter | null = null;
private _refreshDelayMs: number;
constructor(refreshDelayMs: number = CONTEXT_REFRESH_DELAY_MS) {
super();
this._refreshDelayMs = refreshDelayMs;
}
/**
* Set the session writer for performing actual operations.
*/
setSessionWriter(writer: SessionWriter): void {
this._sessionWriter = writer;
}
/**
* Get number of pending refresh operations.
*/
get pendingCount(): number {
return this._pending.size;
}
/**
* Check if a refresh is pending for a session.
*/
hasPendingRefresh(sessionId: string): boolean {
for (const tracked of this._pending.values()) {
if (tracked.request.sessionId === sessionId &&
(tracked.status === 'pending' || tracked.status === 'clearing' || tracked.status === 'initializing')) {
return true;
}
}
return false;
}
/**
* Request a context refresh for a task.
*
* @returns Promise that resolves when refresh completes
*/
async requestRefresh(request: ContextRefreshRequest): Promise<ContextRefreshResult> {
if (!this._sessionWriter) {
return {
request,
status: 'failed',
error: 'No session writer configured',
durationMs: 0,
};
}
// Check pending limit
if (this._pending.size >= MAX_PENDING_CONTEXT_REFRESHES) {
return {
request,
status: 'failed',
error: `Max pending refreshes (${MAX_PENDING_CONTEXT_REFRESHES}) exceeded`,
durationMs: 0,
};
}
// Track the refresh
const tracked: TrackedRefresh = {
request,
status: 'pending',
startedAt: Date.now(),
};
this._pending.set(request.taskId, tracked);
this.emit('refreshStarted', {
taskId: request.taskId,
sessionId: request.sessionId,
method: request.method,
});
try {
if (request.method === 'clear-init') {
await this.performClearInit(tracked);
} else {
await this.performNewSession(tracked);
}
tracked.status = 'completed';
tracked.completedAt = Date.now();
const result: ContextRefreshResult = {
request,
status: 'completed',
newSessionId: tracked.newSessionId,
durationMs: tracked.completedAt - tracked.startedAt,
};
this.emit('refreshCompleted', result);
return result;
} catch (err) {
tracked.status = 'failed';
tracked.error = err instanceof Error ? err.message : String(err);
tracked.completedAt = Date.now();
const result: ContextRefreshResult = {
request,
status: 'failed',
error: tracked.error,
durationMs: tracked.completedAt - tracked.startedAt,
};
this.emit('refreshFailed', {
taskId: request.taskId,
sessionId: request.sessionId,
error: tracked.error,
});
return result;
} finally {
// Clean up after a delay
setTimeout(() => {
this._pending.delete(request.taskId);
}, 5000);
}
}
/**
* Perform /clear + /init sequence.
*/
private async performClearInit(tracked: TrackedRefresh): Promise<void> {
if (!this._sessionWriter) throw new Error('No session writer');
const { sessionId, initPrompt } = tracked.request;
// Send /clear
tracked.status = 'clearing';
this._sessionWriter.writeToSession(sessionId, '/clear\r');
// Wait for clear to process
await this.delay(this._refreshDelayMs);
// Send /init (or custom init prompt)
tracked.status = 'initializing';
const prompt = initPrompt || '/init';
this._sessionWriter.writeToSession(sessionId, prompt + '\r');
// Wait for init to complete
await this.delay(this._refreshDelayMs);
}
/**
* Spawn a new session for complete context isolation.
*/
private async performNewSession(tracked: TrackedRefresh): Promise<void> {
if (!this._sessionWriter?.createSession) {
throw new Error('Session creation not supported');
}
const { workingDir, taskId } = tracked.request;
if (!workingDir) {
throw new Error('Working directory required for new-session method');
}
tracked.status = 'initializing';
const result = await this._sessionWriter.createSession(workingDir, `task-${taskId}`);
tracked.newSessionId = result.sessionId;
}
/**
* Cancel a pending refresh.
*/
cancelRefresh(taskId: string): boolean {
const tracked = this._pending.get(taskId);
if (tracked && tracked.status === 'pending') {
tracked.status = 'failed';
tracked.error = 'Cancelled';
tracked.completedAt = Date.now();
this._pending.delete(taskId);
return true;
}
return false;
}
/**
* Get status of all pending refreshes.
*/
getPendingStatus(): Array<{ taskId: string; sessionId: string; status: ContextRefreshStatus; elapsedMs: number }> {
const now = Date.now();
return Array.from(this._pending.values()).map(tracked => ({
taskId: tracked.request.taskId,
sessionId: tracked.request.sessionId,
status: tracked.status,
elapsedMs: now - tracked.startedAt,
}));
}
/**
* Clear all pending operations (for cleanup).
*/
clearAll(): void {
this._pending.clear();
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// ========== Singleton ==========
let managerInstance: ContextManager | null = null;
/**
* Get or create the singleton ContextManager instance.
*/
export function getContextManager(): ContextManager {
if (!managerInstance) {
managerInstance = new ContextManager();
}
return managerInstance;
}
/**
* Reset the singleton (for testing).
*/
export function resetContextManager(): void {
if (managerInstance) {
managerInstance.clearAll();
}
managerInstance = null;
}
-846
View File
@@ -1,846 +0,0 @@
/**
* @fileoverview Execution Bridge - Coordinates parallel task execution.
*
* The central coordinator that:
* - Loads optimized plans from PlanOrchestrator
* - Converts plans to executable groups via GroupScheduler
* - Manages parallel execution within groups
* - Coordinates model selection via ModelSelector
* - Handles fresh context requirements via ContextManager
* - Tracks overall execution progress
* - Integrates with SpawnOrchestrator for session-based execution
*
* @module execution-bridge
*/
import { EventEmitter } from 'node:events';
import {
GroupScheduler,
getGroupScheduler,
type ExecutionGroup,
type GroupTask,
type ExecutionSchedule,
} from './group-scheduler.js';
import {
ModelSelector,
getModelSelector,
type ModelConfig,
type ModelSelection,
type ExecutionMode,
} from './model-selector.js';
import {
ContextManager,
getContextManager,
type SessionWriter,
} from './context-manager.js';
import {
MAX_PARALLEL_TASKS_PER_GROUP,
GROUP_TIMEOUT_MS,
MAX_TASK_RETRIES,
TASK_RETRY_DELAY_MS,
EXECUTION_POLL_INTERVAL_MS,
MAX_EXECUTION_HISTORY,
} from './config/execution-limits.js';
// ========== Types ==========
/** Overall execution status */
export type ExecutionStatus = 'idle' | 'loading' | 'running' | 'paused' | 'completed' | 'partial' | 'failed' | 'cancelled';
/**
* Execution progress for UI display.
*/
export interface ExecutionProgress {
/** Current status */
status: ExecutionStatus;
/** Current group being executed */
currentGroup: number | null;
/** Total groups */
totalGroups: number;
/** Completed groups */
completedGroups: number;
/** Total tasks */
totalTasks: number;
/** Completed tasks */
completedTasks: number;
/** Failed tasks */
failedTasks: number;
/** Tasks currently running */
runningTasks: number;
/** Elapsed time in milliseconds */
elapsedMs: number;
/** Estimated remaining time (if available) */
estimatedRemainingMs?: number;
}
/**
* Task assignment for spawning.
*/
export interface TaskAssignment {
/** Task from the schedule */
task: GroupTask;
/** Selected model */
model: ModelSelection;
/** Execution mode */
executionMode: ExecutionMode;
/** Session ID (if session mode) */
sessionId?: string;
/** Whether fresh context was requested */
freshContextRequested: boolean;
}
/**
* Plan item from PlanOrchestrator (input format).
*/
export interface PlanItem {
id: string;
title: string;
description: string;
parallelGroup?: number;
agentType?: string;
recommendedModel?: string;
requiresFreshContext?: boolean;
estimatedTokens?: number;
inputFiles?: string[];
outputFiles?: string[];
dependencies?: string[];
}
/**
* Execution history entry.
*/
export interface ExecutionHistoryEntry {
/** Unique execution ID */
id: string;
/** When execution started */
startedAt: number;
/** When execution ended */
endedAt?: number;
/** Final status */
status: ExecutionStatus;
/** Task counts */
totalTasks: number;
completedTasks: number;
failedTasks: number;
/** Total cost estimate */
estimatedCost?: number;
}
// ========== Events ==========
export interface ExecutionBridgeEvents {
/** Plan loaded and schedule built */
planLoaded: (schedule: ExecutionSchedule) => void;
/** Execution started */
started: () => void;
/** Execution paused */
paused: () => void;
/** Execution resumed */
resumed: () => void;
/** Execution completed */
completed: (result: { status: ExecutionStatus; stats: ExecutionProgress }) => void;
/** Execution cancelled */
cancelled: (reason: string) => void;
/** Group started */
groupStarted: (data: { groupNumber: number; taskCount: number; executionMode: ExecutionMode }) => void;
/** Group completed */
groupCompleted: (data: { groupNumber: number; status: string; completedCount: number; failedCount: number }) => void;
/** Task assigned to execution */
taskAssigned: (assignment: TaskAssignment) => void;
/** Task completed */
taskCompleted: (data: { taskId: string; groupNumber: number; durationMs: number }) => void;
/** Task failed */
taskFailed: (data: { taskId: string; groupNumber: number; error: string; willRetry: boolean }) => void;
/** Fresh context triggered */
freshContext: (data: { taskId: string; method: string; success: boolean }) => void;
/** Model selected for task */
modelSelected: (data: { taskId: string; model: string; reason: string; optimizerSuggested?: string }) => void;
/** Progress update */
progress: (progress: ExecutionProgress) => void;
}
// ========== Spawn Interface ==========
/**
* Interface for spawning agents.
* Injected to avoid circular dependencies.
*/
export interface AgentSpawner {
/** Spawn an agent with specific model */
spawnAgentWithModel(
taskId: string,
workingDir: string,
prompt: string,
model: string,
options?: { requiresFreshContext?: boolean }
): Promise<{ sessionId: string }>;
/** Use Task tool for lightweight execution */
useTaskTool(
sessionId: string,
taskId: string,
prompt: string,
model: string
): Promise<void>;
/** Check if task is complete */
isTaskComplete(taskId: string): boolean;
/** Get task result */
getTaskResult(taskId: string): { success: boolean; output?: string; error?: string } | null;
}
// ========== Execution Bridge ==========
/**
* ExecutionBridge - Coordinates parallel task execution.
*
* This is the main entry point for the optimized execution system.
* It bridges the gap between PlanOrchestrator's output and actual
* task execution, making use of the optimizer's metadata.
*/
export class ExecutionBridge extends EventEmitter {
private _scheduler: GroupScheduler;
private _modelSelector: ModelSelector;
private _contextManager: ContextManager;
private _status: ExecutionStatus = 'idle';
private _executionId: string | null = null;
private _startedAt: number | null = null;
private _pausedAt: number | null = null;
private _agentSpawner: AgentSpawner | null = null;
private _sessionWriter: SessionWriter | null = null;
private _pollTimer: NodeJS.Timeout | null = null;
private _groupTimeoutTimers: Map<number, NodeJS.Timeout> = new Map();
private _retryTimers: Map<string, NodeJS.Timeout> = new Map();
private _runningTasks: Map<string, { startedAt: number; sessionId?: string }> = new Map();
private _history: ExecutionHistoryEntry[] = [];
private _workingDir: string = process.cwd();
constructor(modelConfig?: Partial<ModelConfig>) {
super();
this._scheduler = getGroupScheduler();
this._modelSelector = getModelSelector(modelConfig);
this._contextManager = getContextManager();
// Forward scheduler events
this._scheduler.on('groupStarted', group => {
this.emit('groupStarted', {
groupNumber: group.groupNumber,
taskCount: group.tasks.length,
executionMode: group.executionMode,
});
});
this._scheduler.on('groupCompleted', group => {
// Clear the group timeout timer to prevent memory leak
const timer = this._groupTimeoutTimers.get(group.groupNumber);
if (timer) {
clearTimeout(timer);
this._groupTimeoutTimers.delete(group.groupNumber);
}
this.emit('groupCompleted', {
groupNumber: group.groupNumber,
status: group.status,
completedCount: group.completedCount,
failedCount: group.failedCount,
});
});
this._scheduler.on('taskStatusChanged', data => {
if (data.newStatus === 'completed') {
const runningInfo = this._runningTasks.get(data.taskId);
const durationMs = runningInfo ? Date.now() - runningInfo.startedAt : 0;
this._runningTasks.delete(data.taskId);
this.emit('taskCompleted', { taskId: data.taskId, groupNumber: data.groupNumber, durationMs });
}
});
}
/**
* Get current execution status.
*/
get status(): ExecutionStatus {
return this._status;
}
/**
* Get current execution progress.
*/
getProgress(): ExecutionProgress {
const stats = this._scheduler.getStats();
const schedule = this._scheduler.schedule;
return {
status: this._status,
currentGroup: schedule?.currentGroupIndex ?? null,
totalGroups: stats.totalGroups,
completedGroups: stats.completedGroups,
totalTasks: stats.totalTasks,
completedTasks: stats.completedTasks,
failedTasks: stats.failedTasks,
runningTasks: this._runningTasks.size,
elapsedMs: this.calculateElapsedMs(),
};
}
/**
* Calculate elapsed time, accounting for pauses.
*/
private calculateElapsedMs(): number {
if (!this._startedAt) return 0;
if (this._pausedAt) {
return this._pausedAt - this._startedAt;
}
return Date.now() - this._startedAt;
}
/**
* Set the agent spawner for session-based execution.
*/
setAgentSpawner(spawner: AgentSpawner): void {
this._agentSpawner = spawner;
}
/**
* Set the session writer for context management.
*/
setSessionWriter(writer: SessionWriter): void {
this._sessionWriter = writer;
this._contextManager.setSessionWriter(writer);
}
/**
* Set working directory for execution.
*/
setWorkingDir(dir: string): void {
this._workingDir = dir;
}
/**
* Update model configuration.
*/
updateModelConfig(config: Partial<ModelConfig>): void {
this._modelSelector.updateConfig(config);
}
/**
* Get current model configuration.
*/
getModelConfig(): ModelConfig {
return this._modelSelector.config;
}
/**
* Load a plan and build execution schedule.
*/
loadPlan(items: PlanItem[]): ExecutionSchedule {
if (this._status === 'running') {
throw new Error('Cannot load plan while execution is running');
}
this._status = 'loading';
const schedule = this._scheduler.buildSchedule(items);
this._status = 'idle';
this.emit('planLoaded', schedule);
return schedule;
}
/**
* Start execution of the loaded plan.
*/
async start(): Promise<void> {
const schedule = this._scheduler.schedule;
if (!schedule) {
throw new Error('No plan loaded');
}
if (this._status === 'running') {
return;
}
if (!this._agentSpawner) {
throw new Error('No agent spawner configured');
}
this._status = 'running';
this._executionId = `exec-${Date.now()}`;
this._startedAt = Date.now();
// Add to history
this._history.unshift({
id: this._executionId,
startedAt: this._startedAt,
status: 'running',
totalTasks: schedule.totalTasks,
completedTasks: 0,
failedTasks: 0,
});
// Trim history
if (this._history.length > MAX_EXECUTION_HISTORY) {
this._history = this._history.slice(0, MAX_EXECUTION_HISTORY);
}
this.emit('started');
// Start the execution loop
this.startExecutionLoop();
}
/**
* Pause execution.
*/
pause(): void {
if (this._status !== 'running') return;
this._status = 'paused';
this._pausedAt = Date.now();
this.stopExecutionLoop();
this.emit('paused');
}
/**
* Resume paused execution.
*/
resume(): void {
if (this._status !== 'paused') return;
this._status = 'running';
this._pausedAt = null;
this.startExecutionLoop();
this.emit('resumed');
}
/**
* Cancel execution.
*/
async cancel(reason: string = 'User cancelled'): Promise<void> {
if (this._status === 'idle' || this._status === 'completed' || this._status === 'cancelled') {
return;
}
this._status = 'cancelled';
this.stopExecutionLoop();
// Clear group timeouts
for (const timer of this._groupTimeoutTimers.values()) {
clearTimeout(timer);
}
this._groupTimeoutTimers.clear();
// Update history
this.updateHistoryEntry('cancelled');
this.emit('cancelled', reason);
}
/**
* Get execution history.
*/
getHistory(): ExecutionHistoryEntry[] {
return [...this._history];
}
/**
* Start the main execution loop.
*/
private startExecutionLoop(): void {
if (this._pollTimer) return;
this._pollTimer = setInterval(() => {
this.tick().catch(err => {
console.error('[execution-bridge] Tick error:', err);
});
}, EXECUTION_POLL_INTERVAL_MS);
// Run immediately
this.tick().catch(err => {
console.error('[execution-bridge] Initial tick error:', err);
});
}
/**
* Stop the execution loop.
*/
private stopExecutionLoop(): void {
if (this._pollTimer) {
clearInterval(this._pollTimer);
this._pollTimer = null;
}
}
/**
* Main execution tick.
*/
private async tick(): Promise<void> {
if (this._status !== 'running') return;
const schedule = this._scheduler.schedule;
if (!schedule) return;
// Check if we're done
if (schedule.status === 'completed' || schedule.status === 'partial' || schedule.status === 'failed') {
this.handleExecutionComplete();
return;
}
// Process current group or start next one
await this.processGroups();
// Emit progress
this.emit('progress', this.getProgress());
}
/**
* Process groups - start new ones or continue existing.
*/
private async processGroups(): Promise<void> {
const schedule = this._scheduler.schedule;
if (!schedule) return;
// Find current running group
const runningGroup = schedule.groups.find(g => g.status === 'running');
if (runningGroup) {
// Continue processing current group
await this.processGroup(runningGroup);
} else {
// Try to start next group
const nextGroup = this._scheduler.getNextReadyGroup();
if (nextGroup) {
await this.startGroup(nextGroup);
}
}
}
/**
* Start executing a group.
*/
private async startGroup(group: ExecutionGroup): Promise<void> {
this._scheduler.startGroup(group.groupNumber);
// Set up group timeout
const timeoutTimer = setTimeout(() => {
this.handleGroupTimeout(group.groupNumber);
}, GROUP_TIMEOUT_MS);
this._groupTimeoutTimers.set(group.groupNumber, timeoutTimer);
// Start initial tasks
await this.processGroup(group);
}
/**
* Process tasks within a group.
*/
private async processGroup(group: ExecutionGroup): Promise<void> {
if (!this._agentSpawner) return;
// Get ready tasks
const readyTasks = this._scheduler.getReadyTasksInGroup(group.groupNumber);
if (readyTasks.length === 0) return;
// Limit parallel tasks
const slotsAvailable = MAX_PARALLEL_TASKS_PER_GROUP - this._runningTasks.size;
if (slotsAvailable <= 0) return;
const tasksToStart = readyTasks.slice(0, slotsAvailable);
for (const task of tasksToStart) {
await this.assignTask(task, group);
}
}
/**
* Assign a task for execution.
*/
private async assignTask(task: GroupTask, group: ExecutionGroup): Promise<void> {
if (!this._agentSpawner) return;
// Select model
const modelSelection = this._modelSelector.selectModel(task.id, {
estimatedTokens: task.estimatedTokens,
agentType: task.agentType,
recommendedModel: task.recommendedModel,
outputFiles: task.outputFiles,
inputFiles: task.inputFiles,
});
this.emit('modelSelected', {
taskId: task.id,
model: modelSelection.model,
reason: modelSelection.reason,
optimizerSuggested: modelSelection.optimizerRecommendation,
});
// Handle fresh context if required
let freshContextRequested = false;
if (task.requiresFreshContext && this._sessionWriter) {
freshContextRequested = true;
// Context refresh will be handled by the spawner
}
// Mark task as running
this._scheduler.updateTaskStatus(task.id, 'running');
this._runningTasks.set(task.id, { startedAt: Date.now() });
const assignment: TaskAssignment = {
task,
model: modelSelection,
executionMode: group.executionMode,
freshContextRequested,
};
this.emit('taskAssigned', assignment);
// Execute based on mode
try {
if (group.executionMode === 'session') {
const result = await this._agentSpawner.spawnAgentWithModel(
task.id,
this._workingDir,
task.description,
modelSelection.model,
{ requiresFreshContext: task.requiresFreshContext }
);
assignment.sessionId = result.sessionId;
this._runningTasks.set(task.id, { startedAt: Date.now(), sessionId: result.sessionId });
} else {
// task-tool mode - would use Task tool in main session
// For now, fall back to session mode
const result = await this._agentSpawner.spawnAgentWithModel(
task.id,
this._workingDir,
task.description,
modelSelection.model
);
assignment.sessionId = result.sessionId;
this._runningTasks.set(task.id, { startedAt: Date.now(), sessionId: result.sessionId });
}
if (task.requiresFreshContext) {
this.emit('freshContext', {
taskId: task.id,
method: 'session',
success: true,
});
}
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
await this.handleTaskFailure(task, group.groupNumber, error);
}
}
/**
* Handle task failure.
*/
private async handleTaskFailure(task: GroupTask, groupNumber: number, error: string): Promise<void> {
task.retryCount++;
const willRetry = task.retryCount < MAX_TASK_RETRIES;
this.emit('taskFailed', {
taskId: task.id,
groupNumber,
error,
willRetry,
});
if (willRetry) {
// Schedule retry with tracked timer
const retryTimer = setTimeout(() => {
this._retryTimers.delete(task.id);
task.status = 'pending';
task.error = undefined;
this._runningTasks.delete(task.id);
}, TASK_RETRY_DELAY_MS);
this._retryTimers.set(task.id, retryTimer);
} else {
// Mark as permanently failed
this._scheduler.updateTaskStatus(task.id, 'failed', error);
this._runningTasks.delete(task.id);
// Mark dependent tasks as blocked
this._scheduler.markDependentTasksBlocked(task.id);
}
}
/**
* Mark a task as complete (called externally when agent finishes).
*/
markTaskComplete(taskId: string): void {
const schedule = this._scheduler.schedule;
if (!schedule) return;
const groupNum = this.findTaskGroup(taskId);
if (groupNum === null) return;
// Clear any pending retry timer for this task
const retryTimer = this._retryTimers.get(taskId);
if (retryTimer) {
clearTimeout(retryTimer);
this._retryTimers.delete(taskId);
}
this._scheduler.updateTaskStatus(taskId, 'completed');
this._runningTasks.delete(taskId);
}
/**
* Mark a task as failed (called externally when agent fails).
*/
markTaskFailed(taskId: string, error: string): void {
const schedule = this._scheduler.schedule;
if (!schedule) return;
const groupNum = this.findTaskGroup(taskId);
if (groupNum === null) return;
// Get the task to check retry count
for (const group of schedule.groups) {
const task = group.tasks.find(t => t.id === taskId);
if (task) {
this.handleTaskFailure(task, group.groupNumber, error);
return;
}
}
}
/**
* Find which group a task belongs to.
*/
private findTaskGroup(taskId: string): number | null {
const schedule = this._scheduler.schedule;
if (!schedule) return null;
for (const group of schedule.groups) {
if (group.tasks.some(t => t.id === taskId)) {
return group.groupNumber;
}
}
return null;
}
/**
* Handle group timeout.
*/
private handleGroupTimeout(groupNumber: number): void {
const schedule = this._scheduler.schedule;
if (!schedule) return;
const group = schedule.groups.find(g => g.groupNumber === groupNumber);
if (!group || group.status !== 'running') return;
console.warn(`[execution-bridge] Group ${groupNumber} timed out`);
// Mark running tasks as failed
for (const task of group.tasks) {
if (task.status === 'running') {
this._scheduler.updateTaskStatus(task.id, 'failed', 'Group timeout');
this._runningTasks.delete(task.id);
} else if (task.status === 'pending') {
this._scheduler.updateTaskStatus(task.id, 'skipped', 'Group timeout');
}
}
this._groupTimeoutTimers.delete(groupNumber);
}
/**
* Handle execution complete.
*/
private handleExecutionComplete(): void {
this.stopExecutionLoop();
// Clear timers
for (const timer of this._groupTimeoutTimers.values()) {
clearTimeout(timer);
}
this._groupTimeoutTimers.clear();
const schedule = this._scheduler.schedule;
if (!schedule) return;
// Map schedule status to execution status
if (schedule.status === 'completed') {
this._status = 'completed';
} else if (schedule.status === 'partial') {
this._status = 'partial';
} else {
this._status = 'failed';
}
// Update history
this.updateHistoryEntry(this._status);
this.emit('completed', {
status: this._status,
stats: this.getProgress(),
});
}
/**
* Update history entry for current execution.
*/
private updateHistoryEntry(status: ExecutionStatus): void {
if (!this._executionId) return;
const entry = this._history.find(e => e.id === this._executionId);
if (entry) {
entry.status = status;
entry.endedAt = Date.now();
entry.completedTasks = this._scheduler.getStats().completedTasks;
entry.failedTasks = this._scheduler.getStats().failedTasks;
}
}
/**
* Reset the bridge for a new execution.
*/
reset(): void {
this.stopExecutionLoop();
for (const timer of this._groupTimeoutTimers.values()) {
clearTimeout(timer);
}
this._groupTimeoutTimers.clear();
// Clear any pending retry timers
for (const timer of this._retryTimers.values()) {
clearTimeout(timer);
}
this._retryTimers.clear();
this._scheduler.reset();
this._runningTasks.clear();
this._status = 'idle';
this._executionId = null;
this._startedAt = null;
this._pausedAt = null;
}
}
// ========== Singleton ==========
let bridgeInstance: ExecutionBridge | null = null;
/**
* Get or create the singleton ExecutionBridge instance.
*/
export function getExecutionBridge(modelConfig?: Partial<ModelConfig>): ExecutionBridge {
if (!bridgeInstance) {
bridgeInstance = new ExecutionBridge(modelConfig);
}
return bridgeInstance;
}
/**
* Reset the singleton (for testing).
*/
export function resetExecutionBridge(): void {
if (bridgeInstance) {
bridgeInstance.reset();
}
bridgeInstance = null;
}
-551
View File
@@ -1,551 +0,0 @@
/**
* @fileoverview Group Scheduler - Topological ordering and dependency management.
*
* Builds execution schedules from parallel groups, manages dependencies
* between groups, and tracks group-level completion status.
*
* @module group-scheduler
*/
import { EventEmitter } from 'node:events';
import type { ExecutionMode } from './model-selector.js';
import type { ModelTier, AgentType } from './model-selector.js';
// ========== Types ==========
/** Status of a task within a group */
export type GroupTaskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'blocked' | 'skipped';
/** Status of an execution group */
export type ExecutionGroupStatus = 'pending' | 'ready' | 'running' | 'completed' | 'partial' | 'failed';
/**
* A task within an execution group.
*/
export interface GroupTask {
/** Task ID from the plan */
id: string;
/** Task title/description */
title: string;
/** Full task description */
description: string;
/** Parallel group number */
parallelGroup: number;
/** Agent type for model selection */
agentType: AgentType;
/** Recommended model */
recommendedModel?: ModelTier;
/** Whether task requires fresh context */
requiresFreshContext: boolean;
/** Estimated token usage */
estimatedTokens?: number;
/** Input files (read-only) */
inputFiles?: string[];
/** Output files (will be modified) */
outputFiles?: string[];
/** Current status */
status: GroupTaskStatus;
/** Task dependencies (other task IDs) */
dependencies: string[];
/** Error message if failed */
error?: string;
/** Retry count */
retryCount: number;
}
/**
* An execution group containing tasks that can run in parallel.
*/
export interface ExecutionGroup {
/** Group number (from parallelGroup) */
groupNumber: number;
/** Tasks in this group */
tasks: GroupTask[];
/** Group status */
status: ExecutionGroupStatus;
/** Execution mode for this group */
executionMode: ExecutionMode;
/** Rationale for execution mode choice */
executionModeRationale: string;
/** Groups that must complete before this one */
dependsOnGroups: number[];
/** When group started executing */
startedAt?: number;
/** When group completed */
completedAt?: number;
/** Number of tasks completed */
completedCount: number;
/** Number of tasks failed */
failedCount: number;
/** Number of tasks skipped due to dependency failures */
skippedCount: number;
}
/**
* Full execution schedule.
*/
export interface ExecutionSchedule {
/** Ordered groups (by dependency order) */
groups: ExecutionGroup[];
/** Total task count */
totalTasks: number;
/** Completed task count */
completedTasks: number;
/** Failed task count */
failedTasks: number;
/** Current executing group index (-1 if not started) */
currentGroupIndex: number;
/** Overall status */
status: 'pending' | 'running' | 'completed' | 'partial' | 'failed';
}
// ========== Events ==========
export interface GroupSchedulerEvents {
/** Schedule built from plan */
scheduleBuilt: (schedule: ExecutionSchedule) => void;
/** Group started executing */
groupStarted: (group: ExecutionGroup) => void;
/** Group completed (fully or partially) */
groupCompleted: (group: ExecutionGroup) => void;
/** Task status changed */
taskStatusChanged: (data: { taskId: string; groupNumber: number; oldStatus: GroupTaskStatus; newStatus: GroupTaskStatus }) => void;
}
// ========== Group Scheduler ==========
/**
* GroupScheduler - Manages execution order and dependencies.
*
* Responsibilities:
* - Build topologically ordered groups from plan items
* - Track group dependencies (lower groups must complete first)
* - Handle partial failures (continue with independent tasks)
* - Determine group execution mode (session vs task-tool)
*/
export class GroupScheduler extends EventEmitter {
private _schedule: ExecutionSchedule | null = null;
private _taskToGroup: Map<string, number> = new Map();
constructor() {
super();
}
/**
* Get current schedule.
*/
get schedule(): ExecutionSchedule | null {
return this._schedule;
}
/**
* Build execution schedule from plan items.
*
* @param items - Array of plan items with parallelGroup assignments
* @returns Built execution schedule
*/
buildSchedule(items: Array<{
id: string;
title: string;
description: string;
parallelGroup?: number;
agentType?: string;
recommendedModel?: string;
requiresFreshContext?: boolean;
estimatedTokens?: number;
inputFiles?: string[];
outputFiles?: string[];
dependencies?: string[];
}>): ExecutionSchedule {
// Group items by parallel group
const groupMap = new Map<number, GroupTask[]>();
for (const item of items) {
const groupNum = item.parallelGroup ?? 0;
const task: GroupTask = {
id: item.id,
title: item.title,
description: item.description,
parallelGroup: groupNum,
agentType: (item.agentType as AgentType) ?? 'general',
recommendedModel: item.recommendedModel as ModelTier | undefined,
requiresFreshContext: item.requiresFreshContext ?? false,
estimatedTokens: item.estimatedTokens,
inputFiles: item.inputFiles,
outputFiles: item.outputFiles,
status: 'pending',
dependencies: item.dependencies ?? [],
retryCount: 0,
};
if (!groupMap.has(groupNum)) {
groupMap.set(groupNum, []);
}
groupMap.get(groupNum)!.push(task);
this._taskToGroup.set(item.id, groupNum);
}
// Sort groups by number
const sortedGroupNums = Array.from(groupMap.keys()).sort((a, b) => a - b);
// Build execution groups
const groups: ExecutionGroup[] = sortedGroupNums.map(groupNum => {
const tasks = groupMap.get(groupNum)!;
// Determine which groups this one depends on
const dependsOnGroups = new Set<number>();
for (const task of tasks) {
for (const depId of task.dependencies) {
const depGroup = this._taskToGroup.get(depId);
if (depGroup !== undefined && depGroup !== groupNum && depGroup < groupNum) {
dependsOnGroups.add(depGroup);
}
}
}
// Determine execution mode based on task characteristics
const { mode, rationale } = this.determineGroupExecutionMode(tasks);
return {
groupNumber: groupNum,
tasks,
status: 'pending',
executionMode: mode,
executionModeRationale: rationale,
dependsOnGroups: Array.from(dependsOnGroups).sort((a, b) => a - b),
completedCount: 0,
failedCount: 0,
skippedCount: 0,
};
});
this._schedule = {
groups,
totalTasks: items.length,
completedTasks: 0,
failedTasks: 0,
currentGroupIndex: -1,
status: 'pending',
};
this.emit('scheduleBuilt', this._schedule);
return this._schedule;
}
/**
* Determine execution mode for a group based on task characteristics.
*/
private determineGroupExecutionMode(tasks: GroupTask[]): { mode: ExecutionMode; rationale: string } {
// High token estimate → session mode
const highTokenTask = tasks.find(t => t.estimatedTokens && t.estimatedTokens > 50000);
if (highTokenTask) {
return {
mode: 'session',
rationale: `Task ${highTokenTask.id} has high token estimate (${highTokenTask.estimatedTokens})`,
};
}
// Complex agent types → session mode
const complexTask = tasks.find(t => t.agentType === 'implement' || t.agentType === 'review');
if (complexTask) {
return {
mode: 'session',
rationale: `Task ${complexTask.id} has complex agent type (${complexTask.agentType})`,
};
}
// Multiple output files in any task → session mode
const multiOutputTask = tasks.find(t => t.outputFiles && t.outputFiles.length > 2);
if (multiOutputTask) {
return {
mode: 'session',
rationale: `Task ${multiOutputTask.id} has multiple output files (${multiOutputTask.outputFiles!.length})`,
};
}
// Fresh context required → session mode
const freshContextTask = tasks.find(t => t.requiresFreshContext);
if (freshContextTask) {
return {
mode: 'session',
rationale: `Task ${freshContextTask.id} requires fresh context`,
};
}
// All low-token explore tasks → task-tool mode
const allLowToken = tasks.every(t => !t.estimatedTokens || t.estimatedTokens < 15000);
const allExplore = tasks.every(t => t.agentType === 'explore' || t.agentType === 'general');
if (allLowToken && allExplore) {
return {
mode: 'task-tool',
rationale: 'All tasks are low-token explore/general tasks',
};
}
// Default to session mode for reliability
return {
mode: 'session',
rationale: 'Default to session mode for reliability',
};
}
/**
* Get the next group ready for execution.
*/
getNextReadyGroup(): ExecutionGroup | null {
if (!this._schedule) return null;
for (const group of this._schedule.groups) {
if (group.status === 'pending' && this.areGroupDependenciesSatisfied(group)) {
group.status = 'ready';
return group;
}
}
return null;
}
/**
* Check if a group's dependencies are satisfied.
*/
areGroupDependenciesSatisfied(group: ExecutionGroup): boolean {
if (!this._schedule) return false;
for (const depGroupNum of group.dependsOnGroups) {
const depGroup = this._schedule.groups.find(g => g.groupNumber === depGroupNum);
if (!depGroup) continue;
// Dependency must be completed (fully or partially)
if (depGroup.status !== 'completed' && depGroup.status !== 'partial') {
return false;
}
}
return true;
}
/**
* Mark a group as started.
*/
startGroup(groupNumber: number): void {
if (!this._schedule) return;
const group = this._schedule.groups.find(g => g.groupNumber === groupNumber);
if (!group) return;
group.status = 'running';
group.startedAt = Date.now();
this._schedule.status = 'running';
this._schedule.currentGroupIndex = this._schedule.groups.indexOf(group);
this.emit('groupStarted', group);
}
/**
* Update task status within a group.
*/
updateTaskStatus(taskId: string, status: GroupTaskStatus, error?: string): void {
if (!this._schedule) return;
const groupNum = this._taskToGroup.get(taskId);
if (groupNum === undefined) return;
const group = this._schedule.groups.find(g => g.groupNumber === groupNum);
if (!group) return;
const task = group.tasks.find(t => t.id === taskId);
if (!task) return;
const oldStatus = task.status;
task.status = status;
if (error) task.error = error;
// Update group counters
if (status === 'completed') {
group.completedCount++;
this._schedule.completedTasks++;
} else if (status === 'failed') {
group.failedCount++;
this._schedule.failedTasks++;
} else if (status === 'skipped') {
group.skippedCount++;
}
this.emit('taskStatusChanged', { taskId, groupNumber: groupNum, oldStatus, newStatus: status });
// Check if group is complete
this.checkGroupCompletion(group);
}
/**
* Mark tasks blocked by a failed dependency.
*/
markDependentTasksBlocked(failedTaskId: string): void {
if (!this._schedule) return;
for (const group of this._schedule.groups) {
for (const task of group.tasks) {
if (task.dependencies.includes(failedTaskId) && task.status === 'pending') {
this.updateTaskStatus(task.id, 'skipped', `Blocked by failed task ${failedTaskId}`);
}
}
}
}
/**
* Get tasks ready to execute in a group.
*/
getReadyTasksInGroup(groupNumber: number): GroupTask[] {
if (!this._schedule) return [];
const group = this._schedule.groups.find(g => g.groupNumber === groupNumber);
if (!group) return [];
return group.tasks.filter(task => {
if (task.status !== 'pending') return false;
// Check if task dependencies are satisfied (within and across groups)
for (const depId of task.dependencies) {
// Check if dependency is in the same group
const sameGroupDep = group.tasks.find(t => t.id === depId);
if (sameGroupDep && sameGroupDep.status !== 'completed') {
return false;
}
// Check if dependency is in a different group
const depGroupNum = this._taskToGroup.get(depId);
if (depGroupNum !== undefined && depGroupNum !== groupNumber) {
const depGroup = this._schedule!.groups.find(g => g.groupNumber === depGroupNum);
const depTask = depGroup?.tasks.find(t => t.id === depId);
if (depTask && depTask.status !== 'completed') {
return false;
}
}
}
return true;
});
}
/**
* Check if a group has completed (all tasks done, failed, or skipped).
*/
private checkGroupCompletion(group: ExecutionGroup): void {
const pendingOrRunning = group.tasks.filter(
t => t.status === 'pending' || t.status === 'running'
);
if (pendingOrRunning.length > 0) return;
group.completedAt = Date.now();
// Determine final status
if (group.failedCount === 0 && group.skippedCount === 0) {
group.status = 'completed';
} else if (group.completedCount > 0) {
group.status = 'partial';
} else {
group.status = 'failed';
}
this.emit('groupCompleted', group);
// Check if all groups are done
this.checkScheduleCompletion();
}
/**
* Check if entire schedule has completed.
*/
private checkScheduleCompletion(): void {
if (!this._schedule) return;
const pendingOrRunning = this._schedule.groups.filter(
g => g.status === 'pending' || g.status === 'ready' || g.status === 'running'
);
if (pendingOrRunning.length > 0) return;
// Determine final status
const failedGroups = this._schedule.groups.filter(g => g.status === 'failed');
const partialGroups = this._schedule.groups.filter(g => g.status === 'partial');
if (failedGroups.length === this._schedule.groups.length) {
this._schedule.status = 'failed';
} else if (failedGroups.length > 0 || partialGroups.length > 0) {
this._schedule.status = 'partial';
} else {
this._schedule.status = 'completed';
}
}
/**
* Get schedule statistics.
*/
getStats(): {
totalGroups: number;
completedGroups: number;
failedGroups: number;
partialGroups: number;
totalTasks: number;
completedTasks: number;
failedTasks: number;
skippedTasks: number;
} {
if (!this._schedule) {
return {
totalGroups: 0,
completedGroups: 0,
failedGroups: 0,
partialGroups: 0,
totalTasks: 0,
completedTasks: 0,
failedTasks: 0,
skippedTasks: 0,
};
}
return {
totalGroups: this._schedule.groups.length,
completedGroups: this._schedule.groups.filter(g => g.status === 'completed').length,
failedGroups: this._schedule.groups.filter(g => g.status === 'failed').length,
partialGroups: this._schedule.groups.filter(g => g.status === 'partial').length,
totalTasks: this._schedule.totalTasks,
completedTasks: this._schedule.completedTasks,
failedTasks: this._schedule.failedTasks,
skippedTasks: this._schedule.groups.reduce((sum, g) => sum + g.skippedCount, 0),
};
}
/**
* Reset the scheduler.
*/
reset(): void {
this._schedule = null;
this._taskToGroup.clear();
}
}
// ========== Singleton ==========
let schedulerInstance: GroupScheduler | null = null;
/**
* Get or create the singleton GroupScheduler instance.
*/
export function getGroupScheduler(): GroupScheduler {
if (!schedulerInstance) {
schedulerInstance = new GroupScheduler();
}
return schedulerInstance;
}
/**
* Reset the singleton (for testing).
*/
export function resetGroupScheduler(): void {
if (schedulerInstance) {
schedulerInstance.reset();
}
schedulerInstance = null;
}
-320
View File
@@ -1,320 +0,0 @@
/**
* @fileoverview Model Selector - Routes tasks to appropriate Claude models.
*
* Handles model selection based on:
* - User-configured default model
* - Optimizer recommendations (advisory)
* - Agent type mappings
* - Per-task overrides
*
* @module model-selector
*/
import { EventEmitter } from 'node:events';
import {
DEFAULT_MODEL,
TOKEN_THRESHOLD_HAIKU,
TOKEN_THRESHOLD_FOR_SESSION_MODE,
} from './config/execution-limits.js';
// ========== Types ==========
/** Supported Claude model tiers */
export type ModelTier = 'opus' | 'sonnet' | 'haiku';
/** Agent types that influence model selection */
export type AgentType = 'explore' | 'implement' | 'test' | 'review' | 'general';
/** Execution mode for tasks */
export type ExecutionMode = 'session' | 'task-tool';
/**
* User-configurable model settings.
* Stored in settings.json and editable via App Settings.
*/
export interface ModelConfig {
/** User's preferred default model */
defaultModel: ModelTier;
/** Whether to show optimizer recommendations in UI (advisory only) */
showRecommendations: boolean;
/** Override map for specific agent types */
agentTypeOverrides: Partial<Record<AgentType, ModelTier>>;
}
/**
* Model selection result with reasoning.
*/
export interface ModelSelection {
/** The model to use */
model: ModelTier;
/** Why this model was selected */
reason: string;
/** What the optimizer recommended (if different) */
optimizerRecommendation?: ModelTier;
/** Was user default used? */
usedUserDefault: boolean;
}
/**
* Execution mode selection result.
*/
export interface ExecutionModeSelection {
/** How to execute this task */
mode: ExecutionMode;
/** Why this mode was selected */
rationale: string;
}
/**
* Task characteristics for selection decisions.
*/
export interface TaskCharacteristics {
/** Estimated token usage */
estimatedTokens?: number;
/** Agent type */
agentType?: AgentType;
/** Optimizer's recommended model */
recommendedModel?: ModelTier;
/** Files task will modify */
outputFiles?: string[];
/** Files task will read */
inputFiles?: string[];
/** Task complexity hint */
complexity?: 'low' | 'medium' | 'high';
}
// ========== Events ==========
export interface ModelSelectorEvents {
/** Emitted when model is selected */
modelSelected: (data: { taskId: string; selection: ModelSelection }) => void;
/** Emitted when config changes */
configUpdated: (config: ModelConfig) => void;
}
// ========== Default Configuration ==========
/**
* Default optimizer recommendations by agent type.
* These are advisory - user default always wins.
*/
const DEFAULT_RECOMMENDATIONS: Record<AgentType, ModelTier> = {
explore: 'haiku',
implement: 'sonnet',
test: 'sonnet',
review: 'opus',
general: 'sonnet',
};
/**
* Creates default model configuration.
*/
export function createDefaultModelConfig(): ModelConfig {
return {
defaultModel: DEFAULT_MODEL,
showRecommendations: true,
agentTypeOverrides: {},
};
}
// ========== Model Selector ==========
/**
* ModelSelector - Manages model selection for task execution.
*
* User preferences always take precedence. Optimizer recommendations
* are shown in the UI for awareness but don't override user settings.
*/
export class ModelSelector extends EventEmitter {
private _config: ModelConfig;
constructor(config?: Partial<ModelConfig>) {
super();
this._config = { ...createDefaultModelConfig(), ...config };
}
/**
* Get current configuration.
*/
get config(): ModelConfig {
return { ...this._config };
}
/**
* Update configuration.
*/
updateConfig(config: Partial<ModelConfig>): void {
Object.assign(this._config, config);
this.emit('configUpdated', this._config);
}
/**
* Select model for a task.
*
* Priority order:
* 1. User's agent type override (if set)
* 2. User's default model
* 3. Optimizer recommendation (only if no user preference)
*/
selectModel(taskId: string, characteristics: TaskCharacteristics): ModelSelection {
const { agentType, recommendedModel } = characteristics;
let model: ModelTier;
let reason: string;
let usedUserDefault = false;
// Check for agent type override
if (agentType && this._config.agentTypeOverrides[agentType]) {
model = this._config.agentTypeOverrides[agentType]!;
reason = `User override for ${agentType} tasks`;
} else {
// Use user's default model
model = this._config.defaultModel;
reason = 'User default model';
usedUserDefault = true;
}
// Determine what optimizer would have recommended
const optimizerRecommendation = recommendedModel ||
(agentType ? DEFAULT_RECOMMENDATIONS[agentType] : undefined);
const selection: ModelSelection = {
model,
reason,
usedUserDefault,
};
// Include optimizer recommendation if different (for UI display)
if (optimizerRecommendation && optimizerRecommendation !== model) {
selection.optimizerRecommendation = optimizerRecommendation;
if (this._config.showRecommendations) {
selection.reason += ` (optimizer suggested ${optimizerRecommendation})`;
}
}
this.emit('modelSelected', { taskId, selection });
return selection;
}
/**
* Select execution mode for a task.
*
* Decision based on task characteristics:
* - High token estimate → session mode (needs full context)
* - Complex agent types → session mode (dedicated Claude)
* - Low token estimate → task-tool mode (efficient)
* - Multiple output files → session mode (avoid conflicts)
* - Read-only tasks → task-tool mode (no side effects)
*/
selectExecutionMode(characteristics: TaskCharacteristics): ExecutionModeSelection {
const { estimatedTokens, agentType, outputFiles, inputFiles, complexity } = characteristics;
// High token estimate needs session mode
if (estimatedTokens && estimatedTokens > TOKEN_THRESHOLD_FOR_SESSION_MODE) {
return {
mode: 'session',
rationale: `High token estimate (${estimatedTokens} > ${TOKEN_THRESHOLD_FOR_SESSION_MODE})`,
};
}
// Complex agent types benefit from session mode
if (agentType === 'implement' || agentType === 'review') {
return {
mode: 'session',
rationale: `Complex agent type (${agentType}) benefits from dedicated context`,
};
}
// Multiple output files need session mode to avoid conflicts
if (outputFiles && outputFiles.length > 2) {
return {
mode: 'session',
rationale: `Multiple output files (${outputFiles.length}) - avoid conflicts`,
};
}
// High complexity tasks need session mode
if (complexity === 'high') {
return {
mode: 'session',
rationale: 'High complexity task needs dedicated context',
};
}
// Low token tasks can use task-tool mode
if (estimatedTokens && estimatedTokens < TOKEN_THRESHOLD_HAIKU) {
return {
mode: 'task-tool',
rationale: `Low token estimate (${estimatedTokens} < ${TOKEN_THRESHOLD_HAIKU})`,
};
}
// Explore tasks typically work well with task-tool
if (agentType === 'explore') {
return {
mode: 'task-tool',
rationale: 'Explore tasks efficient with shared context',
};
}
// Read-only tasks (input files only) can use task-tool
if (inputFiles && inputFiles.length > 0 && (!outputFiles || outputFiles.length === 0)) {
return {
mode: 'task-tool',
rationale: 'Read-only task (no output files)',
};
}
// Default to session mode for safety
return {
mode: 'session',
rationale: 'Default to session mode for reliability',
};
}
/**
* Get optimizer's recommendation for an agent type (for UI display).
*/
getOptimizerRecommendation(agentType: AgentType): ModelTier {
return DEFAULT_RECOMMENDATIONS[agentType];
}
/**
* Get model cost multiplier (relative to sonnet).
* Used for cost estimation in UI.
*/
getModelCostMultiplier(model: ModelTier): number {
switch (model) {
case 'opus': return 5.0; // ~5x more expensive
case 'sonnet': return 1.0; // baseline
case 'haiku': return 0.04; // ~25x cheaper
default: {
// Exhaustive check - if new ModelTier added, this will catch it
const _exhaustive: never = model;
console.warn(`[ModelSelector] Unknown model tier: ${_exhaustive}, using sonnet multiplier`);
return 1.0;
}
}
}
}
// ========== Singleton ==========
let selectorInstance: ModelSelector | null = null;
/**
* Get or create the singleton ModelSelector instance.
*/
export function getModelSelector(config?: Partial<ModelConfig>): ModelSelector {
if (!selectorInstance) {
selectorInstance = new ModelSelector(config);
}
return selectorInstance;
}
/**
* Reset the singleton (for testing).
*/
export function resetModelSelector(): void {
selectorInstance = null;
}
+243 -2172
View File
File diff suppressed because it is too large Load Diff
-32
View File
@@ -1,32 +0,0 @@
/**
* Architecture Planner Prompt
*
* Designs software component architecture for the task.
*
* Placeholders: {TASK}, {RESEARCH_CONTEXT}
*/
export const ARCHITECTURE_PLANNER_PROMPT = `You are an Architecture Planner specializing in software component design.
## YOUR TASK
Design the architecture for implementing this task:
## TASK DESCRIPTION
{TASK}
{RESEARCH_CONTEXT}
## INSTRUCTIONS
1. Identify all modules/components needed
2. Define interfaces between components
3. Specify data structures and types
4. Note configuration and setup requirements
5. Consider separation of concerns
## OUTPUT FORMAT
Return ONLY a JSON array:
[
{"category": "module|interface|type|config|infrastructure", "content": "component description", "rationale": "why needed"}
]
Generate 10-20 items. Think about the complete system architecture.`;
-134
View File
@@ -1,134 +0,0 @@
/**
* Execution Optimizer Prompt
*
* Optimizes the plan for Claude Code execution with parallel groups,
* agent types, model recommendations, and token estimates.
*
* Placeholders: {TASK}, {PLAN}
*/
export const EXECUTION_OPTIMIZER_PROMPT = `You are a Claude Code Execution Optimizer. Your job is to analyze an implementation plan and optimize it for efficient execution using Claude Code's agent system.
## ORIGINAL TASK
{TASK}
## CURRENT PLAN
{PLAN}
## YOUR MISSION
Analyze and enhance this plan for optimal Claude Code execution:
### 1. PARALLEL EXECUTION GROUPS
Identify tasks that can run simultaneously in separate agents:
- Tasks with NO dependencies between them
- Tasks that modify DIFFERENT files
- Tasks that read-only operations (exploration, analysis)
- Assign a parallelGroup ID (e.g., "parallel-1", "parallel-2") to related tasks
### 2. AGENT TYPE RECOMMENDATIONS
For each task, recommend the optimal Claude Code agent type:
- "explore": For codebase exploration, finding files, understanding patterns
- "implement": For writing new code, features, modifications
- "test": For writing and running tests
- "review": For code review, security analysis, best practices
- "general": For mixed or unclear tasks
### 3. FRESH CONTEXT RECOMMENDATIONS
Mark tasks that benefit from a fresh context (new conversation):
- After large file modifications (>500 lines changed)
- When switching between unrelated features
- After test failures that need fresh analysis
- When accumulated context might cause confusion
### 4. MODEL RECOMMENDATIONS
Suggest the optimal model for each task:
- "opus": Complex architecture, critical decisions, security review
- "sonnet": Standard implementation, most coding tasks
- "haiku": Quick exploration, simple searches, routine checks
### 5. FILE SCOPE ANALYSIS
For each task, identify:
- inputFiles: Files the task will need to READ
- outputFiles: Files the task will CREATE or MODIFY
### 6. TOKEN ESTIMATION
Estimate token usage for each task:
- Small (exploration, simple changes): 5000-15000
- Medium (feature implementation): 15000-50000
- Large (complex features, refactoring): 50000-100000
## OUTPUT FORMAT
Return ONLY a JSON object:
{
"optimizedPlan": [
{
"id": "P0-001",
"content": "Explore existing auth patterns in codebase",
"priority": "P0",
"tddPhase": "setup",
"verificationCriteria": "Documented auth patterns with file locations",
"dependencies": [],
"parallelGroup": "parallel-1",
"agentType": "explore",
"recommendedModel": "haiku",
"requiresFreshContext": false,
"estimatedTokens": 8000,
"inputFiles": ["src/auth/**/*.ts", "src/middleware/*.ts"],
"outputFiles": [],
"executionNotes": "Quick exploration, can run alongside P0-002"
},
{
"id": "P0-002",
"content": "Explore test patterns and fixtures",
"priority": "P0",
"tddPhase": "setup",
"verificationCriteria": "Understood test setup and conventions",
"dependencies": [],
"parallelGroup": "parallel-1",
"agentType": "explore",
"recommendedModel": "haiku",
"requiresFreshContext": false,
"estimatedTokens": 6000,
"inputFiles": ["test/**/*.test.ts", "test/fixtures/**/*"],
"outputFiles": [],
"executionNotes": "Parallel with P0-001, different file scope"
}
],
"parallelGroups": [
{
"id": "parallel-1",
"tasks": ["P0-001", "P0-002"],
"rationale": "Independent exploration tasks with no file overlap",
"estimatedDuration": "2-3 minutes",
"totalTokens": 14000
}
],
"executionStrategy": {
"totalParallelGroups": 3,
"sequentialBlockers": ["P0-005 blocks all P1 tasks"],
"freshContextPoints": ["After P0-005 (large refactor)", "After P1-003 (test failures)"],
"estimatedTotalTokens": 150000,
"estimatedAgentSpawns": 8,
"criticalPath": ["P0-001", "P0-003", "P0-005", "P1-001"],
"optimizationNotes": [
"Group 1 saves ~3 min by parallelizing exploration",
"Use haiku for 4 exploration tasks to reduce cost",
"Fresh context after auth refactor prevents confusion"
]
}
}
CRITICAL REQUIREMENTS:
1. Every task MUST have parallelGroup, agentType, recommendedModel
2. Parallel groups MUST NOT have overlapping outputFiles
3. Tasks in same parallelGroup MUST NOT depend on each other
4. Preserve all existing task fields (id, content, priority, etc.)
5. Add executionNotes explaining WHY this optimization
PARALLELIZATION GUIDELINES (BE CONSERVATIVE):
- Only parallelize tasks when you are CERTAIN they have no file conflicts
- Prefer sequential execution for complex or risky tasks
- Limit parallel groups to 2-3 tasks maximum per group
- When in doubt, keep tasks sequential - correctness over speed
- Focus parallelization on exploration/read-only tasks, not implementations
- Never parallelize tasks that might share state or side effects`;
-100
View File
@@ -1,100 +0,0 @@
/**
* Final Review Expert Prompt
*
* Provides holistic analysis of the complete implementation plan
* with scoring and improvement suggestions.
*
* Placeholders: {TASK}, {PLAN}
*/
export const FINAL_REVIEW_PROMPT = `You are a Final Review Expert providing a holistic analysis of an implementation plan.
## ORIGINAL TASK
{TASK}
## COMPLETE PLAN
{PLAN}
## YOUR MISSION
Review the ENTIRE plan from a high-level perspective. You have the bird's eye view.
### 1. LOGICAL FLOW ANALYSIS
Check if the plan makes logical sense:
- Does the order of tasks make sense?
- Are there circular dependencies or impossible orderings?
- Is there a clear progression from setup → implementation → testing → review?
- Are foundation tasks (types, configs, setup) done before dependent tasks?
### 2. COMPLETENESS CHECK
Verify nothing is missing:
- Every implementation has a corresponding test?
- Every test has clear verification criteria?
- Error handling and edge cases are covered?
- Setup and teardown steps are included?
- Documentation tasks if needed?
### 3. COHERENCE VALIDATION
Ensure the plan is internally consistent:
- Do task descriptions match their dependencies?
- Are file references consistent across tasks?
- Do parallel groups actually make sense together?
- Are priority levels justified?
### 4. FEASIBILITY ASSESSMENT
Is this plan actually achievable?
- Are any tasks too vague to execute?
- Are there unrealistic expectations?
- Are there hidden complexities not addressed?
- Is the scope creep under control?
### 5. SUGGESTED IMPROVEMENTS
Provide actionable fixes:
- Tasks to add if missing
- Tasks to split if too large
- Tasks to merge if redundant
- Order changes if needed
- Clarifications needed
## OUTPUT FORMAT
Return ONLY a JSON object:
{
"overallAssessment": "ready|needs-revision|major-issues",
"logicScore": 0.85,
"completenessScore": 0.90,
"coherenceScore": 0.88,
"feasibilityScore": 0.82,
"overallScore": 0.86,
"summary": "Brief 2-3 sentence summary of the plan quality",
"logicIssues": [
{"severity": "warning|error", "issue": "Description", "affectedTasks": ["P0-001"], "suggestion": "How to fix"}
],
"missingTasks": [
{"content": "Add database migration script", "reason": "Schema changes require migration", "insertAfter": "P0-002", "priority": "P0"}
],
"tasksToSplit": [
{"taskId": "P1-005", "reason": "Too complex", "splitInto": ["Implement auth logic", "Add session management"]}
],
"tasksToMerge": [
{"taskIds": ["P2-001", "P2-002"], "reason": "Redundant", "mergedContent": "Combined task description"}
],
"orderChanges": [
{"taskId": "P0-003", "currentPosition": 3, "suggestedPosition": 1, "reason": "Should run earlier"}
],
"clarificationsNeeded": [
{"taskId": "P1-002", "issue": "Unclear which API endpoint", "question": "Is this REST or GraphQL?"}
],
"finalRecommendations": [
"Start with P0 tasks in sequence for stable foundation",
"Consider adding integration tests after P1-004",
"Review security implications of auth changes"
]
}
SCORING GUIDELINES:
- 0.9+: Excellent, ready to execute
- 0.8-0.9: Good, minor tweaks recommended
- 0.7-0.8: Acceptable, some issues to address
- 0.6-0.7: Needs revision before execution
- <0.6: Major issues, significant rework needed
Be thorough but constructive. The goal is to catch issues before execution, not to criticize.`;
+1 -7
View File
@@ -6,11 +6,5 @@
*/
export { RESEARCH_AGENT_PROMPT } from './research-agent.js';
export { REQUIREMENTS_ANALYST_PROMPT } from './requirements-analyst.js';
export { ARCHITECTURE_PLANNER_PROMPT } from './architecture-planner.js';
export { TESTING_SPECIALIST_PROMPT } from './testing-specialist.js';
export { RISK_ANALYST_PROMPT } from './risk-analyst.js';
export { PLANNER_PROMPT } from './planner.js';
export { CODE_REVIEWER_PROMPT } from './code-reviewer.js';
export { VERIFICATION_PROMPT } from './verification.js';
export { EXECUTION_OPTIMIZER_PROMPT } from './execution-optimizer.js';
export { FINAL_REVIEW_PROMPT } from './final-review.js';
+83
View File
@@ -0,0 +1,83 @@
/**
* Planner Prompt - Single agent for TDD plan generation
*
* Combines what was previously 5 separate agents:
* - Requirements Analyst (redundant)
* - Architecture Planner (redundant)
* - Testing Specialist (kept - TDD focus)
* - Risk Analyst (redundant)
* - Verification Expert (kept - structure)
*
* Placeholders: {TASK}, {RESEARCH_CONTEXT}
*/
export const PLANNER_PROMPT = `You are a TDD Plan Generator. Create a complete implementation plan with test-first approach.
## TASK DESCRIPTION
{TASK}
{RESEARCH_CONTEXT}
## YOUR MISSION
Generate a complete TDD implementation plan with:
1. Tests BEFORE implementations (red-green-refactor)
2. Review tasks AFTER implementations
3. Clear priorities (P0=blocking, P1=required, P2=polish)
4. Dependencies between tasks
## TDD CYCLE
For each feature:
1. Write failing test first
2. Implement to make test pass
3. Review implementation
## PRIORITY GUIDELINES
- P0: Foundation, types, project setup, blocking dependencies
- P1: Core features, main implementation, error handling
- P2: Polish, optimization, documentation
## OUTPUT FORMAT
Return ONLY a JSON object:
{
"items": [
{
"id": "P0-001",
"content": "Write failing test for user authentication endpoint",
"priority": "P0",
"tddPhase": "test",
"verificationCriteria": "Test file exists, test fails with 'not implemented'",
"testCommand": "npm test -- --grep='auth'",
"dependencies": []
},
{
"id": "P0-002",
"content": "Implement user authentication handler",
"priority": "P0",
"tddPhase": "impl",
"verificationCriteria": "npm test -- --grep='auth' passes",
"pairedWith": "P0-001",
"dependencies": ["P0-001"]
},
{
"id": "P0-003",
"content": "Review auth implementation for security",
"priority": "P0",
"tddPhase": "review",
"verificationCriteria": "No security issues, follows best practices",
"reviewChecklist": ["Input validation", "XSS prevention", "Error handling"],
"pairedWith": "P0-002",
"dependencies": ["P0-002"]
}
],
"gaps": ["any missing requirements noted"],
"warnings": ["any concerns or risks identified"]
}
CRITICAL REQUIREMENTS:
1. Every implementation MUST have a paired test task that comes BEFORE it
2. Every implementation MUST have a review task that comes AFTER it
3. Use sequential IDs: P0-001, P0-002, P1-001, etc.
4. verificationCriteria must be SPECIFIC and observable
5. Dependencies must form a valid DAG (no cycles)
Generate 15-40 items covering the complete implementation.`;
-31
View File
@@ -1,31 +0,0 @@
/**
* Requirements Analyst Prompt
*
* Extracts explicit and implicit requirements from task descriptions.
*
* Placeholders: {TASK}, {RESEARCH_CONTEXT}
*/
export const REQUIREMENTS_ANALYST_PROMPT = `You are a Requirements Analyst specializing in extracting all requirements from task descriptions.
## YOUR TASK
Analyze the following task and extract ALL requirements (explicit and implicit):
## TASK DESCRIPTION
{TASK}
{RESEARCH_CONTEXT}
## INSTRUCTIONS
1. Identify explicit requirements (directly stated)
2. Infer implicit requirements (unstated but necessary)
3. Note any assumptions that should be validated
4. Consider non-functional requirements (performance, security, usability)
## OUTPUT FORMAT
Return ONLY a JSON array:
[
{"category": "functional|non-functional|constraint|assumption", "content": "requirement description", "rationale": "why this is needed"}
]
Generate 8-15 items. Be thorough - missing requirements cause project failures.`;
-32
View File
@@ -1,32 +0,0 @@
/**
* Risk Analyst Prompt
*
* Identifies potential issues, edge cases, and blockers.
*
* Placeholders: {TASK}, {RESEARCH_CONTEXT}
*/
export const RISK_ANALYST_PROMPT = `You are a Risk Analyst identifying potential issues and blockers.
## YOUR TASK
Identify risks and edge cases for this task:
## TASK DESCRIPTION
{TASK}
{RESEARCH_CONTEXT}
## INSTRUCTIONS
1. Identify potential failure points
2. Note edge cases that could cause bugs
3. Consider security vulnerabilities
4. Flag performance concerns
5. Identify dependencies that could block progress
## OUTPUT FORMAT
Return ONLY a JSON array:
[
{"category": "failure|edge-case|security|performance|dependency", "content": "risk description", "rationale": "mitigation approach"}
]
Generate 8-15 items. Being proactive about risks prevents surprises.`;
-76
View File
@@ -1,76 +0,0 @@
/**
* Testing Specialist Prompt
*
* Designs comprehensive, realistic test coverage with TDD approach.
*
* Placeholders: {TASK}, {RESEARCH_CONTEXT}
*/
export const TESTING_SPECIALIST_PROMPT = `You are a TDD Specialist designing a comprehensive, REALISTIC test strategy.
## YOUR TASK
Design detailed, executable test coverage for this task:
## TASK DESCRIPTION
{TASK}
{RESEARCH_CONTEXT}
## INSTRUCTIONS
Create REALISTIC tests that would actually run in a real codebase:
### 1. Unit Tests (test individual functions/methods in isolation)
- Mock external dependencies (databases, APIs, file system)
- Test pure logic with specific input/output examples
- Include exact assertion values, not placeholders
### 2. Integration Tests (test component interactions)
- Test API endpoints with realistic request/response bodies
- Test database operations with actual schema
- Test service-to-service communication
### 3. Edge Cases & Boundary Tests
- Empty inputs, null values, undefined
- Maximum/minimum values, overflow conditions
- Unicode, special characters, injection attempts
- Concurrent access, race conditions
### 4. Error Scenario Tests
- Network failures, timeouts, connection refused
- Invalid input validation with specific error messages
- Authorization failures, permission denied
- Resource not found, conflict states
### 5. Performance & Load Tests (where applicable)
- Response time thresholds
- Memory usage limits
- Concurrent user handling
## REALISTIC TEST EXAMPLE
BAD: "Test user login" (too vague)
GOOD: "Test POST /api/auth/login with valid email 'test@example.com' and password 'ValidPass123!' returns 200 with JWT token containing userId and exp claims, sets httpOnly cookie 'session'"
## OUTPUT FORMAT
Return ONLY a JSON array:
[
{
"category": "unit|integration|edge-case|error|e2e|performance",
"content": "Test POST /api/users with email 'new@test.com' creates user and returns 201 with {id, email, createdAt}",
"rationale": "Validates user creation happy path with all required response fields",
"verificationCriteria": "Response status 201, body contains id (uuid), email matches input, createdAt is valid ISO timestamp",
"testCommand": "npm test -- --grep='POST /api/users creates user'",
"testSetup": "Clear users table, seed with test data",
"testTeardown": "Delete created test user",
"pairedImpl": "Implement POST /api/users endpoint with validation and database insert",
"mockDependencies": ["database connection", "email service"],
"assertionDetails": ["status === 201", "body.id matches UUID regex", "body.email === 'new@test.com'"]
}
]
CRITICAL REQUIREMENTS:
- verificationCriteria: SPECIFIC observable outcomes with exact values
- testCommand: Actual runnable command (npm test, pytest, vitest, etc.)
- pairedImpl: The exact implementation step this test validates
- assertionDetails: List of specific assertions to make
Generate 15-30 detailed test items. Tests MUST be specific enough to implement directly.`;
-96
View File
@@ -1,96 +0,0 @@
/**
* Verification Expert Prompt
*
* Reviews and enhances the synthesized plan with priorities,
* verification criteria, and TDD pairing.
*
* Placeholders: {TASK}, {PLAN}
*/
export const VERIFICATION_PROMPT = `You are a Plan Verification Expert reviewing an implementation plan for completeness and quality.
## ORIGINAL TASK
{TASK}
## SYNTHESIZED PLAN (from multiple analysis subagents)
{PLAN}
## YOUR MISSION
Review and enhance this plan:
1. Assign priorities (P0=critical/blocking, P1=required, P2=enhancement)
2. Add verification criteria to EVERY task (how to know it's done)
3. Pair test tasks with implementation tasks (TDD cycle)
4. Add dependencies where one task blocks another
5. Identify gaps and calculate quality score
## PRIORITY GUIDELINES
- P0: Foundation tasks, type definitions, project setup, blocking dependencies
- P1: Core implementation, tests, main features, error handling
- P2: Polish, optimization, documentation, nice-to-have features
## TDD + REVIEW CYCLE RULES
The complete cycle is: test → impl → review
- Every implementation task should have a corresponding test task AND review task
- Test task comes BEFORE its paired implementation task
- Review task comes AFTER the implementation it reviews
- Use "pairedWith" to link test ↔ implementation ↔ review
- Verification criteria should reference test results where applicable
## REVIEW TASK REQUIREMENTS
After EVERY implementation task, add a review task that checks:
- Best practices for the language/framework
- Security vulnerabilities (OWASP top 10)
- Performance concerns
- Error handling completeness
- Code quality (DRY, SOLID, readability)
## OUTPUT FORMAT
Return ONLY a JSON object:
{
"validatedPlan": [
{
"id": "P0-001",
"content": "Write failing test for user authentication",
"priority": "P0",
"tddPhase": "test",
"verificationCriteria": "Test file exists, test fails with 'not implemented'",
"testCommand": "npm test -- --grep='auth'",
"pairedWith": "P0-002",
"dependencies": [],
"complexity": "low"
},
{
"id": "P0-002",
"content": "Implement user authentication handler",
"priority": "P0",
"tddPhase": "impl",
"verificationCriteria": "npm test -- --grep='auth' passes",
"pairedWith": "P0-001",
"dependencies": ["P0-001"],
"complexity": "medium"
},
{
"id": "P0-003",
"content": "Review auth implementation for security and best practices",
"priority": "P0",
"tddPhase": "review",
"verificationCriteria": "No security issues found, follows TypeScript best practices",
"reviewChecklist": ["Input validation", "XSS prevention", "Session security", "Error handling"],
"pairedWith": "P0-002",
"dependencies": ["P0-002"],
"complexity": "low"
}
],
"gaps": ["missing requirement 1", "missing test coverage for X"],
"warnings": ["consider Y before Z", "potential issue with..."],
"qualityScore": 0.85
}
CRITICAL REQUIREMENTS:
1. EVERY task MUST have verificationCriteria (how to verify completion)
2. Implementation tasks MUST have a paired test task AND a review task
3. Review tasks MUST have a reviewChecklist with specific items to check
4. Dependencies must form a valid DAG (no cycles)
5. Use sequential IDs: P0-001, P0-002, P0-003, P1-001, etc.
Be critical but constructive. A thorough review catches issues that tests miss.`;
+2 -33
View File
@@ -1255,37 +1255,6 @@ export interface ImageDetectedEvent {
size: number;
}
// ========== Execution Bridge Re-exports ==========
// ========== Plan Orchestrator Re-exports ==========
export type {
ExecutionStatus,
ExecutionProgress,
TaskAssignment as ExecutionTaskAssignment,
PlanItem,
ExecutionHistoryEntry,
} from './execution-bridge.js';
export type {
ModelTier,
AgentType,
ExecutionMode,
ModelConfig,
ModelSelection,
ExecutionModeSelection,
TaskCharacteristics,
} from './model-selector.js';
export type {
GroupTaskStatus,
ExecutionGroupStatus,
GroupTask,
ExecutionGroup,
ExecutionSchedule,
} from './group-scheduler.js';
export type {
ContextRefreshMethod,
ContextRefreshStatus,
ContextRefreshRequest,
ContextRefreshResult,
} from './context-manager.js';
export type { PlanItem } from './plan-orchestrator.js';
-342
View File
@@ -34,13 +34,6 @@ import { v4 as uuidv4 } from 'uuid';
import { createRequire } from 'node:module';
import { RunSummaryTracker } from '../run-summary.js';
import { PlanOrchestrator, type DetailedPlanResult } from '../plan-orchestrator.js';
import {
ExecutionBridge,
getExecutionBridge,
type PlanItem as ExecutionPlanItem,
type AgentSpawner,
} from '../execution-bridge.js';
import { type ModelConfig } from '../model-selector.js';
// Load version from package.json
const require = createRequire(import.meta.url);
@@ -314,8 +307,6 @@ export class WebServer extends EventEmitter {
private pendingRespawnStarts: Map<string, NodeJS.Timeout> = new Map();
// Active plan orchestrators (for cancellation via API)
private activePlanOrchestrators: Map<string, PlanOrchestrator> = new Map();
// Execution bridge for parallel task execution
private executionBridge: ExecutionBridge;
// Grace period before starting restored respawn controllers (2 minutes)
private static readonly RESPAWN_RESTORE_GRACE_PERIOD_MS = 2 * 60 * 1000;
@@ -346,10 +337,6 @@ export class WebServer extends EventEmitter {
this.broadcast('screen:statsUpdated', screens);
});
// Initialize execution bridge with model config from settings
this.executionBridge = getExecutionBridge(this.loadModelConfig());
this.setupExecutionBridgeListeners();
// Set up subagent watcher listeners
this.setupSubagentWatcherListeners();
@@ -3045,148 +3032,6 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return this.getSystemStats();
});
// ========== Execution Bridge Endpoints ==========
// Get execution status
this.app.get('/api/execution/status', async () => {
return {
success: true,
data: {
status: this.executionBridge.status,
progress: this.executionBridge.getProgress(),
},
};
});
// Get execution schedule (the current plan)
this.app.get('/api/execution/schedule', async () => {
const schedule = this.executionBridge['_scheduler'].schedule;
return { success: true, data: schedule };
});
// Load a plan for execution
// Accepts either ExecutionPlanItem format (id, title, description) or
// PlanOrchestrator PlanItem format (id, content) and converts as needed
this.app.post('/api/execution/load', async (req) => {
const { items, workingDir } = req.body as {
items: Array<{
id?: string;
title?: string;
description?: string;
content?: string;
parallelGroup?: number;
agentType?: string;
recommendedModel?: string;
requiresFreshContext?: boolean;
estimatedTokens?: number;
inputFiles?: string[];
outputFiles?: string[];
dependencies?: string[];
}>;
workingDir?: string;
};
if (!items || !Array.isArray(items)) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'items array is required');
}
try {
if (workingDir) {
this.executionBridge.setWorkingDir(workingDir);
}
// Convert to ExecutionPlanItem format
const execItems: ExecutionPlanItem[] = items.map((item, idx) => ({
id: item.id || `task-${idx}`,
title: item.title || item.content?.slice(0, 50) || `Task ${idx + 1}`,
description: item.description || item.content || '',
parallelGroup: item.parallelGroup,
agentType: item.agentType,
recommendedModel: item.recommendedModel,
requiresFreshContext: item.requiresFreshContext,
estimatedTokens: item.estimatedTokens,
inputFiles: item.inputFiles,
outputFiles: item.outputFiles,
dependencies: item.dependencies,
}));
const schedule = this.executionBridge.loadPlan(execItems);
return { success: true, data: schedule };
} catch (err) {
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
}
});
// Start execution
this.app.post('/api/execution/start', async () => {
try {
await this.executionBridge.start();
return { success: true };
} catch (err) {
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
}
});
// Pause execution
this.app.post('/api/execution/pause', async () => {
this.executionBridge.pause();
return { success: true };
});
// Resume execution
this.app.post('/api/execution/resume', async () => {
this.executionBridge.resume();
return { success: true };
});
// Cancel execution
this.app.post('/api/execution/cancel', async (req) => {
const { reason } = (req.body as { reason?: string }) || {};
await this.executionBridge.cancel(reason || 'Cancelled via API');
return { success: true };
});
// Reset execution bridge
this.app.post('/api/execution/reset', async () => {
this.executionBridge.reset();
return { success: true };
});
// Get execution history
this.app.get('/api/execution/history', async () => {
return { success: true, data: this.executionBridge.getHistory() };
});
// Get model configuration
this.app.get('/api/execution/model-config', async () => {
return { success: true, data: this.executionBridge.getModelConfig() };
});
// Update model configuration
this.app.put('/api/execution/model-config', async (req) => {
const config = req.body as Partial<ModelConfig>;
try {
this.executionBridge.updateModelConfig(config);
const fullConfig = this.executionBridge.getModelConfig();
this.saveModelConfig(fullConfig);
this.broadcast('execution:modelConfigUpdated', fullConfig);
return { success: true, data: fullConfig };
} catch (err) {
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
}
});
// Mark task complete (external signal)
this.app.post('/api/execution/tasks/:taskId/complete', async (req) => {
const { taskId } = req.params as { taskId: string };
this.executionBridge.markTaskComplete(taskId);
return { success: true };
});
// Mark task failed (external signal)
this.app.post('/api/execution/tasks/:taskId/failed', async (req) => {
const { taskId } = req.params as { taskId: string };
const { error } = (req.body as { error?: string }) || {};
this.executionBridge.markTaskFailed(taskId, error || 'Failed via API');
return { success: true };
});
// ========== Subagent Monitoring (Claude Code Background Agents) ==========
// List all known subagents
@@ -3910,193 +3755,6 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
});
}
/**
* Load model configuration from settings file.
*/
private loadModelConfig(): Partial<ModelConfig> {
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
try {
if (existsSync(settingsPath)) {
const content = readFileSync(settingsPath, 'utf-8');
const settings = JSON.parse(content);
return settings.modelConfig || {};
}
} catch (err) {
console.error('Failed to load model config:', getErrorMessage(err));
}
return {};
}
/**
* Save model configuration to settings file.
*/
private saveModelConfig(config: ModelConfig): void {
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
try {
const dir = dirname(settingsPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
let settings: Record<string, unknown> = {};
if (existsSync(settingsPath)) {
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
}
settings.modelConfig = config;
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
} catch (err) {
console.error('Failed to save model config:', getErrorMessage(err));
}
}
/**
* Set up event listeners for execution bridge.
* Broadcasts execution progress to SSE clients.
*/
private setupExecutionBridgeListeners(): void {
// Create agent spawner interface
const spawner: AgentSpawner = {
spawnAgentWithModel: async (taskId, workingDir, prompt, _model, _options) => {
// Note: model and options are reserved for future model selection integration
const globalNice = this.getGlobalNiceConfig();
const session = new Session({
workingDir,
screenManager: this.screenManager,
useScreen: true,
mode: 'claude',
name: `exec:${taskId}`,
niceConfig: globalNice,
});
this.sessions.set(session.id, session);
this.store.incrementSessionsCreated();
this.setupSessionListeners(session);
await session.startInteractive();
this.broadcast('session:created', session.toDetailedState());
this.broadcast('session:interactive', { id: session.id });
this.persistSessionState(session);
// Configure ralph tracker for completion detection
session.ralphTracker.enable();
// Set up completion listener for execution bridge
session.on('ralphCompletionDetected', () => {
this.executionBridge.markTaskComplete(taskId);
});
// Send the prompt
setTimeout(() => {
session.writeViaScreen(prompt + '\r');
}, 2000);
return { sessionId: session.id };
},
useTaskTool: async (_sessionId, _taskId, _prompt, _model) => {
// Task tool mode - not yet implemented, falls back to session mode
throw new Error('Task tool mode not yet implemented');
},
isTaskComplete: (taskId) => {
// Check if task is marked complete in scheduler
const schedule = this.executionBridge['_scheduler'].schedule;
if (!schedule) return false;
for (const group of schedule.groups) {
const task = group.tasks.find(t => t.id === taskId);
if (task) return task.status === 'completed';
}
return false;
},
getTaskResult: (taskId) => {
const schedule = this.executionBridge['_scheduler'].schedule;
if (!schedule) return null;
for (const group of schedule.groups) {
const task = group.tasks.find(t => t.id === taskId);
if (task) {
if (task.status === 'completed') {
return { success: true };
} else if (task.status === 'failed') {
return { success: false, error: task.error };
}
}
}
return null;
},
};
this.executionBridge.setAgentSpawner(spawner);
// Set up session writer for context management
this.executionBridge.setSessionWriter({
writeToSession: (sessionId, text) => {
const session = this.sessions.get(sessionId);
if (session) {
session.writeViaScreen(text);
}
},
createSession: async (workingDir, name) => {
const globalNice = this.getGlobalNiceConfig();
const session = new Session({
workingDir,
screenManager: this.screenManager,
useScreen: true,
mode: 'claude',
name: name || 'exec-context',
niceConfig: globalNice,
});
this.sessions.set(session.id, session);
this.store.incrementSessionsCreated();
this.setupSessionListeners(session);
await session.startInteractive();
this.broadcast('session:created', session.toDetailedState());
this.persistSessionState(session);
return { sessionId: session.id };
},
});
// Forward execution bridge events as SSE broadcasts
this.executionBridge.on('planLoaded', (schedule) => {
this.broadcast('execution:planLoaded', { schedule });
});
this.executionBridge.on('started', () => {
this.broadcast('execution:started', {});
});
this.executionBridge.on('paused', () => {
this.broadcast('execution:paused', {});
});
this.executionBridge.on('resumed', () => {
this.broadcast('execution:resumed', {});
});
this.executionBridge.on('completed', (data) => {
this.broadcast('execution:completed', data);
});
this.executionBridge.on('cancelled', (reason) => {
this.broadcast('execution:cancelled', { reason });
});
this.executionBridge.on('groupStarted', (data) => {
this.broadcast('execution:groupStarted', data);
});
this.executionBridge.on('groupCompleted', (data) => {
this.broadcast('execution:groupCompleted', data);
});
this.executionBridge.on('taskAssigned', (data) => {
this.broadcast('execution:taskAssigned', data);
});
this.executionBridge.on('taskCompleted', (data) => {
this.broadcast('execution:taskCompleted', data);
});
this.executionBridge.on('taskFailed', (data) => {
this.broadcast('execution:taskFailed', data);
});
this.executionBridge.on('freshContext', (data) => {
this.broadcast('execution:freshContext', data);
});
this.executionBridge.on('modelSelected', (data) => {
this.broadcast('execution:modelSelected', data);
});
this.executionBridge.on('progress', (progress) => {
this.broadcast('execution:progress', progress);
});
}
private setupTimedRespawn(sessionId: string, durationMinutes: number): void {
// Clear existing timer if any
const existing = this.respawnTimers.get(sessionId);
-270
View File
@@ -1,270 +0,0 @@
/**
* @fileoverview Tests for the Execution Bridge system.
*
* Tests the core functionality of:
* - ModelSelector
* - GroupScheduler
* - ExecutionBridge
*
* Uses mocks to avoid spawning real Claude sessions.
*
* Port: none (unit tests, no server)
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
ModelSelector,
resetModelSelector,
createDefaultModelConfig,
type ModelConfig,
type AgentType,
} from '../src/model-selector.js';
import {
GroupScheduler,
resetGroupScheduler,
type GroupTask,
} from '../src/group-scheduler.js';
import {
ExecutionBridge,
resetExecutionBridge,
type PlanItem,
} from '../src/execution-bridge.js';
describe('ModelSelector', () => {
let selector: ModelSelector;
beforeEach(() => {
resetModelSelector();
selector = new ModelSelector();
});
afterEach(() => {
resetModelSelector();
});
it('should use default model when no overrides', () => {
const selection = selector.selectModel('task-1', { agentType: 'explore' });
expect(selection.model).toBe('sonnet');
expect(selection.usedUserDefault).toBe(true);
});
it('should respect user default model', () => {
selector.updateConfig({ defaultModel: 'opus' });
const selection = selector.selectModel('task-1', { agentType: 'explore' });
expect(selection.model).toBe('opus');
expect(selection.usedUserDefault).toBe(true);
});
it('should use agent type override when set', () => {
selector.updateConfig({
agentTypeOverrides: { explore: 'haiku' },
});
const selection = selector.selectModel('task-1', { agentType: 'explore' });
expect(selection.model).toBe('haiku');
expect(selection.usedUserDefault).toBe(false);
});
it('should include optimizer recommendation when different', () => {
selector.updateConfig({ defaultModel: 'opus' });
const selection = selector.selectModel('task-1', {
agentType: 'explore',
recommendedModel: 'haiku',
});
expect(selection.model).toBe('opus');
expect(selection.optimizerRecommendation).toBe('haiku');
});
it('should select session mode for high token tasks', () => {
const mode = selector.selectExecutionMode({ estimatedTokens: 60000 });
expect(mode.mode).toBe('session');
});
it('should select task-tool mode for low token explore tasks', () => {
const mode = selector.selectExecutionMode({
estimatedTokens: 10000,
agentType: 'explore',
});
expect(mode.mode).toBe('task-tool');
});
it('should return correct cost multipliers', () => {
expect(selector.getModelCostMultiplier('opus')).toBe(5.0);
expect(selector.getModelCostMultiplier('sonnet')).toBe(1.0);
expect(selector.getModelCostMultiplier('haiku')).toBe(0.04);
});
});
describe('GroupScheduler', () => {
let scheduler: GroupScheduler;
beforeEach(() => {
resetGroupScheduler();
scheduler = new GroupScheduler();
});
afterEach(() => {
resetGroupScheduler();
});
it('should build schedule from plan items', () => {
const items: PlanItem[] = [
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 0 },
{ id: 't2', title: 'Task 2', description: 'Desc 2', parallelGroup: 0 },
{ id: 't3', title: 'Task 3', description: 'Desc 3', parallelGroup: 1 },
];
const schedule = scheduler.buildSchedule(items);
expect(schedule.groups).toHaveLength(2);
expect(schedule.groups[0].tasks).toHaveLength(2);
expect(schedule.groups[1].tasks).toHaveLength(1);
expect(schedule.totalTasks).toBe(3);
});
it('should order groups by number', () => {
const items: PlanItem[] = [
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 2 },
{ id: 't2', title: 'Task 2', description: 'Desc 2', parallelGroup: 0 },
{ id: 't3', title: 'Task 3', description: 'Desc 3', parallelGroup: 1 },
];
const schedule = scheduler.buildSchedule(items);
expect(schedule.groups[0].groupNumber).toBe(0);
expect(schedule.groups[1].groupNumber).toBe(1);
expect(schedule.groups[2].groupNumber).toBe(2);
});
it('should track group dependencies', () => {
const items: PlanItem[] = [
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 0 },
{ id: 't2', title: 'Task 2', description: 'Desc 2', parallelGroup: 1, dependencies: ['t1'] },
];
const schedule = scheduler.buildSchedule(items);
expect(schedule.groups[1].dependsOnGroups).toContain(0);
});
it('should return first group as ready initially', () => {
const items: PlanItem[] = [
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 0 },
{ id: 't2', title: 'Task 2', description: 'Desc 2', parallelGroup: 1 },
];
scheduler.buildSchedule(items);
const nextGroup = scheduler.getNextReadyGroup();
expect(nextGroup).not.toBeNull();
expect(nextGroup!.groupNumber).toBe(0);
});
it('should update task status', () => {
const items: PlanItem[] = [
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 0 },
];
scheduler.buildSchedule(items);
scheduler.startGroup(0);
scheduler.updateTaskStatus('t1', 'completed');
const stats = scheduler.getStats();
expect(stats.completedTasks).toBe(1);
});
it('should determine execution mode based on task characteristics', () => {
const items: PlanItem[] = [
{
id: 't1',
title: 'High token task',
description: 'Desc',
parallelGroup: 0,
estimatedTokens: 60000,
},
];
const schedule = scheduler.buildSchedule(items);
expect(schedule.groups[0].executionMode).toBe('session');
});
});
describe('ExecutionBridge', () => {
let bridge: ExecutionBridge;
beforeEach(() => {
resetExecutionBridge();
resetGroupScheduler();
resetModelSelector();
bridge = new ExecutionBridge();
});
afterEach(() => {
bridge.reset();
resetExecutionBridge();
resetGroupScheduler();
resetModelSelector();
});
it('should have idle status initially', () => {
expect(bridge.status).toBe('idle');
});
it('should load a plan', () => {
const items: PlanItem[] = [
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 0 },
{ id: 't2', title: 'Task 2', description: 'Desc 2', parallelGroup: 1 },
];
const schedule = bridge.loadPlan(items);
expect(schedule).not.toBeNull();
expect(schedule.totalTasks).toBe(2);
expect(bridge.status).toBe('idle');
});
it('should report progress', () => {
const items: PlanItem[] = [
{ id: 't1', title: 'Task 1', description: 'Desc 1' },
];
bridge.loadPlan(items);
const progress = bridge.getProgress();
expect(progress.totalTasks).toBe(1);
expect(progress.completedTasks).toBe(0);
expect(progress.status).toBe('idle');
});
it('should update model config', () => {
bridge.updateModelConfig({ defaultModel: 'opus' });
const config = bridge.getModelConfig();
expect(config.defaultModel).toBe('opus');
});
it('should reset properly', () => {
const items: PlanItem[] = [
{ id: 't1', title: 'Task 1', description: 'Desc 1' },
];
bridge.loadPlan(items);
bridge.reset();
expect(bridge.status).toBe('idle');
expect(bridge.getProgress().totalTasks).toBe(0);
});
it('should throw when starting without a spawner', async () => {
const items: PlanItem[] = [
{ id: 't1', title: 'Task 1', description: 'Desc 1' },
];
bridge.loadPlan(items);
await expect(bridge.start()).rejects.toThrow('No agent spawner configured');
});
it('should track execution history', () => {
const history = bridge.getHistory();
expect(Array.isArray(history)).toBe(true);
});
});