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