diff --git a/auto-claude-ui/src/main/agent/agent-manager.ts b/auto-claude-ui/src/main/agent/agent-manager.ts index 1ed3455f..1166d183 100644 --- a/auto-claude-ui/src/main/agent/agent-manager.ts +++ b/auto-claude-ui/src/main/agent/agent-manager.ts @@ -128,6 +128,20 @@ export class AgentManager extends EventEmitter { args.push('--auto-approve'); } + // Pass model and thinking level configuration + // For auto profile, use phase-specific config; otherwise use single model/thinking + if (metadata?.isAutoProfile && metadata.phaseModels && metadata.phaseThinking) { + // Pass the spec phase model and thinking level to spec_runner + args.push('--model', metadata.phaseModels.spec); + args.push('--thinking-level', metadata.phaseThinking.spec); + } else if (metadata?.model) { + // Non-auto profile: use single model and thinking level + args.push('--model', metadata.model); + if (metadata.thinkingLevel) { + args.push('--thinking-level', metadata.thinkingLevel); + } + } + // Store context for potential restart this.storeTaskContext(taskId, projectPath, '', {}, true, taskDescription, specDir, metadata); @@ -183,6 +197,8 @@ export class AgentManager extends EventEmitter { // Note: --parallel was removed from run.py CLI - parallel execution is handled internally by the agent // The options.parallel and options.workers are kept for future use or logging purposes + // Note: Model configuration is read from task_metadata.json by the Python scripts, + // which allows per-phase configuration for planner, coder, and QA phases // Store context for potential restart this.storeTaskContext(taskId, projectPath, specId, options, false); diff --git a/auto-claude-ui/src/main/agent/types.ts b/auto-claude-ui/src/main/agent/types.ts index ddf680e8..141e4124 100644 --- a/auto-claude-ui/src/main/agent/types.ts +++ b/auto-claude-ui/src/main/agent/types.ts @@ -48,6 +48,23 @@ export interface TaskExecutionOptions { export interface SpecCreationMetadata { requireReviewBeforeCoding?: boolean; + // Auto profile - phase-based model and thinking configuration + isAutoProfile?: boolean; + phaseModels?: { + spec: 'haiku' | 'sonnet' | 'opus'; + planning: 'haiku' | 'sonnet' | 'opus'; + coding: 'haiku' | 'sonnet' | 'opus'; + qa: 'haiku' | 'sonnet' | 'opus'; + }; + phaseThinking?: { + spec: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; + planning: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; + coding: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; + qa: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; + }; + // Non-auto profile - single model and thinking level + model?: 'haiku' | 'sonnet' | 'opus'; + thinkingLevel?: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; } export interface IdeationProgressData { diff --git a/auto-claude-ui/src/renderer/components/AgentProfileSelector.tsx b/auto-claude-ui/src/renderer/components/AgentProfileSelector.tsx new file mode 100644 index 00000000..85f0f4c8 --- /dev/null +++ b/auto-claude-ui/src/renderer/components/AgentProfileSelector.tsx @@ -0,0 +1,370 @@ +/** + * AgentProfileSelector - Reusable component for selecting agent profile in forms + * + * Provides a dropdown for quick profile selection (Auto, Complex, Balanced, Quick) + * with an inline "Custom" option that reveals model and thinking level selects. + * The "Auto" profile shows per-phase model configuration. + * + * Used in TaskCreationWizard and TaskEditDialog. + */ +import { useState } from 'react'; +import { Brain, Scale, Zap, Sliders, Sparkles, ChevronDown, ChevronUp } from 'lucide-react'; +import { Label } from './ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from './ui/select'; +import { + DEFAULT_AGENT_PROFILES, + AVAILABLE_MODELS, + THINKING_LEVELS, + DEFAULT_PHASE_MODELS, + DEFAULT_PHASE_THINKING +} from '../../shared/constants'; +import type { ModelType, ThinkingLevel } from '../../shared/types'; +import type { PhaseModelConfig, PhaseThinkingConfig } from '../../shared/types/settings'; +import { cn } from '../lib/utils'; + +interface AgentProfileSelectorProps { + /** Currently selected profile ID ('auto', 'complex', 'balanced', 'quick', or 'custom') */ + profileId: string; + /** Current model value (fallback for non-auto profiles) */ + model: ModelType | ''; + /** Current thinking level value (fallback for non-auto profiles) */ + thinkingLevel: ThinkingLevel | ''; + /** Phase model configuration (for auto profile) */ + phaseModels?: PhaseModelConfig; + /** Phase thinking configuration (for auto profile) */ + phaseThinking?: PhaseThinkingConfig; + /** Called when profile selection changes */ + onProfileChange: (profileId: string, model: ModelType, thinkingLevel: ThinkingLevel) => void; + /** Called when model changes (in custom mode) */ + onModelChange: (model: ModelType) => void; + /** Called when thinking level changes (in custom mode) */ + onThinkingLevelChange: (level: ThinkingLevel) => void; + /** Called when phase models change (in auto mode) */ + onPhaseModelsChange?: (phaseModels: PhaseModelConfig) => void; + /** Called when phase thinking changes (in auto mode) */ + onPhaseThinkingChange?: (phaseThinking: PhaseThinkingConfig) => void; + /** Whether the selector is disabled */ + disabled?: boolean; +} + +const iconMap: Record = { + Brain, + Scale, + Zap, + Sparkles +}; + +const PHASE_LABELS: Record = { + spec: { label: 'Spec Creation', description: 'Discovery, requirements, context gathering' }, + planning: { label: 'Planning', description: 'Implementation planning and architecture' }, + coding: { label: 'Coding', description: 'Actual code implementation' }, + qa: { label: 'QA Review', description: 'Quality assurance and validation' } +}; + +export function AgentProfileSelector({ + profileId, + model, + thinkingLevel, + phaseModels, + phaseThinking, + onProfileChange, + onModelChange, + onThinkingLevelChange, + onPhaseModelsChange, + onPhaseThinkingChange, + disabled +}: AgentProfileSelectorProps) { + const [showPhaseDetails, setShowPhaseDetails] = useState(false); + + const isCustom = profileId === 'custom'; + const isAuto = profileId === 'auto'; + + // Use provided phase configs or defaults + const currentPhaseModels = phaseModels || DEFAULT_PHASE_MODELS; + const currentPhaseThinking = phaseThinking || DEFAULT_PHASE_THINKING; + + const handleProfileSelect = (selectedId: string) => { + if (selectedId === 'custom') { + // Keep current model/thinking level, just mark as custom + onProfileChange('custom', model as ModelType || 'sonnet', thinkingLevel as ThinkingLevel || 'medium'); + } else if (selectedId === 'auto') { + // Auto profile - set defaults + const autoProfile = DEFAULT_AGENT_PROFILES.find(p => p.id === 'auto'); + if (autoProfile) { + onProfileChange('auto', autoProfile.model, autoProfile.thinkingLevel); + // Initialize phase configs with defaults if callback provided + if (onPhaseModelsChange && autoProfile.phaseModels) { + onPhaseModelsChange(autoProfile.phaseModels); + } + if (onPhaseThinkingChange && autoProfile.phaseThinking) { + onPhaseThinkingChange(autoProfile.phaseThinking); + } + } + } else { + const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === selectedId); + if (profile) { + onProfileChange(profile.id, profile.model, profile.thinkingLevel); + } + } + }; + + const handlePhaseModelChange = (phase: keyof PhaseModelConfig, value: ModelType) => { + if (onPhaseModelsChange) { + onPhaseModelsChange({ + ...currentPhaseModels, + [phase]: value + }); + } + }; + + const handlePhaseThinkingChange = (phase: keyof PhaseThinkingConfig, value: ThinkingLevel) => { + if (onPhaseThinkingChange) { + onPhaseThinkingChange({ + ...currentPhaseThinking, + [phase]: value + }); + } + }; + + // Get profile display info + const getProfileDisplay = () => { + if (isCustom) { + return { + icon: Sliders, + label: 'Custom Configuration', + description: 'Choose model & thinking level' + }; + } + const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === profileId); + if (profile) { + return { + icon: iconMap[profile.icon || 'Scale'] || Scale, + label: profile.name, + description: profile.description + }; + } + // Default to balanced + return { + icon: Scale, + label: 'Balanced', + description: 'Good balance of speed and quality' + }; + }; + + const display = getProfileDisplay(); + + return ( +
+ {/* Agent Profile Selection */} +
+ + +

+ {display.description} +

+
+ + {/* Auto Profile - Phase Configuration */} + {isAuto && ( +
+ {/* Phase Summary */} +
+ + + {/* Compact summary when collapsed */} + {!showPhaseDetails && ( +
+ {(Object.keys(PHASE_LABELS) as Array).map((phase) => { + const modelLabel = AVAILABLE_MODELS.find(m => m.value === currentPhaseModels[phase])?.label?.replace('Claude ', '') || currentPhaseModels[phase]; + return ( +
+ {PHASE_LABELS[phase].label}: + {modelLabel} +
+ ); + })} +
+ )} +
+ + {/* Detailed Phase Configuration */} + {showPhaseDetails && ( +
+ {(Object.keys(PHASE_LABELS) as Array).map((phase) => ( +
+
+ + + {PHASE_LABELS[phase].description} + +
+
+ + +
+
+ ))} +
+ )} +
+ )} + + {/* Custom Configuration (shown only when custom is selected) */} + {isCustom && ( +
+ {/* Model Selection */} +
+ + +
+ + {/* Thinking Level Selection */} +
+ + +
+
+ )} +
+ ); +} diff --git a/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx b/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx index 5565601f..a15ca40e 100644 --- a/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx +++ b/auto-claude-ui/src/renderer/components/TaskCreationWizard.tsx @@ -40,10 +40,12 @@ import { } from './ImageUpload'; import { ReferencedFilesSection } from './ReferencedFilesSection'; import { TaskFileExplorerDrawer } from './TaskFileExplorerDrawer'; +import { AgentProfileSelector } from './AgentProfileSelector'; import { createTask, saveDraft, loadDraft, clearDraft, isDraftEmpty } from '../stores/task-store'; import { useProjectStore } from '../stores/project-store'; import { cn } from '../lib/utils'; import type { TaskCategory, TaskPriority, TaskComplexity, TaskImpact, TaskMetadata, ImageAttachment, TaskDraft, ModelType, ThinkingLevel, ReferencedFile } from '../../shared/types'; +import type { PhaseModelConfig, PhaseThinkingConfig } from '../../shared/types/settings'; import { TASK_CATEGORY_LABELS, TASK_PRIORITY_LABELS, @@ -53,8 +55,8 @@ import { MAX_REFERENCED_FILES, ALLOWED_IMAGE_TYPES_DISPLAY, DEFAULT_AGENT_PROFILES, - AVAILABLE_MODELS, - THINKING_LEVELS + DEFAULT_PHASE_MODELS, + DEFAULT_PHASE_THINKING } from '../../shared/constants'; import { useSettingsStore } from '../stores/settings-store'; @@ -73,7 +75,7 @@ export function TaskCreationWizard({ const { settings } = useSettingsStore(); const selectedProfile = DEFAULT_AGENT_PROFILES.find( p => p.id === settings.selectedAgentProfile - ) || DEFAULT_AGENT_PROFILES.find(p => p.id === 'balanced')!; + ) || DEFAULT_AGENT_PROFILES.find(p => p.id === 'auto')!; const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); @@ -97,8 +99,16 @@ export function TaskCreationWizard({ const [impact, setImpact] = useState(''); // Model configuration (initialized from selected agent profile) + const [profileId, setProfileId] = useState(settings.selectedAgentProfile || 'auto'); const [model, setModel] = useState(selectedProfile.model); const [thinkingLevel, setThinkingLevel] = useState(selectedProfile.thinkingLevel); + // Auto profile - per-phase configuration + const [phaseModels, setPhaseModels] = useState( + selectedProfile.phaseModels || DEFAULT_PHASE_MODELS + ); + const [phaseThinking, setPhaseThinking] = useState( + selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING + ); // Image attachments const [images, setImages] = useState([]); @@ -161,9 +171,12 @@ export function TaskCreationWizard({ setPriority(draft.priority); setComplexity(draft.complexity); setImpact(draft.impact); - // Load model/thinkingLevel from draft if present, otherwise use profile defaults + // Load model/thinkingLevel/profileId from draft if present, otherwise use profile defaults + setProfileId(draft.profileId || settings.selectedAgentProfile || 'balanced'); setModel(draft.model || selectedProfile.model); setThinkingLevel(draft.thinkingLevel || selectedProfile.thinkingLevel); + setPhaseModels(draft.phaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS); + setPhaseThinking(draft.phaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING); setImages(draft.images); setReferencedFiles(draft.referencedFiles ?? []); setRequireReviewBeforeCoding(draft.requireReviewBeforeCoding ?? false); @@ -178,12 +191,15 @@ export function TaskCreationWizard({ } // Note: Referenced Files section is always visible, no need to expand } else { - // No draft - initialize model/thinkingLevel from selected profile + // No draft - initialize from selected profile + setProfileId(settings.selectedAgentProfile || 'balanced'); setModel(selectedProfile.model); setThinkingLevel(selectedProfile.thinkingLevel); + setPhaseModels(selectedProfile.phaseModels || DEFAULT_PHASE_MODELS); + setPhaseThinking(selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING); } } - }, [open, projectId, selectedProfile.model, selectedProfile.thinkingLevel]); + }, [open, projectId, settings.selectedAgentProfile, selectedProfile.model, selectedProfile.thinkingLevel]); /** * Get current form state as a draft @@ -196,13 +212,16 @@ export function TaskCreationWizard({ priority, complexity, impact, + profileId, model, thinkingLevel, + phaseModels, + phaseThinking, images, referencedFiles, requireReviewBeforeCoding, savedAt: new Date() - }), [projectId, title, description, category, priority, complexity, impact, model, thinkingLevel, images, referencedFiles, requireReviewBeforeCoding]); + }), [projectId, title, description, category, priority, complexity, impact, profileId, model, thinkingLevel, phaseModels, phaseThinking, images, referencedFiles, requireReviewBeforeCoding]); /** * Handle paste event for screenshot support */ @@ -532,6 +551,12 @@ export function TaskCreationWizard({ if (impact) metadata.impact = impact; if (model) metadata.model = model; if (thinkingLevel) metadata.thinkingLevel = thinkingLevel; + // Auto profile - per-phase configuration + if (profileId === 'auto') { + metadata.isAutoProfile = true; + if (phaseModels) metadata.phaseModels = phaseModels; + if (phaseThinking) metadata.phaseThinking = phaseThinking; + } if (images.length > 0) metadata.attachedImages = images; if (allReferencedFiles.length > 0) metadata.referencedFiles = allReferencedFiles; if (requireReviewBeforeCoding) metadata.requireReviewBeforeCoding = true; @@ -561,9 +586,12 @@ export function TaskCreationWizard({ setPriority(''); setComplexity(''); setImpact(''); - // Reset model/thinkingLevel to selected profile defaults + // Reset to selected profile defaults + setProfileId(settings.selectedAgentProfile || 'balanced'); setModel(selectedProfile.model); setThinkingLevel(selectedProfile.thinkingLevel); + setPhaseModels(selectedProfile.phaseModels || DEFAULT_PHASE_MODELS); + setPhaseThinking(selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING); setImages([]); setReferencedFiles([]); setRequireReviewBeforeCoding(false); @@ -765,57 +793,24 @@ export function TaskCreationWizard({

- {/* Model Selection */} -
- - -

- The Claude model to use for this task. Defaults to your selected agent profile. -

-
- - {/* Thinking Level Selection */} -
- - -

- Extended thinking depth for complex reasoning. Higher levels use more tokens but provide deeper analysis. -

-
+ {/* Agent Profile Selection */} + { + setProfileId(newProfileId); + setModel(newModel); + setThinkingLevel(newThinkingLevel); + }} + onModelChange={setModel} + onThinkingLevelChange={setThinkingLevel} + onPhaseModelsChange={setPhaseModels} + onPhaseThinkingChange={setPhaseThinking} + disabled={isCreating} + /> {/* Paste Success Indicator */} {pasteSuccess && ( diff --git a/auto-claude-ui/src/renderer/components/TaskEditDialog.tsx b/auto-claude-ui/src/renderer/components/TaskEditDialog.tsx index d50c7961..d87ae977 100644 --- a/auto-claude-ui/src/renderer/components/TaskEditDialog.tsx +++ b/auto-claude-ui/src/renderer/components/TaskEditDialog.tsx @@ -54,17 +54,23 @@ import { isValidImageMimeType, resolveFilename } from './ImageUpload'; +import { AgentProfileSelector } from './AgentProfileSelector'; import { persistUpdateTask } from '../stores/task-store'; import { cn } from '../lib/utils'; -import type { Task, ImageAttachment, TaskCategory, TaskPriority, TaskComplexity, TaskImpact } from '../../shared/types'; +import type { Task, ImageAttachment, TaskCategory, TaskPriority, TaskComplexity, TaskImpact, ModelType, ThinkingLevel } from '../../shared/types'; import { TASK_CATEGORY_LABELS, TASK_PRIORITY_LABELS, TASK_COMPLEXITY_LABELS, TASK_IMPACT_LABELS, MAX_IMAGES_PER_TASK, - ALLOWED_IMAGE_TYPES_DISPLAY + ALLOWED_IMAGE_TYPES_DISPLAY, + DEFAULT_AGENT_PROFILES, + DEFAULT_PHASE_MODELS, + DEFAULT_PHASE_THINKING } from '../../shared/constants'; +import type { PhaseModelConfig, PhaseThinkingConfig } from '../../shared/types/settings'; +import { useSettingsStore } from '../stores/settings-store'; /** * Props for the TaskEditDialog component @@ -81,6 +87,12 @@ interface TaskEditDialogProps { } export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDialogProps) { + // Get selected agent profile from settings for defaults + const { settings } = useSettingsStore(); + const selectedProfile = DEFAULT_AGENT_PROFILES.find( + p => p.id === settings.selectedAgentProfile + ) || DEFAULT_AGENT_PROFILES.find(p => p.id === 'auto')!; + const [title, setTitle] = useState(task.title); const [description, setDescription] = useState(task.description); const [isSaving, setIsSaving] = useState(false); @@ -95,6 +107,36 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi const [complexity, setComplexity] = useState(task.metadata?.complexity || ''); const [impact, setImpact] = useState(task.metadata?.impact || ''); + // Agent profile / model configuration + const [profileId, setProfileId] = useState(() => { + // Check if task uses Auto profile + if (task.metadata?.isAutoProfile) { + return 'auto'; + } + // Determine profile ID from task metadata or default to 'auto' + const taskModel = task.metadata?.model; + const taskThinking = task.metadata?.thinkingLevel; + if (taskModel && taskThinking) { + // Check if it matches a known profile + const matchingProfile = DEFAULT_AGENT_PROFILES.find( + p => p.model === taskModel && p.thinkingLevel === taskThinking && !p.isAutoProfile + ); + return matchingProfile?.id || 'custom'; + } + return settings.selectedAgentProfile || 'auto'; + }); + const [model, setModel] = useState(task.metadata?.model || selectedProfile.model); + const [thinkingLevel, setThinkingLevel] = useState( + task.metadata?.thinkingLevel || selectedProfile.thinkingLevel + ); + // Auto profile - per-phase configuration + const [phaseModels, setPhaseModels] = useState( + task.metadata?.phaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS + ); + const [phaseThinking, setPhaseThinking] = useState( + task.metadata?.phaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING + ); + // Image attachments const [images, setImages] = useState(task.metadata?.attachedImages || []); @@ -118,6 +160,35 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi setPriority(task.metadata?.priority || ''); setComplexity(task.metadata?.complexity || ''); setImpact(task.metadata?.impact || ''); + + // Reset model configuration + const taskModel = task.metadata?.model; + const taskThinking = task.metadata?.thinkingLevel; + const isAutoProfile = task.metadata?.isAutoProfile; + + if (isAutoProfile) { + setProfileId('auto'); + setModel(taskModel || selectedProfile.model); + setThinkingLevel(taskThinking || selectedProfile.thinkingLevel); + setPhaseModels(task.metadata?.phaseModels || DEFAULT_PHASE_MODELS); + setPhaseThinking(task.metadata?.phaseThinking || DEFAULT_PHASE_THINKING); + } else if (taskModel && taskThinking) { + const matchingProfile = DEFAULT_AGENT_PROFILES.find( + p => p.model === taskModel && p.thinkingLevel === taskThinking && !p.isAutoProfile + ); + setProfileId(matchingProfile?.id || 'custom'); + setModel(taskModel); + setThinkingLevel(taskThinking); + setPhaseModels(DEFAULT_PHASE_MODELS); + setPhaseThinking(DEFAULT_PHASE_THINKING); + } else { + setProfileId(settings.selectedAgentProfile || 'auto'); + setModel(selectedProfile.model); + setThinkingLevel(selectedProfile.thinkingLevel); + setPhaseModels(selectedProfile.phaseModels || DEFAULT_PHASE_MODELS); + setPhaseThinking(selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING); + } + setImages(task.metadata?.attachedImages || []); setRequireReviewBeforeCoding(task.metadata?.requireReviewBeforeCoding ?? false); setError(null); @@ -130,7 +201,7 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi setShowImages((task.metadata?.attachedImages || []).length > 0); setPasteSuccess(false); } - }, [open, task]); + }, [open, task, settings.selectedAgentProfile, selectedProfile.model, selectedProfile.thinkingLevel]); /** * Handle paste event for screenshot support @@ -328,6 +399,8 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi priority !== (task.metadata?.priority || '') || complexity !== (task.metadata?.complexity || '') || impact !== (task.metadata?.impact || '') || + model !== (task.metadata?.model || '') || + thinkingLevel !== (task.metadata?.thinkingLevel || '') || requireReviewBeforeCoding !== (task.metadata?.requireReviewBeforeCoding ?? false) || JSON.stringify(images) !== JSON.stringify(task.metadata?.attachedImages || []); @@ -346,6 +419,17 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi if (priority) metadataUpdates.priority = priority; if (complexity) metadataUpdates.complexity = complexity; if (impact) metadataUpdates.impact = impact; + if (model) metadataUpdates.model = model as ModelType; + if (thinkingLevel) metadataUpdates.thinkingLevel = thinkingLevel as ThinkingLevel; + // Auto profile - per-phase configuration + if (profileId === 'auto') { + metadataUpdates.isAutoProfile = true; + if (phaseModels) metadataUpdates.phaseModels = phaseModels; + if (phaseThinking) metadataUpdates.phaseThinking = phaseThinking; + } else { + // Clear auto profile fields if switching away from auto + metadataUpdates.isAutoProfile = false; + } if (images.length > 0) metadataUpdates.attachedImages = images; metadataUpdates.requireReviewBeforeCoding = requireReviewBeforeCoding; @@ -429,6 +513,25 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi

+ {/* Agent Profile Selection */} + { + setProfileId(newProfileId); + setModel(newModel); + setThinkingLevel(newThinkingLevel); + }} + onModelChange={setModel} + onThinkingLevelChange={setThinkingLevel} + onPhaseModelsChange={setPhaseModels} + onPhaseThinkingChange={setPhaseThinking} + disabled={isSaving} + /> + {/* Paste Success Indicator */} {pasteSuccess && (
diff --git a/auto-claude-ui/src/renderer/components/task-detail/TaskLogs.tsx b/auto-claude-ui/src/renderer/components/task-detail/TaskLogs.tsx index c457117c..9f5a9ed9 100644 --- a/auto-claude-ui/src/renderer/components/task-detail/TaskLogs.tsx +++ b/auto-claude-ui/src/renderer/components/task-detail/TaskLogs.tsx @@ -14,12 +14,15 @@ import { Search, FolderSearch, Wrench, - Info + Info, + Brain, + Cpu } from 'lucide-react'; import { Badge } from '../ui/badge'; import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../ui/collapsible'; import { cn } from '../../lib/utils'; -import type { Task, TaskLogs, TaskLogPhase, TaskPhaseLog, TaskLogEntry } from '../../../shared/types'; +import type { Task, TaskLogs, TaskLogPhase, TaskPhaseLog, TaskLogEntry, TaskMetadata } from '../../../shared/types'; +import type { PhaseModelConfig, PhaseThinkingConfig, ThinkingLevel, ModelTypeShort } from '../../../shared/types/settings'; interface TaskLogsProps { task: Task; @@ -51,6 +54,60 @@ const PHASE_COLORS: Record = { validation: 'text-purple-500 bg-purple-500/10 border-purple-500/30' }; +// Map log phases to config phase keys +// Note: 'planning' log phase covers both spec creation and implementation planning +const LOG_PHASE_TO_CONFIG_PHASE: Record = { + planning: 'spec', // Planning log phase primarily shows spec creation + coding: 'coding', + validation: 'qa' +}; + +// Short labels for models +const MODEL_SHORT_LABELS: Record = { + opus: 'Opus', + sonnet: 'Sonnet', + haiku: 'Haiku' +}; + +// Short labels for thinking levels +const THINKING_SHORT_LABELS: Record = { + none: 'None', + low: 'Low', + medium: 'Med', + high: 'High', + ultrathink: 'Ultra' +}; + +// Helper to get model and thinking info for a log phase +function getPhaseConfig( + metadata: TaskMetadata | undefined, + logPhase: TaskLogPhase +): { model: string; thinking: string } | null { + if (!metadata) return null; + + const configPhase = LOG_PHASE_TO_CONFIG_PHASE[logPhase]; + + // Auto profile with per-phase config + if (metadata.isAutoProfile && metadata.phaseModels && metadata.phaseThinking) { + const model = metadata.phaseModels[configPhase]; + const thinking = metadata.phaseThinking[configPhase]; + return { + model: MODEL_SHORT_LABELS[model] || model, + thinking: THINKING_SHORT_LABELS[thinking] || thinking + }; + } + + // Non-auto profile with single model/thinking + if (metadata.model && metadata.thinkingLevel) { + return { + model: MODEL_SHORT_LABELS[metadata.model] || metadata.model, + thinking: THINKING_SHORT_LABELS[metadata.thinkingLevel] || metadata.thinkingLevel + }; + } + + return null; +} + export function TaskLogs({ task, phaseLogs, @@ -84,6 +141,7 @@ export function TaskLogs({ isExpanded={expandedPhases.has(phase)} onToggle={() => onTogglePhase(phase)} isTaskStuck={isStuck} + phaseConfig={getPhaseConfig(task.metadata, phase)} /> ))}
@@ -113,9 +171,10 @@ interface PhaseLogSectionProps { isExpanded: boolean; onToggle: () => void; isTaskStuck?: boolean; + phaseConfig?: { model: string; thinking: string } | null; } -function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck }: PhaseLogSectionProps) { +function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, phaseConfig }: PhaseLogSectionProps) { const Icon = PHASE_ICONS[phase]; const status = phaseLog?.status || 'pending'; const hasEntries = (phaseLog?.entries.length || 0) > 0; @@ -190,7 +249,23 @@ function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck }: )}
- {getStatusBadge()} +
+ {/* Model and thinking level indicator */} + {phaseConfig && ( +
+
+ + {phaseConfig.model} +
+ | +
+ + {phaseConfig.thinking} +
+
+ )} + {getStatusBadge()} +
diff --git a/auto-claude-ui/src/renderer/hooks/useIpc.ts b/auto-claude-ui/src/renderer/hooks/useIpc.ts index ba1103a4..7291ce23 100644 --- a/auto-claude-ui/src/renderer/hooks/useIpc.ts +++ b/auto-claude-ui/src/renderer/hooks/useIpc.ts @@ -48,69 +48,93 @@ export function useIpcListeners(): void { ); // Roadmap event listeners - const setGenerationStatus = useRoadmapStore.getState().setGenerationStatus; - const setRoadmap = useRoadmapStore.getState().setRoadmap; + // Helper to check if event is for the currently viewed project + const isCurrentProject = (eventProjectId: string): boolean => { + const currentProjectId = useRoadmapStore.getState().currentProjectId; + return currentProjectId === eventProjectId; + }; const cleanupRoadmapProgress = window.electronAPI.onRoadmapProgress( - (_projectId: string, status: RoadmapGenerationStatus) => { + (projectId: string, status: RoadmapGenerationStatus) => { // Debug logging if (window.DEBUG) { console.log('[Roadmap] Progress update:', { - projectId: _projectId, + projectId, + currentProjectId: useRoadmapStore.getState().currentProjectId, phase: status.phase, progress: status.progress, message: status.message }); } - setGenerationStatus(status); + // Only update if this is for the currently viewed project + if (isCurrentProject(projectId)) { + useRoadmapStore.getState().setGenerationStatus(status); + } } ); const cleanupRoadmapComplete = window.electronAPI.onRoadmapComplete( - (_projectId: string, roadmap: Roadmap) => { + (projectId: string, roadmap: Roadmap) => { // Debug logging if (window.DEBUG) { console.log('[Roadmap] Generation complete:', { - projectId: _projectId, + projectId, + currentProjectId: useRoadmapStore.getState().currentProjectId, featuresCount: roadmap.features?.length || 0, phasesCount: roadmap.phases?.length || 0 }); } - setRoadmap(roadmap); - setGenerationStatus({ - phase: 'complete', - progress: 100, - message: 'Roadmap ready' - }); + // Only update if this is for the currently viewed project + if (isCurrentProject(projectId)) { + useRoadmapStore.getState().setRoadmap(roadmap); + useRoadmapStore.getState().setGenerationStatus({ + phase: 'complete', + progress: 100, + message: 'Roadmap ready' + }); + } } ); const cleanupRoadmapError = window.electronAPI.onRoadmapError( - (_projectId: string, error: string) => { + (projectId: string, error: string) => { // Debug logging if (window.DEBUG) { - console.error('[Roadmap] Error received:', { projectId: _projectId, error }); + console.error('[Roadmap] Error received:', { + projectId, + currentProjectId: useRoadmapStore.getState().currentProjectId, + error + }); + } + // Only update if this is for the currently viewed project + if (isCurrentProject(projectId)) { + useRoadmapStore.getState().setGenerationStatus({ + phase: 'error', + progress: 0, + message: 'Generation failed', + error + }); } - setGenerationStatus({ - phase: 'error', - progress: 0, - message: 'Generation failed', - error - }); } ); const cleanupRoadmapStopped = window.electronAPI.onRoadmapStopped( - (_projectId: string) => { + (projectId: string) => { // Debug logging if (window.DEBUG) { - console.log('[Roadmap] Generation stopped:', { projectId: _projectId }); + console.log('[Roadmap] Generation stopped:', { + projectId, + currentProjectId: useRoadmapStore.getState().currentProjectId + }); + } + // Only update if this is for the currently viewed project + if (isCurrentProject(projectId)) { + useRoadmapStore.getState().setGenerationStatus({ + phase: 'idle', + progress: 0, + message: 'Generation stopped' + }); } - setGenerationStatus({ - phase: 'idle', - progress: 0, - message: 'Generation stopped' - }); } ); diff --git a/auto-claude-ui/src/shared/constants/config.ts b/auto-claude-ui/src/shared/constants/config.ts index 8ca5443b..213882cd 100644 --- a/auto-claude-ui/src/shared/constants/config.ts +++ b/auto-claude-ui/src/shared/constants/config.ts @@ -25,8 +25,8 @@ export const DEFAULT_APP_SETTINGS = { // Global API keys (used as defaults for all projects) globalClaudeOAuthToken: undefined as string | undefined, globalOpenAIApiKey: undefined as string | undefined, - // Selected agent profile - defaults to 'balanced' for good speed/quality balance - selectedAgentProfile: 'balanced', + // Selected agent profile - defaults to 'auto' for per-phase optimized model selection + selectedAgentProfile: 'auto', // Changelog preferences (persisted between sessions) changelogFormat: 'keep-a-changelog' as const, changelogAudience: 'user-facing' as const, diff --git a/auto-claude-ui/src/shared/constants/models.ts b/auto-claude-ui/src/shared/constants/models.ts index b853a3dc..8e9e5b89 100644 --- a/auto-claude-ui/src/shared/constants/models.ts +++ b/auto-claude-ui/src/shared/constants/models.ts @@ -3,7 +3,7 @@ * Claude models, thinking levels, memory backends, and agent profiles */ -import type { AgentProfile } from '../types/settings'; +import type { AgentProfile, PhaseModelConfig } from '../types/settings'; // ============================================ // Available Models @@ -48,8 +48,36 @@ export const THINKING_LEVELS = [ // Agent Profiles // ============================================ +// Default phase model configuration for Auto profile +// Optimized for each phase: fast discovery, quality planning, balanced coding, thorough QA +export const DEFAULT_PHASE_MODELS: PhaseModelConfig = { + spec: 'sonnet', // Good quality specs without being too slow + planning: 'opus', // Complex architecture decisions benefit from Opus + coding: 'sonnet', // Good balance of speed and quality for implementation + qa: 'sonnet' // Thorough but not overly slow QA +}; + +// Default phase thinking configuration for Auto profile +export const DEFAULT_PHASE_THINKING: import('../types/settings').PhaseThinkingConfig = { + spec: 'medium', // Moderate thinking for spec creation + planning: 'high', // Deep thinking for planning complex features + coding: 'medium', // Standard thinking for coding + qa: 'high' // Thorough analysis for QA review +}; + // Default agent profiles for preset model/thinking configurations export const DEFAULT_AGENT_PROFILES: AgentProfile[] = [ + { + id: 'auto', + name: 'Auto (Optimized)', + description: 'Uses different models per phase for optimal speed & quality', + model: 'sonnet', // Fallback/default model + thinkingLevel: 'medium', + icon: 'Sparkles', + isAutoProfile: true, + phaseModels: DEFAULT_PHASE_MODELS, + phaseThinking: DEFAULT_PHASE_THINKING + }, { id: 'complex', name: 'Complex Tasks', diff --git a/auto-claude-ui/src/shared/types/settings.ts b/auto-claude-ui/src/shared/types/settings.ts index 7c002167..9b5d0748 100644 --- a/auto-claude-ui/src/shared/types/settings.ts +++ b/auto-claude-ui/src/shared/types/settings.ts @@ -8,14 +8,38 @@ import type { ChangelogFormat, ChangelogAudience, ChangelogEmojiLevel } from './ // Thinking level for Claude model (budget token allocation) export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; +// Model type shorthand +export type ModelTypeShort = 'haiku' | 'sonnet' | 'opus'; + +// Phase-based model configuration for Auto profile +// Each phase can use a different model optimized for that task type +export interface PhaseModelConfig { + spec: ModelTypeShort; // Spec creation (discovery, requirements, context) + planning: ModelTypeShort; // Implementation planning + coding: ModelTypeShort; // Actual coding implementation + qa: ModelTypeShort; // QA review and fixing +} + +// Thinking level configuration per phase +export interface PhaseThinkingConfig { + spec: ThinkingLevel; + planning: ThinkingLevel; + coding: ThinkingLevel; + qa: ThinkingLevel; +} + // Agent profile for preset model/thinking configurations export interface AgentProfile { id: string; name: string; description: string; - model: 'haiku' | 'sonnet' | 'opus'; + model: ModelTypeShort; thinkingLevel: ThinkingLevel; icon?: string; // Lucide icon name + // Auto profile specific - per-phase configuration + isAutoProfile?: boolean; + phaseModels?: PhaseModelConfig; + phaseThinking?: PhaseThinkingConfig; } export interface AppSettings { diff --git a/auto-claude-ui/src/shared/types/task.ts b/auto-claude-ui/src/shared/types/task.ts index c4896278..33d6e15c 100644 --- a/auto-claude-ui/src/shared/types/task.ts +++ b/auto-claude-ui/src/shared/types/task.ts @@ -2,7 +2,7 @@ * Task-related types */ -import type { ThinkingLevel } from './settings'; +import type { ThinkingLevel, PhaseModelConfig, PhaseThinkingConfig } from './settings'; export type TaskStatus = 'backlog' | 'in_progress' | 'ai_review' | 'human_review' | 'done'; @@ -136,8 +136,12 @@ export interface TaskDraft { priority: TaskPriority | ''; complexity: TaskComplexity | ''; impact: TaskImpact | ''; + profileId?: string; // Agent profile ID ('auto', 'complex', 'balanced', 'quick', 'custom') model: ModelType | ''; thinkingLevel: ThinkingLevel | ''; + // Auto profile - per-phase configuration + phaseModels?: PhaseModelConfig; + phaseThinking?: PhaseThinkingConfig; images: ImageAttachment[]; referencedFiles: ReferencedFile[]; requireReviewBeforeCoding?: boolean; @@ -209,8 +213,12 @@ export interface TaskMetadata { requireReviewBeforeCoding?: boolean; // Require human review of spec/plan before coding starts // Agent configuration (from agent profile or manual selection) - model?: ModelType; // Claude model to use (haiku, sonnet, opus) + model?: ModelType; // Claude model to use (haiku, sonnet, opus) - used when not auto profile thinkingLevel?: ThinkingLevel; // Thinking budget level (none, low, medium, high, ultrathink) + // Auto profile - per-phase model configuration + isAutoProfile?: boolean; // True when using Auto (Optimized) profile + phaseModels?: PhaseModelConfig; // Per-phase model configuration + phaseThinking?: PhaseThinkingConfig; // Per-phase thinking configuration // Archive status archivedAt?: string; // ISO date when task was archived diff --git a/auto-claude/agents/coder.py b/auto-claude/agents/coder.py index 2af5233f..d0e99a53 100644 --- a/auto-claude/agents/coder.py +++ b/auto-claude/agents/coder.py @@ -9,7 +9,7 @@ import asyncio import logging from pathlib import Path -from client import create_client +from core.client import create_client from linear_updater import ( LinearTaskState, is_linear_enabled, @@ -17,6 +17,7 @@ from linear_updater import ( linear_task_started, linear_task_stuck, ) +from phase_config import get_phase_model from progress import ( count_subtasks, count_subtasks_detailed, @@ -244,8 +245,19 @@ async def run_autonomous_agent( commit_before = get_latest_commit(project_dir) commit_count_before = get_commit_count(project_dir) - # Create client (fresh context) - client = create_client(project_dir, spec_dir, model) + # Get the phase-specific model (respects task_metadata.json configuration) + # first_run means we're in planning phase, otherwise coding phase + current_phase = "planning" if first_run else "coding" + phase_model = get_phase_model(spec_dir, current_phase, model) + + # Create client (fresh context) with phase-specific model + # Coding phase uses no extended thinking for fast iteration + client = create_client( + project_dir, + spec_dir, + phase_model, + max_thinking_tokens=None, # No extended thinking for coding + ) # Generate appropriate prompt if first_run: diff --git a/auto-claude/agents/planner.py b/auto-claude/agents/planner.py index 3bd34e67..7f6f3f98 100644 --- a/auto-claude/agents/planner.py +++ b/auto-claude/agents/planner.py @@ -8,7 +8,7 @@ Handles follow-up planner sessions for adding new subtasks to completed specs. import logging from pathlib import Path -from client import create_client +from core.client import create_client from task_logger import ( LogPhase, get_task_logger, diff --git a/auto-claude/cli/build_commands.py b/auto-claude/cli/build_commands.py index 6aa7399e..0de13e58 100644 --- a/auto-claude/cli/build_commands.py +++ b/auto-claude/cli/build_commands.py @@ -68,7 +68,7 @@ def handle_build_command( Args: project_dir: Project root directory spec_dir: Spec directory path - model: Model to use + model: Model to use (used as default; may be overridden by task_metadata.json) max_iterations: Maximum number of iterations (None for unlimited) verbose: Enable verbose output force_isolated: Force isolated workspace mode @@ -86,14 +86,27 @@ def handle_build_command( debug_section, debug_success, ) + from phase_config import get_phase_model from qa_loop import run_qa_validation_loop, should_run_qa from .utils import print_banner, validate_environment + # Get the resolved model for the planning phase (first phase of build) + # This respects task_metadata.json phase configuration from the UI + planning_model = get_phase_model(spec_dir, "planning", model) + coding_model = get_phase_model(spec_dir, "coding", model) + qa_model = get_phase_model(spec_dir, "qa", model) + print_banner() print(f"\nProject directory: {project_dir}") print(f"Spec: {spec_dir.name}") - print(f"Model: {model}") + # Show phase-specific models if they differ + if planning_model != coding_model or coding_model != qa_model: + print(f"Models: Planning={planning_model.split('-')[1] if '-' in planning_model else planning_model}, " + f"Coding={coding_model.split('-')[1] if '-' in coding_model else coding_model}, " + f"QA={qa_model.split('-')[1] if '-' in qa_model else qa_model}") + else: + print(f"Model: {planning_model}") if max_iterations: print(f"Max iterations: {max_iterations}") diff --git a/auto-claude/core/client.py b/auto-claude/core/client.py index 32e1d016..1b7c11db 100644 --- a/auto-claude/core/client.py +++ b/auto-claude/core/client.py @@ -132,6 +132,7 @@ def create_client( spec_dir: Path, model: str, agent_type: str = "coder", + max_thinking_tokens: int | None = None, ) -> ClaudeSDKClient: """ Create a Claude Agent SDK client with multi-layered security. @@ -142,6 +143,11 @@ def create_client( model: Claude model to use agent_type: Type of agent - 'planner', 'coder', 'qa_reviewer', or 'qa_fixer' This determines which custom auto-claude tools are available. + max_thinking_tokens: Token budget for extended thinking (None = disabled) + - ultrathink: 16000 (spec creation) + - high: 10000 (QA review) + - medium: 5000 (planning, validation) + - None: disabled (coding) Returns: Configured ClaudeSDKClient @@ -237,6 +243,10 @@ def create_client( print(" - Sandbox enabled (OS-level bash isolation)") print(f" - Filesystem restricted to: {project_dir.resolve()}") print(" - Bash commands restricted to allowlist") + if max_thinking_tokens: + print(f" - Extended thinking: {max_thinking_tokens:,} tokens") + else: + print(" - Extended thinking: disabled") mcp_servers_list = ["puppeteer (browser automation)", "context7 (documentation)"] if linear_enabled: @@ -320,5 +330,6 @@ def create_client( cwd=str(project_dir.resolve()), settings=str(settings_file.resolve()), env=sdk_env, # Pass ANTHROPIC_BASE_URL etc. to subprocess + max_thinking_tokens=max_thinking_tokens, # Extended thinking budget ) ) diff --git a/auto-claude/phase_config.py b/auto-claude/phase_config.py new file mode 100644 index 00000000..dd9e2759 --- /dev/null +++ b/auto-claude/phase_config.py @@ -0,0 +1,290 @@ +""" +Phase Configuration Module +=========================== + +Handles model and thinking level configuration for different execution phases. +Reads configuration from task_metadata.json and provides resolved model IDs. +""" + +import json +from pathlib import Path +from typing import Literal, TypedDict + +# Model shorthand to full model ID mapping +MODEL_ID_MAP: dict[str, str] = { + "opus": "claude-opus-4-5-20251101", + "sonnet": "claude-sonnet-4-5-20250929", + "haiku": "claude-haiku-4-5-20251001", +} + +# Thinking level to budget tokens mapping (None = no extended thinking) +# Values calibrated for Claude Opus 4.5 extended thinking +THINKING_BUDGET_MAP: dict[str, int | None] = { + "none": None, + "low": 1024, + "medium": 5000, # Balanced thinking for light phases + "high": 10000, # Deep thinking for QA review + "ultrathink": 16000, # Maximum thinking for spec creation +} + +# Spec runner phase-specific thinking levels +# Heavy phases use ultrathink for deep analysis +# Light phases use medium after compaction +SPEC_PHASE_THINKING_LEVELS: dict[str, str] = { + # Heavy phases - ultrathink (discovery, spec creation, self-critique) + "discovery": "ultrathink", + "spec_writing": "ultrathink", + "self_critique": "ultrathink", + # Light phases - medium (after first invocation with compaction) + "requirements": "medium", + "research": "medium", + "context": "medium", + "planning": "medium", + "validation": "medium", + "quick_spec": "medium", + "historical_context": "medium", + "complexity_assessment": "medium", +} + +# Default phase configuration (matches UI defaults) +DEFAULT_PHASE_MODELS: dict[str, str] = { + "spec": "sonnet", + "planning": "opus", + "coding": "sonnet", + "qa": "sonnet", +} + +DEFAULT_PHASE_THINKING: dict[str, str] = { + "spec": "medium", + "planning": "high", + "coding": "medium", + "qa": "high", +} + + +class PhaseModelConfig(TypedDict, total=False): + spec: str + planning: str + coding: str + qa: str + + +class PhaseThinkingConfig(TypedDict, total=False): + spec: str + planning: str + coding: str + qa: str + + +class TaskMetadataConfig(TypedDict, total=False): + """Structure of model-related fields in task_metadata.json""" + isAutoProfile: bool + phaseModels: PhaseModelConfig + phaseThinking: PhaseThinkingConfig + model: str + thinkingLevel: str + + +Phase = Literal["spec", "planning", "coding", "qa"] + + +def resolve_model_id(model: str) -> str: + """ + Resolve a model shorthand (haiku, sonnet, opus) to a full model ID. + If the model is already a full ID, return it unchanged. + + Args: + model: Model shorthand or full ID + + Returns: + Full Claude model ID + """ + # Check if it's a shorthand + if model in MODEL_ID_MAP: + return MODEL_ID_MAP[model] + + # Already a full model ID + return model + + +def get_thinking_budget(thinking_level: str) -> int | None: + """ + Get the thinking budget for a thinking level. + + Args: + thinking_level: Thinking level (none, low, medium, high, ultrathink) + + Returns: + Token budget or None for no extended thinking + """ + return THINKING_BUDGET_MAP.get(thinking_level, THINKING_BUDGET_MAP["medium"]) + + +def load_task_metadata(spec_dir: Path) -> TaskMetadataConfig | None: + """ + Load task_metadata.json from the spec directory. + + Args: + spec_dir: Path to the spec directory + + Returns: + Parsed task metadata or None if not found + """ + metadata_path = spec_dir / "task_metadata.json" + if not metadata_path.exists(): + return None + + try: + with open(metadata_path) as f: + return json.load(f) + except (json.JSONDecodeError, OSError): + return None + + +def get_phase_model( + spec_dir: Path, + phase: Phase, + cli_model: str | None = None, +) -> str: + """ + Get the resolved model ID for a specific execution phase. + + Priority: + 1. CLI argument (if provided) + 2. Phase-specific config from task_metadata.json (if auto profile) + 3. Single model from task_metadata.json (if not auto profile) + 4. Default phase configuration + + Args: + spec_dir: Path to the spec directory + phase: Execution phase (spec, planning, coding, qa) + cli_model: Model from CLI argument (optional) + + Returns: + Resolved full model ID + """ + # CLI argument takes precedence + if cli_model: + return resolve_model_id(cli_model) + + # Load task metadata + metadata = load_task_metadata(spec_dir) + + if metadata: + # Check for auto profile with phase-specific config + if metadata.get("isAutoProfile") and metadata.get("phaseModels"): + phase_models = metadata["phaseModels"] + model = phase_models.get(phase, DEFAULT_PHASE_MODELS[phase]) + return resolve_model_id(model) + + # Non-auto profile: use single model + if metadata.get("model"): + return resolve_model_id(metadata["model"]) + + # Fall back to default phase configuration + return resolve_model_id(DEFAULT_PHASE_MODELS[phase]) + + +def get_phase_thinking( + spec_dir: Path, + phase: Phase, + cli_thinking: str | None = None, +) -> str: + """ + Get the thinking level for a specific execution phase. + + Priority: + 1. CLI argument (if provided) + 2. Phase-specific config from task_metadata.json (if auto profile) + 3. Single thinking level from task_metadata.json (if not auto profile) + 4. Default phase configuration + + Args: + spec_dir: Path to the spec directory + phase: Execution phase (spec, planning, coding, qa) + cli_thinking: Thinking level from CLI argument (optional) + + Returns: + Thinking level string + """ + # CLI argument takes precedence + if cli_thinking: + return cli_thinking + + # Load task metadata + metadata = load_task_metadata(spec_dir) + + if metadata: + # Check for auto profile with phase-specific config + if metadata.get("isAutoProfile") and metadata.get("phaseThinking"): + phase_thinking = metadata["phaseThinking"] + return phase_thinking.get(phase, DEFAULT_PHASE_THINKING[phase]) + + # Non-auto profile: use single thinking level + if metadata.get("thinkingLevel"): + return metadata["thinkingLevel"] + + # Fall back to default phase configuration + return DEFAULT_PHASE_THINKING[phase] + + +def get_phase_thinking_budget( + spec_dir: Path, + phase: Phase, + cli_thinking: str | None = None, +) -> int | None: + """ + Get the thinking budget tokens for a specific execution phase. + + Args: + spec_dir: Path to the spec directory + phase: Execution phase (spec, planning, coding, qa) + cli_thinking: Thinking level from CLI argument (optional) + + Returns: + Token budget or None for no extended thinking + """ + thinking_level = get_phase_thinking(spec_dir, phase, cli_thinking) + return get_thinking_budget(thinking_level) + + +def get_phase_config( + spec_dir: Path, + phase: Phase, + cli_model: str | None = None, + cli_thinking: str | None = None, +) -> tuple[str, str, int | None]: + """ + Get the full configuration for a specific execution phase. + + Args: + spec_dir: Path to the spec directory + phase: Execution phase (spec, planning, coding, qa) + cli_model: Model from CLI argument (optional) + cli_thinking: Thinking level from CLI argument (optional) + + Returns: + Tuple of (model_id, thinking_level, thinking_budget) + """ + model_id = get_phase_model(spec_dir, phase, cli_model) + thinking_level = get_phase_thinking(spec_dir, phase, cli_thinking) + thinking_budget = get_thinking_budget(thinking_level) + + return model_id, thinking_level, thinking_budget + + +def get_spec_phase_thinking_budget(phase_name: str) -> int | None: + """ + Get the thinking budget for a specific spec runner phase. + + This maps granular spec phases (discovery, spec_writing, etc.) to their + appropriate thinking budgets based on SPEC_PHASE_THINKING_LEVELS. + + Args: + phase_name: Name of the spec phase (e.g., 'discovery', 'spec_writing') + + Returns: + Token budget for extended thinking, or None for no extended thinking + """ + thinking_level = SPEC_PHASE_THINKING_LEVELS.get(phase_name, "medium") + return get_thinking_budget(thinking_level) diff --git a/auto-claude/qa/loop.py b/auto-claude/qa/loop.py index bfd04a60..df1845ba 100644 --- a/auto-claude/qa/loop.py +++ b/auto-claude/qa/loop.py @@ -9,8 +9,9 @@ approval or max iterations. import time as time_module from pathlib import Path -from client import create_client +from core.client import create_client from debug import debug, debug_error, debug_section, debug_success, debug_warning +from phase_config import get_phase_model, get_thinking_budget from linear_updater import ( LinearTaskState, is_linear_enabled, @@ -151,9 +152,22 @@ async def run_qa_validation_loop( print(f"\n--- QA Iteration {qa_iteration}/{MAX_QA_ITERATIONS} ---") - # Run QA reviewer - debug("qa_loop", "Creating client for QA reviewer session...") - client = create_client(project_dir, spec_dir, model) + # Run QA reviewer with phase-specific model and high thinking budget + qa_model = get_phase_model(spec_dir, "qa", model) + qa_thinking_budget = get_thinking_budget("high") # 10,000 tokens for thorough review + debug( + "qa_loop", + "Creating client for QA reviewer session...", + model=qa_model, + thinking_budget=qa_thinking_budget, + ) + client = create_client( + project_dir, + spec_dir, + qa_model, + agent_type="qa_reviewer", + max_thinking_tokens=qa_thinking_budget, + ) async with client: debug("qa_loop", "Running QA reviewer agent session...") @@ -278,11 +292,23 @@ async def run_qa_validation_loop( print("Escalating to human review.") break - # Run fixer - debug("qa_loop", "Starting QA fixer session...") + # Run fixer with medium thinking budget + fixer_thinking_budget = get_thinking_budget("medium") # 5,000 tokens for focused fixes + debug( + "qa_loop", + "Starting QA fixer session...", + model=qa_model, + thinking_budget=fixer_thinking_budget, + ) print("\nRunning QA Fixer Agent...") - fix_client = create_client(project_dir, spec_dir, model) + fix_client = create_client( + project_dir, + spec_dir, + qa_model, + agent_type="qa_fixer", + max_thinking_tokens=fixer_thinking_budget, + ) async with fix_client: fix_status, fix_response = await run_qa_fixer_session( diff --git a/auto-claude/runners/spec_runner.py b/auto-claude/runners/spec_runner.py index f04eec55..b0605412 100644 --- a/auto-claude/runners/spec_runner.py +++ b/auto-claude/runners/spec_runner.py @@ -92,6 +92,7 @@ elif dev_env_file.exists(): load_dotenv(dev_env_file) from debug import debug, debug_error, debug_section, debug_success +from phase_config import get_phase_config, resolve_model_id from review import ReviewState from spec import SpecOrchestrator from ui import Icons, highlight, icon, muted, print_section, print_status @@ -161,8 +162,15 @@ Examples: parser.add_argument( "--model", type=str, - default="claude-opus-4-5-20251101", - help="Model to use for agent phases", + default="sonnet", + help="Model to use for agent phases (haiku, sonnet, opus, or full model ID)", + ) + parser.add_argument( + "--thinking-level", + type=str, + default="medium", + choices=["none", "low", "medium", "high", "ultrathink"], + help="Thinking level for extended thinking (none, low, medium, high, ultrathink)", ) parser.add_argument( "--no-ai-assessment", @@ -234,12 +242,16 @@ Examples: f"\n{icon(Icons.GEAR)} Note: --dev flag is deprecated. All specs now go to .auto-claude/specs/\n" ) + # Resolve model shorthand to full model ID + resolved_model = resolve_model_id(args.model) + debug( "spec_runner", "Creating spec orchestrator", project_dir=str(project_dir), task_description=task_description[:200] if task_description else None, - model=args.model, + model=resolved_model, + thinking_level=args.thinking_level, complexity_override=args.complexity, use_ai_assessment=not args.no_ai_assessment, interactive=args.interactive or not task_description, @@ -251,7 +263,8 @@ Examples: task_description=task_description, spec_name=args.continue_spec, spec_dir=args.spec_dir, - model=args.model, + model=resolved_model, + thinking_level=args.thinking_level, complexity_override=args.complexity, use_ai_assessment=not args.no_ai_assessment, dev_mode=args.dev, @@ -321,9 +334,9 @@ Examples: if args.dev: run_cmd.append("--dev") - # Pass through model if not default - if args.model != "claude-opus-4-5-20251101": - run_cmd.extend(["--model", args.model]) + # Note: Model configuration for subsequent phases (planning, coding, qa) + # is read from task_metadata.json by run.py, so we don't pass it here. + # This allows per-phase configuration when using Auto profile. debug( "spec_runner", diff --git a/auto-claude/spec/compaction.py b/auto-claude/spec/compaction.py new file mode 100644 index 00000000..7075c7d7 --- /dev/null +++ b/auto-claude/spec/compaction.py @@ -0,0 +1,155 @@ +""" +Conversation Compaction Module +============================== + +Summarizes phase outputs to maintain continuity between phases while +reducing token usage. After each phase completes, key findings are +summarized and passed as context to subsequent phases. +""" + +from pathlib import Path + +from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient + +from core.auth import get_sdk_env_vars, require_auth_token + + +async def summarize_phase_output( + phase_name: str, + phase_output: str, + model: str = "claude-sonnet-4-5-20250929", + target_words: int = 500, +) -> str: + """ + Summarize phase output to a concise summary for subsequent phases. + + Uses Sonnet for cost efficiency since this is a simple summarization task. + + Args: + phase_name: Name of the completed phase (e.g., 'discovery', 'requirements') + phase_output: Full output content from the phase (file contents, decisions) + model: Model to use for summarization (defaults to Sonnet for efficiency) + target_words: Target summary length in words (~500-1000 recommended) + + Returns: + Concise summary of key findings, decisions, and insights from the phase + """ + # Validate auth token + require_auth_token() + + # Limit input size to avoid token overflow + max_input_chars = 15000 + truncated_output = phase_output[:max_input_chars] + if len(phase_output) > max_input_chars: + truncated_output += "\n\n[... output truncated for summarization ...]" + + prompt = f"""Summarize the key findings from the "{phase_name}" phase in {target_words} words or less. + +Focus on extracting ONLY the most critical information that subsequent phases need: +- Key decisions made and their rationale +- Critical files, components, or patterns identified +- Important constraints or requirements discovered +- Actionable insights for implementation + +Be concise and use bullet points. Skip boilerplate and meta-commentary. + +## Phase Output: +{truncated_output} + +## Summary: +""" + + client = ClaudeSDKClient( + options=ClaudeAgentOptions( + model=model, + system_prompt=( + "You are a concise technical summarizer. Extract only the most " + "critical information from phase outputs. Use bullet points. " + "Focus on decisions, discoveries, and actionable insights." + ), + allowed_tools=[], # No tools needed for summarization + max_turns=1, + env=get_sdk_env_vars(), + ) + ) + + try: + async with client: + await client.query(prompt) + response_text = "" + async for msg in client.receive_response(): + if hasattr(msg, "content"): + for block in msg.content: + if hasattr(block, "text"): + response_text += block.text + return response_text.strip() + except Exception as e: + # Fallback: return truncated raw output on error + # This ensures we don't block the pipeline if summarization fails + fallback = phase_output[:2000] + if len(phase_output) > 2000: + fallback += "\n\n[... truncated ...]" + return f"[Summarization failed: {e}]\n\n{fallback}" + + +def format_phase_summaries(summaries: dict[str, str]) -> str: + """ + Format accumulated phase summaries for injection into agent context. + + Args: + summaries: Dict mapping phase names to their summaries + + Returns: + Formatted string suitable for agent context injection + """ + if not summaries: + return "" + + formatted_parts = ["## Context from Previous Phases\n"] + for phase_name, summary in summaries.items(): + formatted_parts.append(f"### {phase_name.replace('_', ' ').title()}\n{summary}\n") + + return "\n".join(formatted_parts) + + +async def gather_phase_outputs(spec_dir: Path, phase_name: str) -> str: + """ + Gather output files from a completed phase for summarization. + + Args: + spec_dir: Path to the spec directory + phase_name: Name of the completed phase + + Returns: + Concatenated content of phase output files + """ + outputs = [] + + # Map phases to their expected output files + phase_outputs: dict[str, list[str]] = { + "discovery": ["context.json"], + "requirements": ["requirements.json"], + "research": ["research.json"], + "context": ["context.json"], + "quick_spec": ["spec.md"], + "spec_writing": ["spec.md"], + "self_critique": ["spec.md", "critique_notes.md"], + "planning": ["implementation_plan.json"], + "validation": [], # No output files to summarize + } + + output_files = phase_outputs.get(phase_name, []) + + for filename in output_files: + file_path = spec_dir / filename + if file_path.exists(): + try: + content = file_path.read_text() + # Limit individual file size + if len(content) > 10000: + content = content[:10000] + "\n\n[... file truncated ...]" + outputs.append(f"**{filename}**:\n```\n{content}\n```") + except Exception: + pass # Skip files that can't be read + + return "\n\n".join(outputs) if outputs else "" diff --git a/auto-claude/spec/phases/planning_phases.py b/auto-claude/spec/phases/planning_phases.py index 8d057ed1..7cbd81d8 100644 --- a/auto-claude/spec/phases/planning_phases.py +++ b/auto-claude/spec/phases/planning_phases.py @@ -73,7 +73,10 @@ class PlanningPhaseMixin: f"Running planner agent (attempt {attempt + 1})...", "progress" ) - success, output = await self.run_agent_fn("planner.md") + success, output = await self.run_agent_fn( + "planner.md", + phase_name="planning", + ) if success and plan_file.exists(): result = self.spec_validator.validate_implementation_plan() @@ -161,6 +164,7 @@ Read the failed files, understand the errors, and fix them. success, output = await self.run_agent_fn( "validation_fixer.md", additional_context=context_str, + phase_name="validation", ) if not success: diff --git a/auto-claude/spec/phases/requirements_phases.py b/auto-claude/spec/phases/requirements_phases.py index a1eb9628..05a72679 100644 --- a/auto-claude/spec/phases/requirements_phases.py +++ b/auto-claude/spec/phases/requirements_phases.py @@ -221,6 +221,7 @@ Output your findings to research.json. success, output = await self.run_agent_fn( "spec_researcher.md", additional_context=context_str, + phase_name="research", ) if success and research_file.exists(): diff --git a/auto-claude/spec/phases/spec_phases.py b/auto-claude/spec/phases/spec_phases.py index d8ee3117..29689c31 100644 --- a/auto-claude/spec/phases/spec_phases.py +++ b/auto-claude/spec/phases/spec_phases.py @@ -50,6 +50,7 @@ Create: success, output = await self.run_agent_fn( "spec_quick.md", additional_context=context_str, + phase_name="quick_spec", ) if success and spec_file.exists(): @@ -85,7 +86,10 @@ Create: f"Running spec writer (attempt {attempt + 1})...", "progress" ) - success, output = await self.run_agent_fn("spec_writer.md") + success, output = await self.run_agent_fn( + "spec_writer.md", + phase_name="spec_writing", + ) if success and spec_file.exists(): result = self.spec_validator.validate_spec_document() @@ -162,6 +166,7 @@ Output critique_report.json with: success, output = await self.run_agent_fn( "spec_critic.md", additional_context=context_str, + phase_name="self_critique", ) if success: diff --git a/auto-claude/spec/pipeline/agent_runner.py b/auto-claude/spec/pipeline/agent_runner.py index 5dbc7edb..57e08f06 100644 --- a/auto-claude/spec/pipeline/agent_runner.py +++ b/auto-claude/spec/pipeline/agent_runner.py @@ -12,7 +12,7 @@ from ui.capabilities import configure_safe_encoding configure_safe_encoding() -from client import create_client +from core.client import create_client from debug import debug, debug_detailed, debug_error, debug_section, debug_success from task_logger import ( LogEntryType, @@ -49,6 +49,8 @@ class AgentRunner: prompt_file: str, additional_context: str = "", interactive: bool = False, + thinking_budget: int | None = None, + prior_phase_summaries: str | None = None, ) -> tuple[bool, str]: """Run an agent with the given prompt. @@ -56,6 +58,8 @@ class AgentRunner: prompt_file: The prompt file to use (relative to prompts directory) additional_context: Additional context to add to the prompt interactive: Whether to run in interactive mode + thinking_budget: Token budget for extended thinking (None = disabled) + prior_phase_summaries: Summaries from previous phases for context Returns: Tuple of (success, response_text) @@ -88,6 +92,15 @@ class AgentRunner: prompt += f"\n\n---\n\n**Spec Directory**: {self.spec_dir}\n" prompt += f"**Project Directory**: {self.project_dir}\n" + # Add summaries from previous phases (compaction) + if prior_phase_summaries: + prompt += f"\n{prior_phase_summaries}\n" + debug_detailed( + "agent_runner", + "Added prior phase summaries", + summaries_length=len(prior_phase_summaries), + ) + if additional_context: prompt += f"\n{additional_context}\n" debug_detailed( @@ -96,9 +109,18 @@ class AgentRunner: context_length=len(additional_context), ) - # Create client - debug("agent_runner", "Creating Claude SDK client...") - client = create_client(self.project_dir, self.spec_dir, self.model) + # Create client with thinking budget + debug( + "agent_runner", + "Creating Claude SDK client...", + thinking_budget=thinking_budget, + ) + client = create_client( + self.project_dir, + self.spec_dir, + self.model, + max_thinking_tokens=thinking_budget, + ) current_tool = None message_count = 0 diff --git a/auto-claude/spec/pipeline/orchestrator.py b/auto-claude/spec/pipeline/orchestrator.py index 4db90d9a..fbfbffc0 100644 --- a/auto-claude/spec/pipeline/orchestrator.py +++ b/auto-claude/spec/pipeline/orchestrator.py @@ -9,6 +9,7 @@ import json from collections.abc import Callable from pathlib import Path +from phase_config import get_spec_phase_thinking_budget from review import run_review_checkpoint from task_logger import ( LogEntryType, @@ -27,6 +28,7 @@ from ui import ( ) from .. import complexity, phases, requirements +from ..compaction import format_phase_summaries, gather_phase_outputs, summarize_phase_output from ..validate_pkg.spec_validator import SpecValidator from .agent_runner import AgentRunner from .models import ( @@ -48,7 +50,8 @@ class SpecOrchestrator: spec_name: str | None = None, spec_dir: Path | None = None, # Use existing spec directory (for UI integration) - model: str = "claude-opus-4-5-20251101", + model: str = "claude-sonnet-4-5-20250929", + thinking_level: str = "medium", # Thinking level for extended thinking complexity_override: str | None = None, # Force a specific complexity use_ai_assessment: bool = True, # Use AI for complexity assessment (vs heuristics) dev_mode: bool = False, # Dev mode: specs in gitignored folder, code changes to auto-claude/ @@ -61,6 +64,7 @@ class SpecOrchestrator: spec_name: Optional spec name (for existing specs) spec_dir: Optional existing spec directory (for UI integration) model: The model to use for agent execution + thinking_level: Thinking level (none, low, medium, high, ultrathink) complexity_override: Force a specific complexity level use_ai_assessment: Whether to use AI for complexity assessment dev_mode: Deprecated, kept for API compatibility @@ -68,6 +72,7 @@ class SpecOrchestrator: self.project_dir = Path(project_dir) self.task_description = task_description self.model = model + self.thinking_level = thinking_level self.complexity_override = complexity_override self.use_ai_assessment = use_ai_assessment self.dev_mode = dev_mode @@ -96,6 +101,10 @@ class SpecOrchestrator: # Agent runner (initialized when needed) self._agent_runner: AgentRunner | None = None + # Phase summaries for conversation compaction + # Stores summaries from completed phases to provide context to subsequent phases + self._phase_summaries: dict[str, str] = {} + def _get_agent_runner(self) -> AgentRunner: """Get or create the agent runner. @@ -114,6 +123,7 @@ class SpecOrchestrator: prompt_file: str, additional_context: str = "", interactive: bool = False, + phase_name: str | None = None, ) -> tuple[bool, str]: """Run an agent with the given prompt. @@ -121,12 +131,55 @@ class SpecOrchestrator: prompt_file: The prompt file to use additional_context: Additional context to add interactive: Whether to run in interactive mode + phase_name: Name of the phase (for thinking budget lookup) Returns: Tuple of (success, response_text) """ runner = self._get_agent_runner() - return await runner.run_agent(prompt_file, additional_context, interactive) + + # Get thinking budget for this phase + thinking_budget = None + if phase_name: + thinking_budget = get_spec_phase_thinking_budget(phase_name) + + # Format prior phase summaries for context + prior_summaries = format_phase_summaries(self._phase_summaries) + + return await runner.run_agent( + prompt_file, + additional_context, + interactive, + thinking_budget=thinking_budget, + prior_phase_summaries=prior_summaries if prior_summaries else None, + ) + + async def _store_phase_summary(self, phase_name: str) -> None: + """Summarize and store phase output for subsequent phases. + + Args: + phase_name: Name of the completed phase + """ + try: + # Gather outputs from this phase + phase_output = await gather_phase_outputs(self.spec_dir, phase_name) + if not phase_output: + return + + # Summarize the output + summary = await summarize_phase_output( + phase_name, + phase_output, + model="claude-sonnet-4-5-20250929", # Use Sonnet for efficiency + target_words=500, + ) + + if summary: + self._phase_summaries[phase_name] = summary + + except Exception as e: + # Don't fail the pipeline if summarization fails + print_status(f"Phase summarization skipped: {e}", "warning") async def run(self, interactive: bool = True, auto_approve: bool = False) -> bool: """Run the spec creation process with dynamic phase selection. @@ -199,6 +252,8 @@ class SpecOrchestrator: LogPhase.PLANNING, success=False, message="Discovery failed" ) return False + # Store summary for subsequent phases (compaction) + await self._store_phase_summary("discovery") # === PHASE 2: REQUIREMENTS GATHERING === result = await run_phase( @@ -213,6 +268,8 @@ class SpecOrchestrator: message="Requirements gathering failed", ) return False + # Store summary for subsequent phases (compaction) + await self._store_phase_summary("requirements") # Rename spec folder with better name from requirements rename_spec_dir_from_requirements(self.spec_dir) @@ -275,6 +332,10 @@ class SpecOrchestrator: results.append(result) phases_executed.append(phase_name) + # Store summary for subsequent phases (compaction) + if result.success: + await self._store_phase_summary(phase_name) + if not result.success: print() print_status( diff --git a/auto-claude/task_logger/logger.py b/auto-claude/task_logger/logger.py index 9f5bad00..e0f84fa0 100644 --- a/auto-claude/task_logger/logger.py +++ b/auto-claude/task_logger/logger.py @@ -5,6 +5,8 @@ Main TaskLogger class for logging task execution. from datetime import datetime, timezone from pathlib import Path +from core.debug import debug, debug_error, debug_info, debug_success, is_debug_enabled + from .models import LogEntry, LogEntryType, LogPhase from .storage import LogStorage from .streaming import emit_marker @@ -62,6 +64,49 @@ class TaskLogger: """Add an entry to the current phase.""" self.storage.add_entry(entry) + def _debug_log( + self, + content: str, + entry_type: LogEntryType = LogEntryType.TEXT, + phase: str | None = None, + tool_name: str | None = None, + **kwargs, + ) -> None: + """ + Output a log entry to the terminal via the debug logging system. + + Only outputs when DEBUG=true is set in the environment. + + Args: + content: The message content + entry_type: Type of entry for formatting + phase: Current phase name + tool_name: Tool name if this is a tool log + **kwargs: Additional key-value pairs for debug output + """ + if not is_debug_enabled(): + return + + module = "task_logger" + prefix = f"[{phase or 'unknown'}]" if phase else "" + + if tool_name: + prefix = f"{prefix}[{tool_name}]" + + message = f"{prefix} {content}" if prefix else content + + # Route to appropriate debug function based on entry type + if entry_type == LogEntryType.ERROR: + debug_error(module, message, **kwargs) + elif entry_type == LogEntryType.SUCCESS: + debug_success(module, message, **kwargs) + elif entry_type in (LogEntryType.INFO, LogEntryType.PHASE_START, LogEntryType.PHASE_END): + debug_info(module, message, **kwargs) + elif entry_type in (LogEntryType.TOOL_START, LogEntryType.TOOL_END): + debug(module, message, level=2, **kwargs) + else: + debug(module, message, **kwargs) + def set_session(self, session: int) -> None: """Set the current session number.""" self.current_session = session @@ -110,15 +155,19 @@ class TaskLogger: self._emit("PHASE_START", {"phase": phase_key, "timestamp": self._timestamp()}) # Add phase start entry + phase_message = message or f"Starting {phase_key} phase" entry = LogEntry( timestamp=self._timestamp(), type=LogEntryType.PHASE_START.value, - content=message or f"Starting {phase_key} phase", + content=phase_message, phase=phase_key, session=self.current_session, ) self._add_entry(entry) + # Debug log (when DEBUG=true) + self._debug_log(phase_message, LogEntryType.PHASE_START, phase_key) + # Also print the message if message: print(message, flush=True) @@ -147,16 +196,20 @@ class TaskLogger: ) # Add phase end entry + phase_message = message or f"{'Completed' if success else 'Failed'} {phase_key} phase" entry = LogEntry( timestamp=self._timestamp(), type=LogEntryType.PHASE_END.value, - content=message - or f"{'Completed' if success else 'Failed'} {phase_key} phase", + content=phase_message, phase=phase_key, session=self.current_session, ) self._add_entry(entry) + # Debug log (when DEBUG=true) + entry_type = LogEntryType.SUCCESS if success else LogEntryType.ERROR + self._debug_log(phase_message, entry_type, phase_key) + if message: print(message, flush=True) @@ -205,6 +258,9 @@ class TaskLogger: }, ) + # Debug log (when DEBUG=true) + self._debug_log(content, entry_type, phase_key, subtask=self.current_subtask) + # Also print to console (unless caller handles printing) if print_to_console: print(content, flush=True) @@ -272,6 +328,16 @@ class TaskLogger: }, ) + # Debug log (when DEBUG=true) - include detail for verbose mode + self._debug_log( + content, + entry_type, + phase_key, + subtask=self.current_subtask, + subphase=subphase, + detail=detail[:500] + "..." if len(detail) > 500 else detail, + ) + if print_to_console: print(content, flush=True) @@ -308,6 +374,9 @@ class TaskLogger: {"subphase": subphase, "phase": phase_key, "timestamp": self._timestamp()}, ) + # Debug log (when DEBUG=true) + self._debug_log(f"Starting {subphase}", LogEntryType.INFO, phase_key, subphase=subphase) + if print_to_console: print(f"\n--- {subphase} ---", flush=True) @@ -352,6 +421,14 @@ class TaskLogger: {"name": tool_name, "input": display_input, "phase": phase_key}, ) + # Debug log (when DEBUG=true) + self._debug_log( + display_input or "started", + LogEntryType.TOOL_START, + phase_key, + tool_name=tool_name, + ) + if print_to_console: print(f"\n[Tool: {tool_name}]", flush=True) @@ -419,6 +496,18 @@ class TaskLogger: }, ) + # Debug log (when DEBUG=true) + debug_kwargs = {"status": status} + if display_result: + debug_kwargs["result"] = display_result + self._debug_log( + content, + LogEntryType.SUCCESS if success else LogEntryType.ERROR, + phase_key, + tool_name=tool_name, + **debug_kwargs, + ) + if print_to_console: if result: print(f" [{status}] {display_result}", flush=True) diff --git a/run.py/agent.py b/run.py/agent.py new file mode 100644 index 00000000..e69de29b