feat: introduce phase configuration module and enhance agent profiles
This commit adds a new phase configuration module that manages model and thinking level settings for different execution phases. It reads configurations from `task_metadata.json` and provides resolved model IDs for various phases, including spec creation, planning, coding, and QA. Key changes include: - New `phase_config.py` file to handle model ID mappings and thinking budgets. - Updates to agent files (`coder.py`, `planner.py`, `loop.py`) to utilize phase-specific models and thinking levels. - Modifications to the CLI and UI components to support per-phase configuration, enhancing the user experience for task creation and editing. The new structure allows for optimized model selection and thinking depth based on the phase, improving overall task execution efficiency. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, React.ElementType> = {
|
||||
Brain,
|
||||
Scale,
|
||||
Zap,
|
||||
Sparkles
|
||||
};
|
||||
|
||||
const PHASE_LABELS: Record<keyof PhaseModelConfig, { label: string; description: string }> = {
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
{/* Agent Profile Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-profile" className="text-sm font-medium text-foreground">
|
||||
Agent Profile
|
||||
</Label>
|
||||
<Select
|
||||
value={profileId}
|
||||
onValueChange={handleProfileSelect}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger id="agent-profile" className="h-10">
|
||||
<SelectValue>
|
||||
<div className="flex items-center gap-2">
|
||||
<display.icon className="h-4 w-4" />
|
||||
<span>{display.label}</span>
|
||||
</div>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DEFAULT_AGENT_PROFILES.map((profile) => {
|
||||
const ProfileIcon = iconMap[profile.icon || 'Scale'] || Scale;
|
||||
const modelLabel = AVAILABLE_MODELS.find(m => m.value === profile.model)?.label;
|
||||
return (
|
||||
<SelectItem key={profile.id} value={profile.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<ProfileIcon className="h-4 w-4 shrink-0" />
|
||||
<div>
|
||||
<span className="font-medium">{profile.name}</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
{profile.isAutoProfile
|
||||
? '(per-phase optimization)'
|
||||
: `(${modelLabel} + ${profile.thinkingLevel})`
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
<SelectItem value="custom">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sliders className="h-4 w-4 shrink-0" />
|
||||
<div>
|
||||
<span className="font-medium">Custom</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
(Choose model & thinking level)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{display.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Auto Profile - Phase Configuration */}
|
||||
{isAuto && (
|
||||
<div className="space-y-3 rounded-lg border border-border bg-muted/30 p-4">
|
||||
{/* Phase Summary */}
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPhaseDetails(!showPhaseDetails)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between text-sm',
|
||||
'text-muted-foreground hover:text-foreground transition-colors'
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className="font-medium text-foreground">Phase Configuration</span>
|
||||
{showPhaseDetails ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Compact summary when collapsed */}
|
||||
{!showPhaseDetails && (
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
{(Object.keys(PHASE_LABELS) as Array<keyof PhaseModelConfig>).map((phase) => {
|
||||
const modelLabel = AVAILABLE_MODELS.find(m => m.value === currentPhaseModels[phase])?.label?.replace('Claude ', '') || currentPhaseModels[phase];
|
||||
return (
|
||||
<div key={phase} className="flex items-center justify-between rounded bg-background/50 px-2 py-1">
|
||||
<span className="text-muted-foreground">{PHASE_LABELS[phase].label}:</span>
|
||||
<span className="font-medium">{modelLabel}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detailed Phase Configuration */}
|
||||
{showPhaseDetails && (
|
||||
<div className="space-y-4 pt-2">
|
||||
{(Object.keys(PHASE_LABELS) as Array<keyof PhaseModelConfig>).map((phase) => (
|
||||
<div key={phase} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-medium text-muted-foreground">
|
||||
{PHASE_LABELS[phase].label}
|
||||
</Label>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{PHASE_LABELS[phase].description}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Select
|
||||
value={currentPhaseModels[phase]}
|
||||
onValueChange={(value) => handlePhaseModelChange(phase, value as ModelType)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AVAILABLE_MODELS.map((m) => (
|
||||
<SelectItem key={m.value} value={m.value}>
|
||||
{m.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={currentPhaseThinking[phase]}
|
||||
onValueChange={(value) => handlePhaseThinkingChange(phase, value as ThinkingLevel)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{THINKING_LEVELS.map((level) => (
|
||||
<SelectItem key={level.value} value={level.value}>
|
||||
{level.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Configuration (shown only when custom is selected) */}
|
||||
{isCustom && (
|
||||
<div className="space-y-4 rounded-lg border border-border bg-muted/30 p-4">
|
||||
{/* Model Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom-model" className="text-xs font-medium text-muted-foreground">
|
||||
Model
|
||||
</Label>
|
||||
<Select
|
||||
value={model}
|
||||
onValueChange={(value) => onModelChange(value as ModelType)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger id="custom-model" className="h-9">
|
||||
<SelectValue placeholder="Select model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AVAILABLE_MODELS.map((m) => (
|
||||
<SelectItem key={m.value} value={m.value}>
|
||||
{m.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Thinking Level Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom-thinking" className="text-xs font-medium text-muted-foreground">
|
||||
Thinking Level
|
||||
</Label>
|
||||
<Select
|
||||
value={thinkingLevel}
|
||||
onValueChange={(value) => onThinkingLevelChange(value as ThinkingLevel)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger id="custom-thinking" className="h-9">
|
||||
<SelectValue placeholder="Select thinking level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{THINKING_LEVELS.map((level) => (
|
||||
<SelectItem key={level.value} value={level.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{level.label}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
- {level.description}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<TaskImpact | ''>('');
|
||||
|
||||
// Model configuration (initialized from selected agent profile)
|
||||
const [profileId, setProfileId] = useState<string>(settings.selectedAgentProfile || 'auto');
|
||||
const [model, setModel] = useState<ModelType | ''>(selectedProfile.model);
|
||||
const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevel | ''>(selectedProfile.thinkingLevel);
|
||||
// Auto profile - per-phase configuration
|
||||
const [phaseModels, setPhaseModels] = useState<PhaseModelConfig | undefined>(
|
||||
selectedProfile.phaseModels || DEFAULT_PHASE_MODELS
|
||||
);
|
||||
const [phaseThinking, setPhaseThinking] = useState<PhaseThinkingConfig | undefined>(
|
||||
selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING
|
||||
);
|
||||
|
||||
// Image attachments
|
||||
const [images, setImages] = useState<ImageAttachment[]>([]);
|
||||
@@ -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({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Model Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="model" className="text-sm font-medium text-foreground">
|
||||
Model
|
||||
</Label>
|
||||
<Select
|
||||
value={model}
|
||||
onValueChange={(value) => setModel(value as ModelType)}
|
||||
{/* Agent Profile Selection */}
|
||||
<AgentProfileSelector
|
||||
profileId={profileId}
|
||||
model={model}
|
||||
thinkingLevel={thinkingLevel}
|
||||
phaseModels={phaseModels}
|
||||
phaseThinking={phaseThinking}
|
||||
onProfileChange={(newProfileId, newModel, newThinkingLevel) => {
|
||||
setProfileId(newProfileId);
|
||||
setModel(newModel);
|
||||
setThinkingLevel(newThinkingLevel);
|
||||
}}
|
||||
onModelChange={setModel}
|
||||
onThinkingLevelChange={setThinkingLevel}
|
||||
onPhaseModelsChange={setPhaseModels}
|
||||
onPhaseThinkingChange={setPhaseThinking}
|
||||
disabled={isCreating}
|
||||
>
|
||||
<SelectTrigger id="model" className="h-9">
|
||||
<SelectValue placeholder="Select model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AVAILABLE_MODELS.map((m) => (
|
||||
<SelectItem key={m.value} value={m.value}>
|
||||
{m.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The Claude model to use for this task. Defaults to your selected agent profile.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Thinking Level Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="thinking-level" className="text-sm font-medium text-foreground">
|
||||
Thinking Level
|
||||
</Label>
|
||||
<Select
|
||||
value={thinkingLevel}
|
||||
onValueChange={(value) => setThinkingLevel(value as ThinkingLevel)}
|
||||
disabled={isCreating}
|
||||
>
|
||||
<SelectTrigger id="thinking-level" className="h-9">
|
||||
<SelectValue placeholder="Select thinking level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{THINKING_LEVELS.map((level) => (
|
||||
<SelectItem key={level.value} value={level.value}>
|
||||
{level.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Extended thinking depth for complex reasoning. Higher levels use more tokens but provide deeper analysis.
|
||||
</p>
|
||||
</div>
|
||||
/>
|
||||
|
||||
{/* Paste Success Indicator */}
|
||||
{pasteSuccess && (
|
||||
|
||||
@@ -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<TaskComplexity | ''>(task.metadata?.complexity || '');
|
||||
const [impact, setImpact] = useState<TaskImpact | ''>(task.metadata?.impact || '');
|
||||
|
||||
// Agent profile / model configuration
|
||||
const [profileId, setProfileId] = useState<string>(() => {
|
||||
// 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<ModelType | ''>(task.metadata?.model || selectedProfile.model);
|
||||
const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevel | ''>(
|
||||
task.metadata?.thinkingLevel || selectedProfile.thinkingLevel
|
||||
);
|
||||
// Auto profile - per-phase configuration
|
||||
const [phaseModels, setPhaseModels] = useState<PhaseModelConfig | undefined>(
|
||||
task.metadata?.phaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS
|
||||
);
|
||||
const [phaseThinking, setPhaseThinking] = useState<PhaseThinkingConfig | undefined>(
|
||||
task.metadata?.phaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING
|
||||
);
|
||||
|
||||
// Image attachments
|
||||
const [images, setImages] = useState<ImageAttachment[]>(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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Agent Profile Selection */}
|
||||
<AgentProfileSelector
|
||||
profileId={profileId}
|
||||
model={model}
|
||||
thinkingLevel={thinkingLevel}
|
||||
phaseModels={phaseModels}
|
||||
phaseThinking={phaseThinking}
|
||||
onProfileChange={(newProfileId, newModel, newThinkingLevel) => {
|
||||
setProfileId(newProfileId);
|
||||
setModel(newModel);
|
||||
setThinkingLevel(newThinkingLevel);
|
||||
}}
|
||||
onModelChange={setModel}
|
||||
onThinkingLevelChange={setThinkingLevel}
|
||||
onPhaseModelsChange={setPhaseModels}
|
||||
onPhaseThinkingChange={setPhaseThinking}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
|
||||
{/* Paste Success Indicator */}
|
||||
{pasteSuccess && (
|
||||
<div className="flex items-center gap-2 text-sm text-success animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
|
||||
@@ -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<TaskLogPhase, string> = {
|
||||
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<TaskLogPhase, keyof PhaseModelConfig> = {
|
||||
planning: 'spec', // Planning log phase primarily shows spec creation
|
||||
coding: 'coding',
|
||||
validation: 'qa'
|
||||
};
|
||||
|
||||
// Short labels for models
|
||||
const MODEL_SHORT_LABELS: Record<ModelTypeShort, string> = {
|
||||
opus: 'Opus',
|
||||
sonnet: 'Sonnet',
|
||||
haiku: 'Haiku'
|
||||
};
|
||||
|
||||
// Short labels for thinking levels
|
||||
const THINKING_SHORT_LABELS: Record<ThinkingLevel, string> = {
|
||||
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)}
|
||||
/>
|
||||
))}
|
||||
<div ref={logsEndRef} />
|
||||
@@ -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 }:
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Model and thinking level indicator */}
|
||||
{phaseConfig && (
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
||||
<div className="flex items-center gap-0.5" title={`Model: ${phaseConfig.model}`}>
|
||||
<Cpu className="h-3 w-3" />
|
||||
<span>{phaseConfig.model}</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground/50">|</span>
|
||||
<div className="flex items-center gap-0.5" title={`Thinking: ${phaseConfig.thinking}`}>
|
||||
<Brain className="h-3 w-3" />
|
||||
<span>{phaseConfig.thinking}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{getStatusBadge()}
|
||||
</div>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
|
||||
@@ -48,70 +48,94 @@ 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({
|
||||
// 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
|
||||
});
|
||||
}
|
||||
setGenerationStatus({
|
||||
// Only update if this is for the currently viewed project
|
||||
if (isCurrentProject(projectId)) {
|
||||
useRoadmapStore.getState().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
|
||||
});
|
||||
}
|
||||
setGenerationStatus({
|
||||
// Only update if this is for the currently viewed project
|
||||
if (isCurrentProject(projectId)) {
|
||||
useRoadmapStore.getState().setGenerationStatus({
|
||||
phase: 'idle',
|
||||
progress: 0,
|
||||
message: 'Generation stopped'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Terminal rate limit listener
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
+33
-7
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ""
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user