Improvement to let claude code decide parallell agents

This commit is contained in:
AndyMik90
2025-12-14 11:46:55 +01:00
parent d192bc2e6b
commit c7f8d98c19
21 changed files with 1095 additions and 1520 deletions
-63
View File
@@ -1,63 +0,0 @@
# Subtask Worker Agent
You are a focused implementation agent working on a single subtask within a larger feature build. Your job is to implement ONLY your assigned subtask, commit your changes, and report completion.
## Core Responsibilities
1. **Read** the subtask specification carefully
2. **Implement** ONLY the assigned subtask - nothing more, nothing less
3. **Verify** your implementation works (tests pass, build succeeds)
4. **Commit** your changes with a clear commit message
5. **Update** the implementation plan to mark the subtask as completed
## Important Constraints
### Stay Focused
- You are part of a **parallel build** - other agents may be working on other subtasks
- **DO NOT** modify files outside your subtask's scope
- **DO NOT** implement other subtasks, even if they seem related
- **DO NOT** refactor code unless explicitly required by your subtask
### File Boundaries
- Your subtask specifies which files to modify/create
- Stick to those files unless absolutely necessary
- If you need to touch other files, they should be minimal changes (imports, exports)
### Commit Strategy
- Make atomic commits for your subtask
- Use clear commit messages: `feat(subtask-id): description`
- Commit after verification passes
## Workflow
```
1. Read subtask specification
└─> Understand what needs to be implemented
2. Implement the subtask
└─> Write code following existing patterns
└─> Keep changes minimal and focused
3. Verify the implementation
└─> Run relevant tests
└─> Check for lint errors
└─> Ensure build succeeds
4. Commit changes
└─> git add <your files>
└─> git commit -m "feat(subtask-id): description"
5. Update implementation plan
└─> Mark subtask status as "completed"
└─> Save the updated plan
```
## Communication
- Report progress through the implementation plan JSON
- If you encounter blockers, update the subtask status to indicate the issue
- Keep your response focused - avoid lengthy explanations
## Your Assigned Subtask
[The orchestrator will inject the specific subtask details here at runtime]
+3 -7
View File
@@ -33,9 +33,6 @@ python auto-claude/spec_runner.py --task "Fix button" --complexity simple
# Run autonomous build
python auto-claude/run.py --spec 001
# Run with parallel workers
python auto-claude/run.py --spec 001 --parallel 2
# List all specs
python auto-claude/run.py --list
```
@@ -92,7 +89,7 @@ python auto-claude/validate_spec.py --spec-dir auto-claude/specs/001-feature --c
**Implementation (run.py → agent.py)** - Multi-session build:
1. Planner Agent creates subtask-based implementation plan
2. Coder Agent implements subtasks (sequential or parallel via Task tool)
2. Coder Agent implements subtasks (can spawn subagents for parallel work)
3. QA Reviewer validates acceptance criteria
4. QA Fixer resolves issues in a loop
@@ -101,7 +98,6 @@ python auto-claude/validate_spec.py --spec-dir auto-claude/specs/001-feature --c
- **client.py** - Claude SDK client with security hooks and tool permissions
- **security.py** + **project_analyzer.py** - Dynamic command allowlisting based on detected project stack
- **worktree.py** - Git worktree isolation for safe feature development
- **task_tool.py** - Parallel subtask execution using Claude Code's Task tool
- **memory.py** - File-based session memory (primary, always-available storage)
- **graphiti_memory.py** - Optional graph-based cross-session memory with semantic search
- **graphiti_providers.py** - Multi-provider factory for Graphiti (OpenAI, Anthropic, Azure, Ollama)
@@ -144,14 +140,14 @@ main (user's branch)
**Key principles:**
- ONE branch per spec (`auto-claude/{spec-name}`)
- Parallel execution uses Claude Code's Task tool (not separate branches)
- Parallel work uses subagents (agent decides when to spawn)
- NO automatic pushes to GitHub - user controls when to push
- User reviews in spec worktree (`.worktrees/{spec-name}/`)
- Final merge: spec branch → main (after user approval)
**Workflow:**
1. Build runs in isolated worktree on spec branch
2. Parallel subtasks execute via Task tool (same branch)
2. Agent implements subtasks (can spawn subagents for parallel work)
3. User tests feature in `.worktrees/{spec-name}/`
4. User runs `--merge` to add to their project
5. User pushes to remote when ready
+47 -16
View File
@@ -2991,27 +2991,58 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join('\n'
.slice(0, 10); // Last 10 specs
for (const specDir of recentSpecDirs) {
// Look for session memory files
const memoryDir = path.join(specsDir, specDir, 'memory');
if (existsSync(memoryDir)) {
const memoryFiles = readdirSync(memoryDir)
.filter((f: string) => f.endsWith('.json'))
.sort()
.reverse();
// Load session insights from session_insights subdirectory
const sessionInsightsDir = path.join(memoryDir, 'session_insights');
if (existsSync(sessionInsightsDir)) {
const sessionFiles = readdirSync(sessionInsightsDir)
.filter((f: string) => f.startsWith('session_') && f.endsWith('.json'))
.sort()
.reverse();
for (const memFile of memoryFiles.slice(0, 3)) {
for (const sessionFile of sessionFiles.slice(0, 3)) {
try {
const sessionPath = path.join(sessionInsightsDir, sessionFile);
const sessionContent = readFileSync(sessionPath, 'utf-8');
const sessionData = JSON.parse(sessionContent);
// Session files have: session_number, timestamp, subtasks_completed,
// discoveries, what_worked, what_failed, recommendations_for_next_session
if (sessionData.session_number !== undefined) {
recentMemories.push({
id: `${specDir}-${sessionFile}`,
type: 'session_insight',
timestamp: sessionData.timestamp || new Date().toISOString(),
content: JSON.stringify({
discoveries: sessionData.discoveries,
what_worked: sessionData.what_worked,
what_failed: sessionData.what_failed,
recommendations: sessionData.recommendations_for_next_session,
subtasks_completed: sessionData.subtasks_completed
}, null, 2),
session_number: sessionData.session_number
});
}
} catch {
// Skip invalid files
}
}
}
// Also load codebase_map.json as a memory item
const codebaseMapPath = path.join(memoryDir, 'codebase_map.json');
if (existsSync(codebaseMapPath)) {
try {
const memPath = path.join(memoryDir, memFile);
const memContent = readFileSync(memPath, 'utf-8');
const memData = JSON.parse(memContent);
if (memData.insights) {
const mapContent = readFileSync(codebaseMapPath, 'utf-8');
const mapData = JSON.parse(mapContent);
if (mapData.discovered_files && Object.keys(mapData.discovered_files).length > 0) {
recentMemories.push({
id: `${specDir}-${memFile}`,
type: 'session_insight',
timestamp: memData.timestamp || new Date().toISOString(),
content: JSON.stringify(memData.insights, null, 2),
session_number: memData.session_number
id: `${specDir}-codebase_map`,
type: 'codebase_map',
timestamp: mapData.last_updated || new Date().toISOString(),
content: JSON.stringify(mapData.discovered_files, null, 2),
session_number: undefined
});
}
} catch {
@@ -14,16 +14,22 @@ import {
Key,
Eye,
EyeOff,
Info
Info,
Palette,
Bot,
FolderOpen,
Bell,
Package
} from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from './ui/dialog';
FullScreenDialog,
FullScreenDialogContent,
FullScreenDialogHeader,
FullScreenDialogBody,
FullScreenDialogFooter,
FullScreenDialogTitle,
FullScreenDialogDescription
} from './ui/full-screen-dialog';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
@@ -37,6 +43,7 @@ import {
SelectValue
} from './ui/select';
import { Separator } from './ui/separator';
import { cn } from '../lib/utils';
import { useSettingsStore, saveSettings, loadSettings } from '../stores/settings-store';
import { AVAILABLE_MODELS } from '../../shared/constants';
import type {
@@ -51,12 +58,31 @@ interface AppSettingsDialogProps {
onOpenChange: (open: boolean) => void;
}
type SettingsSection = 'appearance' | 'agent' | 'paths' | 'api-keys' | 'framework' | 'notifications';
interface NavItem {
id: SettingsSection;
label: string;
icon: React.ElementType;
description: string;
}
const navItems: NavItem[] = [
{ id: 'appearance', label: 'Appearance', icon: Palette, description: 'Theme and visual preferences' },
{ id: 'agent', label: 'Agent Settings', icon: Bot, description: 'Default model and parallelism' },
{ id: 'paths', label: 'Paths', icon: FolderOpen, description: 'Python and framework paths' },
{ id: 'api-keys', label: 'API Keys', icon: Key, description: 'Global API credentials' },
{ id: 'framework', label: 'Framework', icon: Package, description: 'Auto Claude updates' },
{ id: 'notifications', label: 'Notifications', icon: Bell, description: 'Alert preferences' }
];
export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps) {
const currentSettings = useSettingsStore((state) => state.settings);
const [settings, setSettings] = useState<AppSettingsType>(currentSettings);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [version, setVersion] = useState<string>('');
const [activeSection, setActiveSection] = useState<SettingsSection>('appearance');
// Auto Claude source update state
const [sourceUpdateCheck, setSourceUpdateCheck] = useState<AutoBuildSourceUpdateCheck | null>(null);
@@ -164,82 +190,59 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px] max-h-[90vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2 text-foreground">
<Settings className="h-5 w-5" />
Application Settings
</DialogTitle>
<DialogDescription>
Configure global application settings
</DialogDescription>
</DialogHeader>
<ScrollArea className="flex-1 -mx-6 px-6 min-h-0">
<div className="py-4 space-y-6">
{/* Appearance */}
<section className="space-y-4">
<h3 className="text-sm font-semibold text-foreground">Appearance</h3>
<div className="space-y-2">
<Label htmlFor="theme" className="text-sm font-medium text-foreground">Theme</Label>
<Select
value={settings.theme}
onValueChange={(value: 'light' | 'dark' | 'system') =>
setSettings({ ...settings, theme: value })
}
>
<SelectTrigger id="theme">
<SelectValue>
<span className="flex items-center gap-2">
{getThemeIcon(settings.theme)}
{settings.theme === 'system'
? 'System'
: settings.theme === 'dark'
? 'Dark'
: 'Light'}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="system">
<span className="flex items-center gap-2">
<Monitor className="h-4 w-4" />
System
</span>
</SelectItem>
<SelectItem value="light">
<span className="flex items-center gap-2">
<Sun className="h-4 w-4" />
Light
</span>
</SelectItem>
<SelectItem value="dark">
<span className="flex items-center gap-2">
<Moon className="h-4 w-4" />
Dark
</span>
</SelectItem>
</SelectContent>
</Select>
</div>
</section>
const renderSection = () => {
switch (activeSection) {
case 'appearance':
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-foreground mb-1">Appearance</h3>
<p className="text-sm text-muted-foreground">Customize how Auto Claude looks</p>
</div>
<Separator />
<div className="space-y-4">
<div className="space-y-3">
<Label htmlFor="theme" className="text-sm font-medium text-foreground">Theme</Label>
<p className="text-sm text-muted-foreground">Choose your preferred color scheme</p>
<div className="grid grid-cols-3 gap-3">
{(['system', 'light', 'dark'] as const).map((theme) => (
<button
key={theme}
onClick={() => setSettings({ ...settings, theme })}
className={cn(
'flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all',
settings.theme === theme
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50 hover:bg-accent/50'
)}
>
{getThemeIcon(theme)}
<span className="text-sm font-medium capitalize">{theme}</span>
</button>
))}
</div>
</div>
</div>
</div>
);
{/* Default Agent Settings */}
<section className="space-y-4">
<h3 className="text-sm font-semibold text-foreground">Default Agent Settings</h3>
<div className="space-y-2">
case 'agent':
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-foreground mb-1">Default Agent Settings</h3>
<p className="text-sm text-muted-foreground">Configure defaults for new projects</p>
</div>
<Separator />
<div className="space-y-6">
<div className="space-y-3">
<Label htmlFor="defaultModel" className="text-sm font-medium text-foreground">Default Model</Label>
<p className="text-sm text-muted-foreground">The AI model used for agent tasks</p>
<Select
value={settings.defaultModel}
onValueChange={(value) =>
setSettings({ ...settings, defaultModel: value })
}
onValueChange={(value) => setSettings({ ...settings, defaultModel: value })}
>
<SelectTrigger id="defaultModel">
<SelectTrigger id="defaultModel" className="w-full max-w-md">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -251,13 +254,15 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<div className="space-y-3">
<Label htmlFor="defaultParallelism" className="text-sm font-medium text-foreground">Default Parallelism</Label>
<p className="text-sm text-muted-foreground">Number of concurrent agent workers (1-8)</p>
<Input
id="defaultParallelism"
type="number"
min={1}
max={8}
className="w-full max-w-md"
value={settings.defaultParallelism}
onChange={(e) =>
setSettings({
@@ -267,64 +272,70 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
}
/>
</div>
</section>
</div>
</div>
);
case 'paths':
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-foreground mb-1">Paths</h3>
<p className="text-sm text-muted-foreground">Configure executable and framework paths</p>
</div>
<Separator />
{/* Paths */}
<section className="space-y-4">
<h3 className="text-sm font-semibold text-foreground">Paths</h3>
<div className="space-y-2">
<div className="space-y-6">
<div className="space-y-3">
<Label htmlFor="pythonPath" className="text-sm font-medium text-foreground">Python Path</Label>
<p className="text-sm text-muted-foreground">Path to Python executable (leave empty for default)</p>
<Input
id="pythonPath"
placeholder="python3 (default)"
className="w-full max-w-lg"
value={settings.pythonPath || ''}
onChange={(e) =>
setSettings({ ...settings, pythonPath: e.target.value })
}
onChange={(e) => setSettings({ ...settings, pythonPath: e.target.value })}
/>
<p className="text-xs text-muted-foreground">
Path to Python executable (leave empty for default)
</p>
</div>
<div className="space-y-2">
<div className="space-y-3">
<Label htmlFor="autoBuildPath" className="text-sm font-medium text-foreground">Auto Claude Path</Label>
<p className="text-sm text-muted-foreground">Relative path to auto-claude directory in projects</p>
<Input
id="autoBuildPath"
placeholder="auto-claude (default)"
className="w-full max-w-lg"
value={settings.autoBuildPath || ''}
onChange={(e) =>
setSettings({ ...settings, autoBuildPath: e.target.value })
}
onChange={(e) => setSettings({ ...settings, autoBuildPath: e.target.value })}
/>
<p className="text-xs text-muted-foreground">
Relative path to auto-claude directory in projects
</div>
</div>
</div>
);
case 'api-keys':
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-foreground mb-1">Global API Keys</h3>
<p className="text-sm text-muted-foreground">Set API keys to use across all projects</p>
</div>
<Separator />
<div className="rounded-lg bg-info/10 border border-info/30 p-4 mb-6">
<div className="flex items-start gap-3">
<Info className="h-5 w-5 text-info flex-shrink-0 mt-0.5" />
<p className="text-sm text-muted-foreground">
Keys set here will be used as defaults. Individual projects can override these in their settings.
</p>
</div>
</section>
<Separator />
{/* Global API Keys */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<Key className="h-4 w-4" />
<h3 className="text-sm font-semibold text-foreground">Global API Keys</h3>
</div>
<div className="rounded-lg bg-info/10 border border-info/30 p-3">
<div className="flex items-start gap-2">
<Info className="h-4 w-4 text-info flex-shrink-0 mt-0.5" />
<p className="text-xs text-muted-foreground">
Set API keys here to use them across all projects. Individual projects can override these in their settings.
</p>
</div>
</div>
<div className="space-y-2">
</div>
<div className="space-y-6">
<div className="space-y-3">
<Label htmlFor="globalClaudeToken" className="text-sm font-medium text-foreground">
Claude OAuth Token
</Label>
<div className="relative">
<p className="text-sm text-muted-foreground">
Get your token by running <code className="px-1.5 py-0.5 bg-muted rounded font-mono text-xs">claude setup-token</code>
</p>
<div className="relative max-w-lg">
<Input
id="globalClaudeToken"
type={showGlobalClaudeToken ? 'text' : 'password'}
@@ -343,15 +354,15 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
{showGlobalClaudeToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-xs text-muted-foreground">
Get your token by running <code className="px-1 py-0.5 bg-muted rounded font-mono">claude setup-token</code>
</p>
</div>
<div className="space-y-2">
<div className="space-y-3">
<Label htmlFor="globalOpenAIKey" className="text-sm font-medium text-foreground">
OpenAI API Key
</Label>
<div className="relative">
<p className="text-sm text-muted-foreground">
Required for Graphiti memory backend (embeddings)
</p>
<div className="relative max-w-lg">
<Input
id="globalOpenAIKey"
type={showGlobalOpenAIKey ? 'text' : 'password'}
@@ -370,57 +381,60 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
{showGlobalOpenAIKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-xs text-muted-foreground">
Required for Graphiti memory backend (embeddings)
</p>
</div>
</section>
</div>
</div>
);
case 'framework':
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-foreground mb-1">Auto Claude Framework</h3>
<p className="text-sm text-muted-foreground">Manage framework updates and settings</p>
</div>
<Separator />
{/* Auto Claude Framework Updates */}
<section className="space-y-4">
<h3 className="text-sm font-semibold text-foreground">Auto Claude Framework</h3>
<div className="rounded-lg border border-border bg-muted/50 p-4 space-y-3">
<div className="space-y-6">
<div className="rounded-lg border border-border bg-muted/50 p-5 space-y-4">
{isCheckingSourceUpdate ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin" />
Checking for updates...
</div>
) : sourceUpdateCheck ? (
<>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-foreground">
Current Version: {sourceUpdateCheck.currentVersion}
<p className="text-base font-medium text-foreground">
Version {sourceUpdateCheck.currentVersion}
</p>
{sourceUpdateCheck.latestVersion && sourceUpdateCheck.updateAvailable && (
<p className="text-xs text-info">
<p className="text-sm text-info mt-1">
New version available: {sourceUpdateCheck.latestVersion}
</p>
)}
</div>
{sourceUpdateCheck.updateAvailable ? (
<AlertCircle className="h-5 w-5 text-info" />
<AlertCircle className="h-6 w-6 text-info" />
) : (
<CheckCircle2 className="h-5 w-5 text-success" />
<CheckCircle2 className="h-6 w-6 text-success" />
)}
</div>
{sourceUpdateCheck.error && (
<p className="text-xs text-destructive">{sourceUpdateCheck.error}</p>
<p className="text-sm text-destructive">{sourceUpdateCheck.error}</p>
)}
{!sourceUpdateCheck.updateAvailable && !sourceUpdateCheck.error && (
<p className="text-xs text-muted-foreground">
<p className="text-sm text-muted-foreground">
You're running the latest version of the Auto Claude framework.
</p>
)}
{sourceUpdateCheck.updateAvailable && (
<div className="space-y-3">
<div className="space-y-4 pt-2">
{sourceUpdateCheck.releaseNotes && (
<div className="text-xs text-muted-foreground bg-background rounded p-2 max-h-24 overflow-y-auto">
<div className="text-sm text-muted-foreground bg-background rounded-lg p-3 max-h-32 overflow-y-auto">
<pre className="whitespace-pre-wrap font-sans">
{sourceUpdateCheck.releaseNotes}
</pre>
@@ -428,8 +442,8 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
)}
{isDownloadingUpdate ? (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<div className="space-y-3">
<div className="flex items-center gap-3 text-sm">
<RefreshCw className="h-4 w-4 animate-spin" />
<span>{downloadProgress?.message || 'Downloading...'}</span>
</div>
@@ -438,20 +452,17 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
)}
</div>
) : downloadProgress?.stage === 'complete' ? (
<div className="flex items-center gap-2 text-sm text-success">
<CheckCircle2 className="h-4 w-4" />
<div className="flex items-center gap-3 text-sm text-success">
<CheckCircle2 className="h-5 w-5" />
<span>{downloadProgress.message}</span>
</div>
) : downloadProgress?.stage === 'error' ? (
<div className="flex items-center gap-2 text-sm text-destructive">
<AlertCircle className="h-4 w-4" />
<div className="flex items-center gap-3 text-sm text-destructive">
<AlertCircle className="h-5 w-5" />
<span>{downloadProgress.message}</span>
</div>
) : (
<Button
size="sm"
onClick={handleDownloadSourceUpdate}
>
<Button onClick={handleDownloadSourceUpdate}>
<CloudDownload className="mr-2 h-4 w-4" />
Download Update
</Button>
@@ -460,33 +471,29 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
)}
</>
) : (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<AlertCircle className="h-4 w-4" />
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<AlertCircle className="h-5 w-5" />
Unable to check for updates
</div>
)}
<div className="flex items-center gap-2">
<div className="pt-2">
<Button
size="sm"
variant="outline"
onClick={checkForSourceUpdates}
disabled={isCheckingSourceUpdate}
>
<RefreshCw className={`mr-2 h-3 w-3 ${isCheckingSourceUpdate ? 'animate-spin' : ''}`} />
<RefreshCw className={cn('mr-2 h-4 w-4', isCheckingSourceUpdate && 'animate-spin')} />
Check for Updates
</Button>
</div>
<p className="text-xs text-muted-foreground">
Updates the bundled Auto Claude framework from GitHub. Individual projects can then be updated from Project Settings.
</p>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="font-normal text-foreground">Auto-Update Projects</Label>
<p className="text-xs text-muted-foreground">
<div className="flex items-center justify-between p-4 rounded-lg border border-border">
<div className="space-y-1">
<Label className="font-medium text-foreground">Auto-Update Projects</Label>
<p className="text-sm text-muted-foreground">
Automatically update Auto Claude in projects when a new version is available
</p>
</div>
@@ -497,94 +504,120 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
}
/>
</div>
</section>
<Separator />
{/* Notifications */}
<section className="space-y-4">
<h3 className="text-sm font-semibold text-foreground">Default Notifications</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="font-normal text-foreground">On Task Complete</Label>
<Switch
checked={settings.notifications.onTaskComplete}
onCheckedChange={(checked) =>
setSettings({
...settings,
notifications: {
...settings.notifications,
onTaskComplete: checked
}
})
}
/>
</div>
<div className="flex items-center justify-between">
<Label className="font-normal text-foreground">On Task Failed</Label>
<Switch
checked={settings.notifications.onTaskFailed}
onCheckedChange={(checked) =>
setSettings({
...settings,
notifications: {
...settings.notifications,
onTaskFailed: checked
}
})
}
/>
</div>
<div className="flex items-center justify-between">
<Label className="font-normal text-foreground">On Review Needed</Label>
<Switch
checked={settings.notifications.onReviewNeeded}
onCheckedChange={(checked) =>
setSettings({
...settings,
notifications: {
...settings.notifications,
onReviewNeeded: checked
}
})
}
/>
</div>
<div className="flex items-center justify-between">
<Label className="font-normal text-foreground">Sound</Label>
<Switch
checked={settings.notifications.sound}
onCheckedChange={(checked) =>
setSettings({
...settings,
notifications: {
...settings.notifications,
sound: checked
}
})
}
/>
</div>
</div>
</section>
{/* Error */}
{error && (
<div className="rounded-lg bg-[var(--error-light)] border border-[var(--error)]/30 p-3 text-sm text-[var(--error)]">
{error}
</div>
)}
{/* Version */}
{version && (
<div className="text-xs text-muted-foreground text-center pt-2">
Version {version}
</div>
)}
</div>
</div>
</ScrollArea>
);
<DialogFooter className="flex-shrink-0">
case 'notifications':
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-foreground mb-1">Notifications</h3>
<p className="text-sm text-muted-foreground">Configure default notification preferences</p>
</div>
<Separator />
<div className="space-y-4">
{[
{ key: 'onTaskComplete', label: 'On Task Complete', description: 'Notify when a task finishes successfully' },
{ key: 'onTaskFailed', label: 'On Task Failed', description: 'Notify when a task encounters an error' },
{ key: 'onReviewNeeded', label: 'On Review Needed', description: 'Notify when QA requires your review' },
{ key: 'sound', label: 'Sound', description: 'Play sound with notifications' }
].map((item) => (
<div key={item.key} className="flex items-center justify-between p-4 rounded-lg border border-border">
<div className="space-y-1">
<Label className="font-medium text-foreground">{item.label}</Label>
<p className="text-sm text-muted-foreground">{item.description}</p>
</div>
<Switch
checked={settings.notifications[item.key as keyof typeof settings.notifications]}
onCheckedChange={(checked) =>
setSettings({
...settings,
notifications: {
...settings.notifications,
[item.key]: checked
}
})
}
/>
</div>
))}
</div>
</div>
);
}
};
return (
<FullScreenDialog open={open} onOpenChange={onOpenChange}>
<FullScreenDialogContent>
<FullScreenDialogHeader>
<FullScreenDialogTitle className="flex items-center gap-3">
<Settings className="h-6 w-6" />
Settings
</FullScreenDialogTitle>
<FullScreenDialogDescription>
Configure application-wide settings and preferences
</FullScreenDialogDescription>
</FullScreenDialogHeader>
<FullScreenDialogBody>
<div className="flex h-full">
{/* Navigation sidebar */}
<nav className="w-64 border-r border-border bg-muted/30 p-4">
<ScrollArea className="h-full">
<div className="space-y-1">
{navItems.map((item) => {
const Icon = item.icon;
return (
<button
key={item.id}
onClick={() => setActiveSection(item.id)}
className={cn(
'w-full flex items-start gap-3 p-3 rounded-lg text-left transition-all',
activeSection === item.id
? 'bg-accent text-accent-foreground'
: 'hover:bg-accent/50 text-muted-foreground hover:text-foreground'
)}
>
<Icon className="h-5 w-5 mt-0.5 shrink-0" />
<div className="min-w-0">
<div className="font-medium text-sm">{item.label}</div>
<div className="text-xs text-muted-foreground truncate">{item.description}</div>
</div>
</button>
);
})}
</div>
{/* Version at bottom */}
{version && (
<div className="mt-8 pt-4 border-t border-border">
<p className="text-xs text-muted-foreground text-center">
Version {version}
</p>
</div>
)}
</ScrollArea>
</nav>
{/* Main content */}
<div className="flex-1 overflow-hidden">
<ScrollArea className="h-full">
<div className="p-8 max-w-2xl">
{renderSection()}
</div>
</ScrollArea>
</div>
</div>
</FullScreenDialogBody>
<FullScreenDialogFooter>
{error && (
<div className="flex-1 rounded-lg bg-destructive/10 border border-destructive/30 px-4 py-2 text-sm text-destructive">
{error}
</div>
)}
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
@@ -601,8 +634,8 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</FullScreenDialogFooter>
</FullScreenDialogContent>
</FullScreenDialog>
);
}
@@ -69,6 +69,7 @@ const serviceTypeColors: Record<string, string> = {
const memoryTypeIcons: Record<string, React.ElementType> = {
session_insight: Lightbulb,
codebase_discovery: FolderTree,
codebase_map: FolderTree,
pattern: Code,
gotcha: AlertTriangle
};
@@ -17,7 +17,8 @@ import {
Github,
FileText,
Sparkles,
GitBranch
GitBranch,
HelpCircle
} from 'lucide-react';
import { Button } from './ui/button';
import { ScrollArea } from './ui/scroll-area';
@@ -281,14 +282,6 @@ export function Sidebar({
</TooltipTrigger>
<TooltipContent>Toggle theme</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={onSettingsClick}>
<Settings className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Settings</TooltipContent>
</Tooltip>
</div>
</div>
@@ -385,8 +378,39 @@ export function Sidebar({
<Separator />
{/* New Task button */}
<div className="p-4">
{/* Bottom section with Settings, Help, and New Task */}
<div className="p-4 space-y-3">
{/* Settings and Help row */}
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="flex-1 justify-start gap-2"
onClick={onSettingsClick}
>
<Settings className="h-4 w-4" />
Settings
</Button>
</TooltipTrigger>
<TooltipContent side="top">Application Settings</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => window.open('https://github.com/anthropics/claude-code/issues', '_blank')}
>
<HelpCircle className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">Help & Feedback</TooltipContent>
</Tooltip>
</div>
{/* New Task button */}
<Button
className="w-full"
onClick={onNewTaskClick}
@@ -1,10 +1,10 @@
import { useEffect, useRef, useCallback } from 'react';
import { useEffect, useRef, useCallback, useState } from 'react';
import { Terminal as XTerm } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import { useDroppable } from '@dnd-kit/core';
import '@xterm/xterm/css/xterm.css';
import { X, Sparkles, TerminalSquare, ListTodo, FileDown, ChevronDown, Circle, Loader2, CheckCircle2, AlertCircle, Clock, Code2, Search, Wrench } from 'lucide-react';
import { X, Sparkles, TerminalSquare, ListTodo, FileDown, ChevronDown, Circle, Loader2, CheckCircle2, AlertCircle, Clock, Code2, Search, Wrench, Pencil } from 'lucide-react';
import { Button } from './ui/button';
import { cn } from '../lib/utils';
import { useTerminalStore, type TerminalStatus } from '../stores/terminal-store';
@@ -65,6 +65,11 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate,
const isCreatingRef = useRef(false);
const isCreatedRef = useRef(false);
const isMountedRef = useRef(true);
const titleInputRef = useRef<HTMLInputElement>(null);
// Title editing state
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [editedTitle, setEditedTitle] = useState('');
const terminal = useTerminalStore((state) => state.terminals.find((t) => t.id === id));
const setTerminalStatus = useTerminalStore((state) => state.setTerminalStatus);
@@ -368,6 +373,7 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate,
}, [onActivate]);
// Handle task selection from dropdown
/* eslint-disable react-hooks/preserve-manual-memoization -- Complex callback with mutable tasks array */
const handleTaskSelect = useCallback((taskId: string) => {
const selectedTask = tasks.find((t) => t.id === taskId);
if (!selectedTask) return;
@@ -387,6 +393,7 @@ Please confirm you're ready by saying: I'm ready to work on ${selectedTask.title
// Send the context message to the terminal
window.electronAPI.sendTerminalInput(id, contextMessage + '\r');
}, [id, tasks, setAssociatedTask, updateTerminal]);
/* eslint-enable react-hooks/preserve-manual-memoization */
// Handle clearing the associated task
const handleClearTask = useCallback(() => {
@@ -399,6 +406,40 @@ Please confirm you're ready by saying: I'm ready to work on ${selectedTask.title
const phaseConfig = PHASE_CONFIG[executionPhase];
const PhaseIcon = phaseConfig.icon;
// Title editing handlers
const handleStartEditTitle = useCallback(() => {
setEditedTitle(terminal?.title || 'Terminal');
setIsEditingTitle(true);
// Focus the input after state update
setTimeout(() => {
titleInputRef.current?.focus();
titleInputRef.current?.select();
}, 0);
}, [terminal?.title]);
const handleSaveTitle = useCallback(() => {
const trimmedTitle = editedTitle.trim();
if (trimmedTitle && trimmedTitle !== terminal?.title) {
updateTerminal(id, { title: trimmedTitle });
}
setIsEditingTitle(false);
}, [editedTitle, terminal?.title, updateTerminal, id]);
const handleCancelEditTitle = useCallback(() => {
setIsEditingTitle(false);
setEditedTitle('');
}, []);
const handleTitleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSaveTitle();
} else if (e.key === 'Escape') {
e.preventDefault();
handleCancelEditTitle();
}
}, [handleSaveTitle, handleCancelEditTitle]);
return (
<div
ref={setDropRef}
@@ -424,24 +465,58 @@ Please confirm you're ready by saying: I'm ready to work on ${selectedTask.title
<div className={cn('h-2 w-2 rounded-full', STATUS_COLORS[terminal?.status || 'idle'])} />
<div className="flex items-center gap-1.5">
<TerminalSquare className="h-3.5 w-3.5 text-muted-foreground" />
{/* Terminal title with optional tooltip showing task description */}
{associatedTask ? (
{/* Terminal title - editable on double-click */}
{isEditingTitle ? (
<input
ref={titleInputRef}
type="text"
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
onKeyDown={handleTitleKeyDown}
onBlur={handleSaveTitle}
onClick={(e) => e.stopPropagation()}
className="text-xs font-medium text-foreground bg-transparent border border-primary/50 rounded px-1 py-0.5 outline-none focus:border-primary max-w-32"
style={{ width: `${Math.max(editedTitle.length * 6 + 16, 60)}px` }}
/>
) : associatedTask ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-xs font-medium text-foreground truncate max-w-32 cursor-help">
<span
className="text-xs font-medium text-foreground truncate max-w-32 cursor-text hover:text-primary/80 transition-colors"
onDoubleClick={(e) => {
e.stopPropagation();
handleStartEditTitle();
}}
>
{terminal?.title || 'Terminal'}
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
<p className="text-sm">{associatedTask.description}</p>
<p className="text-xs text-muted-foreground mt-1">Double-click to rename</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<span className="text-xs font-medium text-foreground truncate max-w-32">
{terminal?.title || 'Terminal'}
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className="text-xs font-medium text-foreground truncate max-w-32 cursor-text hover:text-primary/80 transition-colors"
onDoubleClick={(e) => {
e.stopPropagation();
handleStartEditTitle();
}}
>
{terminal?.title || 'Terminal'}
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<p className="text-xs">Double-click to rename</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{terminal?.isClaudeMode && (
@@ -0,0 +1,144 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '../../lib/utils';
const FullScreenDialog = DialogPrimitive.Root;
const FullScreenDialogTrigger = DialogPrimitive.Trigger;
const FullScreenDialogPortal = DialogPrimitive.Portal;
const FullScreenDialogClose = DialogPrimitive.Close;
const FullScreenDialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-background/95 backdrop-blur-sm',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
/>
));
FullScreenDialogOverlay.displayName = 'FullScreenDialogOverlay';
const FullScreenDialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<FullScreenDialogPortal>
<FullScreenDialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed inset-4 z-50 flex flex-col',
'bg-card border border-border rounded-2xl',
'shadow-2xl overflow-hidden',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'data-[state=closed]:zoom-out-98 data-[state=open]:zoom-in-98',
'duration-200',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close
className={cn(
'absolute right-4 top-4 rounded-lg p-2',
'text-muted-foreground hover:text-foreground',
'hover:bg-accent transition-colors',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background',
'disabled:pointer-events-none z-10'
)}
>
<X className="h-5 w-5" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</FullScreenDialogPortal>
));
FullScreenDialogContent.displayName = 'FullScreenDialogContent';
const FullScreenDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-1.5 p-6 pb-4 border-b border-border',
className
)}
{...props}
/>
);
FullScreenDialogHeader.displayName = 'FullScreenDialogHeader';
const FullScreenDialogBody = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex-1 overflow-hidden', className)} {...props} />
);
FullScreenDialogBody.displayName = 'FullScreenDialogBody';
const FullScreenDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex items-center justify-end gap-3 p-6 pt-4 border-t border-border',
className
)}
{...props}
/>
);
FullScreenDialogFooter.displayName = 'FullScreenDialogFooter';
const FullScreenDialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
'text-xl font-semibold leading-none tracking-tight text-foreground',
className
)}
{...props}
/>
));
FullScreenDialogTitle.displayName = 'FullScreenDialogTitle';
const FullScreenDialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
FullScreenDialogDescription.displayName = 'FullScreenDialogDescription';
export {
FullScreenDialog,
FullScreenDialogPortal,
FullScreenDialogOverlay,
FullScreenDialogClose,
FullScreenDialogTrigger,
FullScreenDialogContent,
FullScreenDialogHeader,
FullScreenDialogBody,
FullScreenDialogFooter,
FullScreenDialogTitle,
FullScreenDialogDescription,
};
@@ -801,3 +801,39 @@ body {
background: #1A1A1F;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
/* ============================================
Full Screen Dialog Animations
============================================ */
/* Zoom in/out animations for full-screen dialogs */
@keyframes zoom-in-98 {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes zoom-out-98 {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.98);
}
}
/* Apply animations via data attributes (Radix UI pattern) */
[data-state="open"].animate-in.zoom-in-98 {
animation: zoom-in-98 0.2s ease-out;
}
[data-state="closed"].animate-out.zoom-out-98 {
animation: zoom-out-98 0.2s ease-in;
}
+1 -1
View File
@@ -690,7 +690,7 @@ export interface GraphitiMemoryState {
export interface MemoryEpisode {
id: string;
type: 'session_insight' | 'codebase_discovery' | 'pattern' | 'gotcha';
type: 'session_insight' | 'codebase_discovery' | 'codebase_map' | 'pattern' | 'gotcha';
timestamp: string;
content: string;
session_number?: number;
+3 -41
View File
@@ -86,7 +86,6 @@ from task_logger import (
get_task_logger,
clear_task_logger,
)
from task_tool import TaskToolCoordinator
# Configure logging
logger = logging.getLogger(__name__)
@@ -868,11 +867,13 @@ async def run_autonomous_agent(
max_iterations: Optional[int] = None,
verbose: bool = False,
source_spec_dir: Optional[Path] = None,
max_parallel_subtasks: int = 1,
) -> None:
"""
Run the autonomous agent loop with automatic memory management.
The agent can use subagents (via Task tool) for parallel execution if needed.
This is decided by the agent itself based on the task complexity.
Args:
project_dir: Root directory for the project
spec_dir: Directory containing the spec (auto-claude/specs/001-name/)
@@ -880,7 +881,6 @@ async def run_autonomous_agent(
max_iterations: Maximum number of iterations (None for unlimited)
verbose: Whether to show detailed output
source_spec_dir: Original spec directory in main project (for syncing from worktree)
max_parallel_subtasks: Maximum parallel subtasks (1=sequential, 2-10=parallel)
"""
# Initialize recovery manager (handles memory persistence)
recovery_manager = RecoveryManager(spec_dir, project_dir)
@@ -1053,44 +1053,6 @@ async def run_autonomous_agent(
task_logger.end_phase(LogPhase.PLANNING, success=True, message="Implementation plan created")
task_logger.start_phase(LogPhase.CODING, "Starting implementation...")
# Check if parallel execution is requested
if max_parallel_subtasks > 1:
print_status(f"Parallel mode: Using TaskToolCoordinator with {max_parallel_subtasks} workers", "info")
coordinator = TaskToolCoordinator(
spec_dir=spec_dir,
project_dir=project_dir,
model=model,
max_parallel=max_parallel_subtasks,
)
# Run parallel build loop
while True:
completed, failed = await coordinator.run_next_batch()
if completed == 0 and failed == 0:
# No more pending subtasks
break
print(f"Batch complete: {completed} succeeded, {failed} failed")
# Check if build is complete
if is_build_complete(spec_dir):
print_build_complete_banner(spec_dir)
status_manager.update(state=BuildState.COMPLETE)
return
await asyncio.sleep(2)
# After parallel loop completes, check final status
if is_build_complete(spec_dir):
print_build_complete_banner(spec_dir)
status_manager.update(state=BuildState.COMPLETE)
else:
print_status("Parallel build incomplete - some subtasks may need retry", "warning")
status_manager.update(state=BuildState.PAUSED)
return
if not next_subtask:
print("No pending subtasks found - build may be complete!")
break
+31 -1
View File
@@ -217,12 +217,25 @@ class Phase:
depends_on: list[int] = field(default_factory=list)
parallel_safe: bool = False # Can subtasks in this phase run in parallel?
# Backwards compatibility: chunks is an alias for subtasks
@property
def chunks(self) -> list[Subtask]:
"""Alias for subtasks (backwards compatibility)."""
return self.subtasks
@chunks.setter
def chunks(self, value: list[Subtask]):
"""Alias for subtasks (backwards compatibility)."""
self.subtasks = value
def to_dict(self) -> dict:
result = {
"phase": self.phase,
"name": self.name,
"type": self.type.value,
"subtasks": [s.to_dict() for s in self.subtasks],
# Also include 'chunks' for backwards compatibility
"chunks": [s.to_dict() for s in self.subtasks],
}
if self.depends_on:
result["depends_on"] = self.depends_on
@@ -233,11 +246,13 @@ class Phase:
@classmethod
def from_dict(cls, data: dict, fallback_phase: int = 1) -> "Phase":
"""Create Phase from dict. Uses fallback_phase if 'phase' field is missing."""
# Support both 'subtasks' and 'chunks' keys for backwards compatibility
subtask_data = data.get("subtasks", data.get("chunks", []))
return cls(
phase=data.get("phase", fallback_phase),
name=data.get("name", f"Phase {fallback_phase}"),
type=PhaseType(data.get("type", "implementation")),
subtasks=[Subtask.from_dict(s) for s in data.get("subtasks", [])],
subtasks=[Subtask.from_dict(s) for s in subtask_data],
depends_on=data.get("depends_on", []),
parallel_safe=data.get("parallel_safe", False),
)
@@ -250,6 +265,11 @@ class Phase:
"""Get subtasks that can be worked on."""
return [s for s in self.subtasks if s.status == SubtaskStatus.PENDING]
# Backwards compatibility alias
def get_pending_chunks(self) -> list[Subtask]:
"""Alias for get_pending_subtasks (backwards compatibility)."""
return self.get_pending_subtasks()
def get_progress(self) -> tuple[int, int]:
"""Get (completed, total) subtask counts."""
done = sum(1 for s in self.subtasks if s.status == SubtaskStatus.COMPLETED)
@@ -815,3 +835,13 @@ if __name__ == "__main__":
# Load and display existing plan
plan = ImplementationPlan.load(Path(sys.argv[1]))
print(plan.get_status_summary())
# =============================================================================
# BACKWARDS COMPATIBILITY ALIASES
# =============================================================================
# These aliases maintain compatibility with code that uses the old "chunk"
# terminology. New code should use Subtask/SubtaskStatus.
Chunk = Subtask
ChunkStatus = SubtaskStatus
+29
View File
@@ -365,6 +365,35 @@ Update `implementation_plan.json`:
"status": "in_progress"
```
### Using Subagents for Complex Work (Optional)
**For complex subtasks**, you can spawn subagents to work in parallel. Subagents are lightweight Claude Code instances that:
- Have their own isolated context windows
- Can work on different parts of the subtask simultaneously
- Report back to you (the orchestrator)
**When to use subagents:**
- Implementing multiple independent files in a subtask
- Research/exploration of different parts of the codebase
- Running different types of verification in parallel
- Large subtasks that can be logically divided
**How to spawn subagents:**
```
Use the Task tool to spawn a subagent:
"Implement the database schema changes in models.py"
"Research how authentication is handled in the existing codebase"
"Run tests for the API endpoints while I work on the frontend"
```
**Best practices:**
- Let Claude Code decide the parallelism level (don't specify batch sizes)
- Subagents work best on disjoint tasks (different files/modules)
- Each subagent has its own context window - use this for large codebases
- You can spawn up to 10 concurrent subagents
**Note:** For simple subtasks, sequential implementation is usually sufficient. Subagents add value when there's genuinely parallel work to be done.
### Implementation Rules
1. **Match patterns exactly** - Use the same style as patterns_from files
+21 -45
View File
@@ -465,7 +465,6 @@ Examples:
python auto-claude/run.py --spec 001 --discard # Delete build (with confirmation)
# Advanced options
python auto-claude/run.py --spec 001 --parallel 2 # Use 2 parallel workers
python auto-claude/run.py --spec 001 --direct # Skip workspace isolation
python auto-claude/run.py --spec 001 --isolated # Force workspace isolation
@@ -524,13 +523,6 @@ Environment Variables:
help="Enable verbose output",
)
parser.add_argument(
"--parallel",
type=int,
default=1,
help="Number of parallel workers (default: 1 = sequential). Use 2-3 for parallelism.",
)
# Workspace options
workspace_group = parser.add_mutually_exclusive_group()
workspace_group.add_argument(
@@ -1000,11 +992,6 @@ def main() -> None:
print(f"Spec: {spec_dir.name}")
print(f"Model: {args.model}")
if args.parallel > 1:
print(f"Parallel mode: {args.parallel} workers")
else:
print("Sequential mode: 1 worker")
if args.max_iterations:
print(f"Max iterations: {args.max_iterations}")
else:
@@ -1072,46 +1059,36 @@ def main() -> None:
worktree_manager = None
source_spec_dir = None # Track original spec dir for syncing back from worktree
if args.parallel > 1:
# Parallel mode always uses worktrees (managed by coordinator)
workspace_mode = WorkspaceMode.ISOLATED
print("Parallel mode uses isolated workspaces automatically.")
else:
# Sequential mode - let user choose (or auto-select if --auto-continue)
workspace_mode = choose_workspace(
project_dir,
spec_dir.name,
force_isolated=args.isolated,
force_direct=args.direct,
auto_continue=args.auto_continue,
# Let user choose workspace mode (or auto-select if --auto-continue)
workspace_mode = choose_workspace(
project_dir,
spec_dir.name,
force_isolated=args.isolated,
force_direct=args.direct,
auto_continue=args.auto_continue,
)
if workspace_mode == WorkspaceMode.ISOLATED:
# Keep reference to original spec directory for syncing progress back
source_spec_dir = spec_dir
working_dir, worktree_manager, localized_spec_dir = setup_workspace(
project_dir, spec_dir.name, workspace_mode, source_spec_dir=spec_dir
)
# Use the localized spec directory (inside worktree) for AI access
if localized_spec_dir:
spec_dir = localized_spec_dir
if workspace_mode == WorkspaceMode.ISOLATED:
# Keep reference to original spec directory for syncing progress back
source_spec_dir = spec_dir
working_dir, worktree_manager, localized_spec_dir = setup_workspace(
project_dir, spec_dir.name, workspace_mode, source_spec_dir=spec_dir
)
# Use the localized spec directory (inside worktree) for AI access
if localized_spec_dir:
spec_dir = localized_spec_dir
# Run the autonomous agent (sequential or parallel)
# Run the autonomous agent
debug_section("run.py", "Starting Build Execution")
debug("run.py", "Build configuration",
parallel=args.parallel,
model=args.model,
workspace_mode=str(workspace_mode),
working_dir=str(working_dir),
spec_dir=str(spec_dir))
try:
# Both sequential and parallel modes now use run_autonomous_agent
# with max_parallel_subtasks parameter
execution_mode = "parallel" if args.parallel > 1 else "sequential"
debug("run.py", f"Starting {execution_mode} execution",
max_parallel_subtasks=args.parallel)
debug("run.py", "Starting agent execution")
asyncio.run(
run_autonomous_agent(
@@ -1121,10 +1098,9 @@ def main() -> None:
max_iterations=args.max_iterations,
verbose=args.verbose,
source_spec_dir=source_spec_dir, # For syncing progress back to main project
max_parallel_subtasks=args.parallel, # Pass parallel count
)
)
debug_success("run.py", f"{execution_mode.capitalize()} execution completed")
debug_success("run.py", "Agent execution completed")
# Run QA validation BEFORE finalization (while worktree still exists)
# QA must sign off before the build is considered complete
-412
View File
@@ -1,412 +0,0 @@
"""
Task Tool Integration for Parallel Subtask Execution
=====================================================
Wrapper around Claude Code SDK's Task tool for spawning parallel subagents.
This replaces the complex SwarmCoordinator with a simpler approach that
leverages Claude Code's built-in parallel execution capabilities.
Design Principles:
- SDK handles subagent spawning, isolation, and coordination
- Python orchestrator only needs to generate prompts and monitor progress
- All work happens in the spec worktree (no worker branches)
- Fallback to sequential execution if parallel isn't needed/available
Usage:
coordinator = TaskToolCoordinator(client, spec_dir, project_dir)
await coordinator.run_subtasks_parallel(subtasks, max_parallel=3)
"""
import asyncio
import json
import logging
from pathlib import Path
from typing import Optional
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from client import create_client
from implementation_plan import ImplementationPlan, Subtask, SubtaskStatus
from prompt_generator import generate_subtask_prompt, load_subtask_context, format_context_for_prompt
logger = logging.getLogger(__name__)
class TaskToolCoordinator:
"""
Coordinates parallel subtask execution using Claude Code Task tool.
Much simpler than SwarmCoordinator - SDK handles:
- Subagent spawning (up to 10 parallel)
- Isolated context windows
- Scheduling and resource management
- Progress tracking
This coordinator generates prompts and lets the agent use Task tool
to spawn subagents for parallel work.
"""
def __init__(
self,
spec_dir: Path,
project_dir: Path,
model: str = "claude-sonnet-4-20250514",
max_parallel: int = 3,
):
"""
Initialize the Task Tool coordinator.
Args:
spec_dir: Directory containing the spec and implementation plan
project_dir: Root project directory (or spec worktree path)
model: Claude model to use for subagents
max_parallel: Maximum number of parallel subagents (1-10)
"""
self.spec_dir = spec_dir
self.project_dir = project_dir
self.model = model
self.max_parallel = min(max(1, max_parallel), 10) # Clamp to 1-10
# Load implementation plan
self.plan_file = spec_dir / "implementation_plan.json"
self.plan: Optional[ImplementationPlan] = None
if self.plan_file.exists():
self.plan = ImplementationPlan.load(self.plan_file)
def get_pending_subtasks(self) -> list[Subtask]:
"""Get all pending subtasks that can be worked on."""
if not self.plan:
return []
pending = []
for phase in self.plan.phases:
# Check if phase dependencies are satisfied
deps_satisfied = True
for dep_id in phase.depends_on:
dep_phase = next((p for p in self.plan.phases if p.id == dep_id), None)
if dep_phase and not all(s.status == SubtaskStatus.COMPLETED for s in dep_phase.subtasks):
deps_satisfied = False
break
if deps_satisfied:
for subtask in phase.subtasks:
if subtask.status == SubtaskStatus.PENDING:
pending.append(subtask)
return pending
def get_parallelizable_subtasks(self, subtasks: list[Subtask]) -> list[list[Subtask]]:
"""
Group subtasks into batches that can run in parallel.
Subtasks within the same phase can potentially run in parallel
if they don't modify the same files.
Args:
subtasks: List of pending subtasks
Returns:
List of batches, where each batch can run in parallel
"""
if not subtasks:
return []
# Group by phase
phase_groups: dict[str, list[Subtask]] = {}
for subtask in subtasks:
phase_id = subtask.id.split("-")[0] if "-" in subtask.id else "default"
if phase_id not in phase_groups:
phase_groups[phase_id] = []
phase_groups[phase_id].append(subtask)
# For now, return each phase group as a batch
# In the future, we could do smarter file conflict detection
batches = []
for phase_subtasks in phase_groups.values():
# Split into batches of max_parallel
for i in range(0, len(phase_subtasks), self.max_parallel):
batch = phase_subtasks[i:i + self.max_parallel]
batches.append(batch)
return batches
async def run_subtask_session(
self,
subtask: Subtask,
phase: Optional[dict] = None,
) -> bool:
"""
Run a single subtask session using the SDK.
Args:
subtask: The subtask to implement
phase: Optional phase context
Returns:
True if subtask was completed successfully
"""
# Convert Subtask dataclass to dict for prompt generator
subtask_dict = {
"id": subtask.id,
"description": subtask.description,
"status": subtask.status.value,
"service": getattr(subtask, "service", None),
"files_to_modify": getattr(subtask, "files_to_modify", []),
"files_to_create": getattr(subtask, "files_to_create", []),
"patterns_from": getattr(subtask, "patterns_from", []),
"verification": getattr(subtask, "verification", None),
}
# Generate the subtask prompt
prompt = generate_subtask_prompt(
spec_dir=self.spec_dir,
project_dir=self.project_dir,
subtask=subtask_dict,
phase=phase or {},
attempt_count=0,
recovery_hints=None,
)
# Load and append context
context = load_subtask_context(self.spec_dir, self.project_dir, subtask_dict)
if context.get("patterns") or context.get("files_to_modify"):
prompt += "\n\n" + format_context_for_prompt(context)
# Create client for this subtask
client = create_client(
project_dir=self.project_dir,
spec_dir=self.spec_dir,
model=self.model,
agent_type="coder",
)
try:
logger.info(f"Starting subtask: {subtask.id}")
print(f"\n[Subtask {subtask.id}] Starting...")
async with client:
await client.query(prompt)
# Collect response
response_text = ""
async for msg in client.receive_response():
msg_type = type(msg).__name__
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
for block in msg.content:
block_type = type(block).__name__
if block_type == "TextBlock" and hasattr(block, "text"):
response_text += block.text
# Reload plan to check if subtask was marked complete
if self.plan_file.exists():
updated_plan = ImplementationPlan.load(self.plan_file)
for phase in updated_plan.phases:
for s in phase.subtasks:
if s.id == subtask.id:
if s.status == SubtaskStatus.COMPLETED:
logger.info(f"Subtask {subtask.id} completed successfully")
print(f"[Subtask {subtask.id}] Completed!")
return True
logger.warning(f"Subtask {subtask.id} not marked as completed")
print(f"[Subtask {subtask.id}] Not completed")
return False
except Exception as e:
logger.error(f"Error running subtask {subtask.id}: {e}")
print(f"[Subtask {subtask.id}] Error: {e}")
return False
async def run_subtasks_parallel(
self,
subtasks: list[Subtask],
) -> dict[str, bool]:
"""
Execute subtasks in parallel using asyncio.
Since the Claude Code SDK doesn't directly expose Task tool spawning,
we achieve parallelism by running multiple SDK sessions concurrently.
Args:
subtasks: List of subtasks to execute
Returns:
Dict mapping subtask_id to success status
"""
results: dict[str, bool] = {}
if not subtasks:
return results
# Group into parallelizable batches
batches = self.get_parallelizable_subtasks(subtasks)
logger.info(f"Running {len(subtasks)} subtasks in {len(batches)} batch(es)")
print(f"\nParallel execution: {len(subtasks)} subtasks, {len(batches)} batch(es)")
print(f"Max parallel: {self.max_parallel}")
for batch_idx, batch in enumerate(batches):
print(f"\n--- Batch {batch_idx + 1}/{len(batches)} ({len(batch)} subtasks) ---")
# Create tasks for parallel execution
tasks = [
self.run_subtask_session(subtask)
for subtask in batch
]
# Run batch in parallel
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
# Record results
for subtask, result in zip(batch, batch_results):
if isinstance(result, Exception):
logger.error(f"Subtask {subtask.id} failed with exception: {result}")
results[subtask.id] = False
else:
results[subtask.id] = result
# Small delay between batches
if batch_idx < len(batches) - 1:
await asyncio.sleep(1)
return results
async def run_next_batch(self) -> tuple[int, int]:
"""
Run the next batch of pending subtasks.
Returns:
(completed_count, failed_count)
"""
pending = self.get_pending_subtasks()
if not pending:
return 0, 0
# Take at most max_parallel subtasks
batch = pending[:self.max_parallel]
results = await self.run_subtasks_parallel(batch)
completed = sum(1 for v in results.values() if v)
failed = sum(1 for v in results.values() if not v)
return completed, failed
async def run_parallel_build(
spec_dir: Path,
project_dir: Path,
model: str = "claude-sonnet-4-20250514",
max_parallel: int = 3,
max_iterations: int = 100,
) -> bool:
"""
Run the full parallel build loop.
This is a simplified alternative to run_autonomous_agent that uses
parallel subtask execution.
Args:
spec_dir: Directory containing the spec
project_dir: Root project directory
model: Claude model to use
max_parallel: Maximum parallel subtasks
max_iterations: Maximum number of build iterations
Returns:
True if build completed successfully
"""
coordinator = TaskToolCoordinator(
spec_dir=spec_dir,
project_dir=project_dir,
model=model,
max_parallel=max_parallel,
)
iteration = 0
while iteration < max_iterations:
iteration += 1
print(f"\n{'='*60}")
print(f"Build Iteration {iteration}")
print(f"{'='*60}")
completed, failed = await coordinator.run_next_batch()
if completed == 0 and failed == 0:
# No pending subtasks
print("\nNo pending subtasks - checking build status...")
# Reload plan and check completion
if coordinator.plan_file.exists():
plan = ImplementationPlan.load(coordinator.plan_file)
all_subtasks = [s for p in plan.phases for s in p.subtasks]
all_completed = all(s.status == SubtaskStatus.COMPLETED for s in all_subtasks)
if all_completed:
print("\nBuild complete! All subtasks finished.")
return True
else:
pending = [s for s in all_subtasks if s.status == SubtaskStatus.PENDING]
print(f"\nBuild incomplete: {len(pending)} subtasks still pending")
print("This may be due to unsatisfied phase dependencies.")
break
print(f"\nBatch results: {completed} completed, {failed} failed")
if failed > 0:
print(f"Warning: {failed} subtask(s) failed in this batch")
# Continue trying - recovery may help
# Small delay between iterations
await asyncio.sleep(2)
return False
# === Convenience Functions ===
def get_parallel_status(spec_dir: Path) -> dict:
"""
Get the current parallel build status.
Returns dict with:
- pending: list of pending subtask IDs
- completed: list of completed subtask IDs
- failed: list of failed subtask IDs
- can_parallelize: bool indicating if parallel execution is possible
"""
plan_file = spec_dir / "implementation_plan.json"
if not plan_file.exists():
return {
"pending": [],
"completed": [],
"failed": [],
"can_parallelize": False,
}
plan = ImplementationPlan.load(plan_file)
pending = []
completed = []
failed = []
for phase in plan.phases:
for subtask in phase.subtasks:
if subtask.status == SubtaskStatus.PENDING:
pending.append(subtask.id)
elif subtask.status == SubtaskStatus.COMPLETED:
completed.append(subtask.id)
elif subtask.status == SubtaskStatus.FAILED:
failed.append(subtask.id)
return {
"pending": pending,
"completed": completed,
"failed": failed,
"can_parallelize": len(pending) > 1,
}
+289
View File
@@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""
Tests for Agent Architecture
============================
Verifies the agent architecture where:
- Python orchestrator runs a single Claude SDK session
- The agent itself decides when to spawn subagents (via Task tool)
- Parallel execution is handled internally by Claude Code, not Python
Key architectural constraints:
- No Python-level parallel orchestration (no coordinator.py, task_tool.py)
- No --parallel CLI flag (agent decides parallelism)
- Agent prompt includes subagent capability documentation
"""
import ast
import inspect
import sys
from pathlib import Path
import pytest
# Add auto-claude directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "auto-claude"))
class TestNoExternalParallelism:
"""Verify no Python-level parallel orchestration exists."""
def test_no_coordinator_module(self):
"""No external coordinator module should exist."""
coordinator_path = Path(__file__).parent.parent / "auto-claude" / "coordinator.py"
assert not coordinator_path.exists(), (
"coordinator.py should not exist. Parallel orchestration is handled "
"internally by the agent using Claude Code's Task tool."
)
def test_no_task_tool_module(self):
"""No task_tool wrapper module should exist."""
task_tool_path = Path(__file__).parent.parent / "auto-claude" / "task_tool.py"
assert not task_tool_path.exists(), (
"task_tool.py should not exist. The agent spawns subagents directly "
"using Claude Code's built-in Task tool."
)
def test_no_subtask_worker_config(self):
"""No external subtask worker agent config should exist."""
worker_config = Path(__file__).parent.parent / ".claude" / "agents" / "subtask-worker.md"
assert not worker_config.exists(), (
"subtask-worker.md should not exist. Subagents use Claude Code's "
"built-in agent types, not custom configs."
)
class TestCLIInterface:
"""Verify CLI doesn't expose parallel orchestration options."""
def test_no_parallel_flag(self):
"""CLI should not have --parallel argument."""
run_py_path = Path(__file__).parent.parent / "auto-claude" / "run.py"
content = run_py_path.read_text()
# Check that --parallel is not defined as an argument
assert '"--parallel"' not in content, (
"CLI should not have --parallel flag. The agent decides when to "
"use parallel execution via subagents."
)
assert "'--parallel'" not in content, (
"CLI should not have --parallel flag. The agent decides when to "
"use parallel execution via subagents."
)
def test_no_parallel_examples_in_docs(self):
"""CLI documentation should not mention parallel mode."""
run_py_path = Path(__file__).parent.parent / "auto-claude" / "run.py"
content = run_py_path.read_text()
# The docstring should not have --parallel examples
assert "--parallel" not in content[:2000], (
"CLI docs should not contain --parallel examples."
)
class TestAgentEntryPoint:
"""Verify the agent entry point function signature."""
def test_no_parallel_parameters(self):
"""Agent entry point should not accept parallel configuration."""
from agent import run_autonomous_agent
sig = inspect.signature(run_autonomous_agent)
param_names = list(sig.parameters.keys())
assert "max_parallel_subtasks" not in param_names, (
"Agent should not accept max_parallel_subtasks. "
"Parallelism is decided by the agent itself."
)
assert "parallel" not in param_names, (
"Agent should not accept a 'parallel' parameter."
)
def test_required_parameters(self):
"""Agent entry point has required parameters."""
from agent import run_autonomous_agent
sig = inspect.signature(run_autonomous_agent)
param_names = list(sig.parameters.keys())
expected = ["project_dir", "spec_dir", "model"]
for param in expected:
assert param in param_names, f"Expected parameter '{param}' not found"
def test_is_async(self):
"""Agent entry point is async."""
from agent import run_autonomous_agent
assert inspect.iscoroutinefunction(run_autonomous_agent), (
"run_autonomous_agent should be async"
)
class TestAgentPrompt:
"""Verify the agent prompt documents subagent capability."""
def test_mentions_subagents(self):
"""Agent prompt mentions subagent capability."""
coder_prompt_path = Path(__file__).parent.parent / "auto-claude" / "prompts" / "coder.md"
content = coder_prompt_path.read_text()
assert "subagent" in content.lower(), (
"Agent prompt should document subagent capability for parallel work."
)
def test_mentions_parallel_capability(self):
"""Agent prompt mentions parallel/concurrent capability."""
coder_prompt_path = Path(__file__).parent.parent / "auto-claude" / "prompts" / "coder.md"
content = coder_prompt_path.read_text()
has_task_tool = "task tool" in content.lower() or "Task tool" in content
has_parallel = "parallel" in content.lower()
has_concurrent = "concurrent" in content.lower() or "simultaneously" in content.lower()
assert has_task_tool or has_parallel or has_concurrent, (
"Agent prompt should mention parallel/concurrent work capability."
)
class TestModuleIntegrity:
"""Verify core modules work correctly."""
def test_agent_module_imports(self):
"""Agent module imports without errors."""
try:
import agent
except ImportError as e:
pytest.fail(f"agent.py failed to import: {e}")
def test_run_module_valid_syntax(self):
"""Run module has valid Python syntax."""
run_py_path = Path(__file__).parent.parent / "auto-claude" / "run.py"
content = run_py_path.read_text()
try:
ast.parse(content)
except SyntaxError as e:
pytest.fail(f"run.py has syntax error: {e}")
def test_no_coordinator_imports(self):
"""Core modules don't import coordinator."""
for filename in ["run.py", "agent.py"]:
filepath = Path(__file__).parent.parent / "auto-claude" / filename
content = filepath.read_text()
assert "from coordinator import" not in content, (
f"{filename} should not import coordinator"
)
assert "import coordinator" not in content, (
f"{filename} should not import coordinator"
)
def test_no_task_tool_imports(self):
"""Core modules don't import task_tool."""
for filename in ["run.py", "agent.py"]:
filepath = Path(__file__).parent.parent / "auto-claude" / filename
content = filepath.read_text()
assert "from task_tool import" not in content, (
f"{filename} should not import task_tool"
)
assert "import task_tool" not in content, (
f"{filename} should not import task_tool"
)
class TestProjectDocumentation:
"""Verify project documentation is accurate."""
def test_no_parallel_cli_documented(self):
"""CLAUDE.md doesn't document --parallel flag."""
claude_md_path = Path(__file__).parent.parent / "CLAUDE.md"
content = claude_md_path.read_text()
assert "--parallel 2" not in content, (
"CLAUDE.md should not document --parallel flag"
)
def test_subagent_architecture_documented(self):
"""CLAUDE.md documents subagent-based architecture."""
claude_md_path = Path(__file__).parent.parent / "CLAUDE.md"
content = claude_md_path.read_text()
has_subagent = "subagent" in content.lower()
has_task_tool = "task tool" in content.lower()
assert has_subagent or has_task_tool, (
"CLAUDE.md should document subagent-based parallel work"
)
class TestSubtaskTerminology:
"""Verify subtask terminology is used consistently."""
def test_implementation_plan_uses_subtask_class(self):
"""Implementation plan uses Subtask class."""
impl_plan_path = Path(__file__).parent.parent / "auto-claude" / "implementation_plan.py"
content = impl_plan_path.read_text()
assert "class Subtask" in content, (
"implementation_plan.py should define 'class Subtask'"
)
assert "SubtaskStatus" in content, (
"implementation_plan.py should define SubtaskStatus enum"
)
def test_progress_uses_subtask_terminology(self):
"""Progress module uses subtask terminology."""
progress_path = Path(__file__).parent.parent / "auto-claude" / "progress.py"
content = progress_path.read_text()
assert "subtask" in content.lower(), (
"progress.py should use subtask terminology"
)
def run_tests():
"""Run all tests when executed directly."""
print("\nTesting Agent Architecture")
print("=" * 60)
test_classes = [
TestNoExternalParallelism,
TestCLIInterface,
TestAgentEntryPoint,
TestAgentPrompt,
TestModuleIntegrity,
TestProjectDocumentation,
TestSubtaskTerminology,
]
passed = 0
failed = 0
for test_class in test_classes:
print(f"\n{test_class.__name__}:")
instance = test_class()
for method_name in dir(instance):
if method_name.startswith("test_"):
method = getattr(instance, method_name)
try:
method()
print(f"{method_name}")
passed += 1
except AssertionError as e:
print(f"{method_name}: {e}")
failed += 1
except Exception as e:
print(f"{method_name}: Unexpected error: {e}")
failed += 1
print("\n" + "=" * 60)
print(f"Results: {passed} passed, {failed} failed")
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(run_tests())
+2 -2
View File
@@ -12,8 +12,8 @@ import json
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
# Add auto-claude directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "auto-claude"))
from critique import (
generate_critique_prompt,
+21 -21
View File
@@ -48,8 +48,8 @@ class TestAddFollowupPhase:
plan = ImplementationPlan(
feature="Test Feature",
phases=[
Phase(phase=1, name="Phase 1", chunks=[]),
Phase(phase=2, name="Phase 2", chunks=[]),
Phase(phase=1, name="Phase 1", subtasks=[]),
Phase(phase=2, name="Phase 2", subtasks=[]),
],
)
@@ -64,9 +64,9 @@ class TestAddFollowupPhase:
plan = ImplementationPlan(
feature="Test Feature",
phases=[
Phase(phase=1, name="Phase 1", chunks=[]),
Phase(phase=2, name="Phase 2", chunks=[]),
Phase(phase=3, name="Phase 3", chunks=[]),
Phase(phase=1, name="Phase 1", subtasks=[]),
Phase(phase=2, name="Phase 2", subtasks=[]),
Phase(phase=3, name="Phase 3", subtasks=[]),
],
)
@@ -142,7 +142,7 @@ class TestAddFollowupPhase:
"""Multiple follow-ups create sequential phase numbers."""
plan = ImplementationPlan(
feature="Test Feature",
phases=[Phase(phase=1, name="Initial", chunks=[])],
phases=[Phase(phase=1, name="Initial", subtasks=[])],
)
# First follow-up
@@ -185,7 +185,7 @@ class TestResetForFollowup:
Phase(
phase=1,
name="Phase 1",
chunks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
),
],
)
@@ -206,7 +206,7 @@ class TestResetForFollowup:
Phase(
phase=1,
name="Phase 1",
chunks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
),
],
)
@@ -227,7 +227,7 @@ class TestResetForFollowup:
Phase(
phase=1,
name="Phase 1",
chunks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
),
],
)
@@ -248,7 +248,7 @@ class TestResetForFollowup:
Phase(
phase=1,
name="Phase 1",
chunks=[
subtasks=[
Chunk(id="c1", description="Task 1", status=ChunkStatus.COMPLETED),
Chunk(id="c2", description="Task 2", status=ChunkStatus.COMPLETED),
],
@@ -271,7 +271,7 @@ class TestResetForFollowup:
Phase(
phase=1,
name="Phase 1",
chunks=[
subtasks=[
Chunk(id="c1", description="Task 1", status=ChunkStatus.COMPLETED),
Chunk(id="c2", description="Task 2", status=ChunkStatus.PENDING),
],
@@ -293,7 +293,7 @@ class TestResetForFollowup:
Phase(
phase=1,
name="Phase 1",
chunks=[Chunk(id="c1", description="Task", status=ChunkStatus.PENDING)],
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.PENDING)],
),
],
)
@@ -313,7 +313,7 @@ class TestResetForFollowup:
Phase(
phase=1,
name="Phase 1",
chunks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
),
],
)
@@ -333,7 +333,7 @@ class TestResetForFollowup:
Phase(
phase=1,
name="Phase 1",
chunks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
),
],
)
@@ -356,7 +356,7 @@ class TestExistingChunksPreserved:
Phase(
phase=1,
name="Original Phase",
chunks=[
subtasks=[
Chunk(
id="original-1",
description="Original task",
@@ -384,13 +384,13 @@ class TestExistingChunksPreserved:
phase=1,
name="Phase 1",
depends_on=[],
chunks=[Chunk(id="c1", description="Task 1", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c1", description="Task 1", status=ChunkStatus.COMPLETED)],
),
Phase(
phase=2,
name="Phase 2",
depends_on=[1],
chunks=[Chunk(id="c2", description="Task 2", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c2", description="Task 2", status=ChunkStatus.COMPLETED)],
),
]
@@ -420,7 +420,7 @@ class TestFollowupPlanSaveLoad:
Phase(
phase=1,
name="Original",
chunks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
),
],
)
@@ -451,7 +451,7 @@ class TestFollowupPlanSaveLoad:
Phase(
phase=1,
name="Original",
chunks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
),
],
)
@@ -487,7 +487,7 @@ class TestFollowupProgressCalculation:
Phase(
phase=1,
name="Original",
chunks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
),
],
)
@@ -516,7 +516,7 @@ class TestFollowupProgressCalculation:
Phase(
phase=1,
name="Original",
chunks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
subtasks=[Chunk(id="c1", description="Task", status=ChunkStatus.COMPLETED)],
),
],
)
+14 -14
View File
@@ -191,7 +191,7 @@ class TestPhase:
phase=1,
name="Setup",
type=PhaseType.SETUP,
chunks=[chunk1, chunk2],
subtasks=[chunk1, chunk2],
)
assert phase.phase == 1
@@ -202,7 +202,7 @@ class TestPhase:
"""Phase completion checks all chunks."""
chunk1 = Chunk(id="c1", description="Chunk 1", status=ChunkStatus.COMPLETED)
chunk2 = Chunk(id="c2", description="Chunk 2", status=ChunkStatus.COMPLETED)
phase = Phase(phase=1, name="Test", chunks=[chunk1, chunk2])
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2])
assert phase.is_complete() is True
@@ -210,7 +210,7 @@ class TestPhase:
"""Phase not complete with pending chunks."""
chunk1 = Chunk(id="c1", description="Chunk 1", status=ChunkStatus.COMPLETED)
chunk2 = Chunk(id="c2", description="Chunk 2", status=ChunkStatus.PENDING)
phase = Phase(phase=1, name="Test", chunks=[chunk1, chunk2])
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2])
assert phase.is_complete() is False
@@ -219,7 +219,7 @@ class TestPhase:
chunk1 = Chunk(id="c1", description="Chunk 1", status=ChunkStatus.COMPLETED)
chunk2 = Chunk(id="c2", description="Chunk 2", status=ChunkStatus.PENDING)
chunk3 = Chunk(id="c3", description="Chunk 3", status=ChunkStatus.PENDING)
phase = Phase(phase=1, name="Test", chunks=[chunk1, chunk2, chunk3])
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2, chunk3])
pending = phase.get_pending_chunks()
@@ -231,7 +231,7 @@ class TestPhase:
chunk1 = Chunk(id="c1", description="Chunk 1", status=ChunkStatus.COMPLETED)
chunk2 = Chunk(id="c2", description="Chunk 2", status=ChunkStatus.COMPLETED)
chunk3 = Chunk(id="c3", description="Chunk 3", status=ChunkStatus.PENDING)
phase = Phase(phase=1, name="Test", chunks=[chunk1, chunk2, chunk3])
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2, chunk3])
completed, total = phase.get_progress()
@@ -245,7 +245,7 @@ class TestPhase:
phase=1,
name="Setup",
type=PhaseType.SETUP,
chunks=[chunk],
subtasks=[chunk],
depends_on=[],
)
@@ -499,10 +499,10 @@ class TestDependencyResolution:
plan = ImplementationPlan(
feature="Test",
phases=[
Phase(phase=1, name="Setup", chunks=[
Phase(phase=1, name="Setup", subtasks=[
Chunk(id="c1", description="Setup", status=ChunkStatus.PENDING)
]),
Phase(phase=2, name="Build", depends_on=[1], chunks=[
Phase(phase=2, name="Build", depends_on=[1], subtasks=[
Chunk(id="c2", description="Build")
]),
],
@@ -519,13 +519,13 @@ class TestDependencyResolution:
plan = ImplementationPlan(
feature="Test",
phases=[
Phase(phase=1, name="Setup", chunks=[
Phase(phase=1, name="Setup", subtasks=[
Chunk(id="c1", description="Setup", status=ChunkStatus.COMPLETED)
]),
Phase(phase=2, name="Backend", depends_on=[1], chunks=[
Phase(phase=2, name="Backend", depends_on=[1], subtasks=[
Chunk(id="c2", description="Backend")
]),
Phase(phase=3, name="Frontend", depends_on=[1], chunks=[
Phase(phase=3, name="Frontend", depends_on=[1], subtasks=[
Chunk(id="c3", description="Frontend")
]),
],
@@ -544,13 +544,13 @@ class TestDependencyResolution:
plan = ImplementationPlan(
feature="Test",
phases=[
Phase(phase=1, name="Phase1", chunks=[
Phase(phase=1, name="Phase1", subtasks=[
Chunk(id="c1", description="C1", status=ChunkStatus.COMPLETED)
]),
Phase(phase=2, name="Phase2", chunks=[
Phase(phase=2, name="Phase2", subtasks=[
Chunk(id="c2", description="C2", status=ChunkStatus.PENDING)
]),
Phase(phase=3, name="Phase3", depends_on=[1, 2], chunks=[
Phase(phase=3, name="Phase3", depends_on=[1, 2], subtasks=[
Chunk(id="c3", description="C3")
]),
],
-606
View File
@@ -1,606 +0,0 @@
#!/usr/bin/env python3
"""
Test script for parallel execution coordinator
Tests the SwarmCoordinator class that manages parallel chunk execution.
"""
import json
import sys
import tempfile
import shutil
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "auto-claude"))
from coordinator import SwarmCoordinator, ParallelGroup, WorkerAssignment, WorkerStatus
from implementation_plan import (
ImplementationPlan,
Phase,
Chunk,
ChunkStatus,
PhaseType,
WorkflowType,
)
class TestCoordinatorInitialization:
"""Tests for SwarmCoordinator initialization."""
def test_creates_with_required_params(self):
"""Coordinator can be initialized with required params."""
spec_dir = Path("/tmp/test-spec")
project_dir = Path("/tmp/test-project")
coordinator = SwarmCoordinator(
spec_dir=spec_dir,
project_dir=project_dir,
)
assert coordinator.spec_dir == spec_dir
assert coordinator.project_dir == project_dir
def test_default_max_workers(self):
"""Default max_workers is 3."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
assert coordinator.max_workers == 3
def test_custom_max_workers(self):
"""Can set custom max_workers."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
max_workers=5,
)
assert coordinator.max_workers == 5
def test_empty_workers_and_files(self):
"""Starts with empty workers and claimed_files."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
assert len(coordinator.workers) == 0
assert len(coordinator.claimed_files) == 0
def test_progress_file_path(self):
"""Progress file is in spec_dir."""
spec_dir = Path("/tmp/test-spec")
coordinator = SwarmCoordinator(
spec_dir=spec_dir,
project_dir=Path("/tmp/test-project"),
)
assert coordinator.progress_file == spec_dir / "parallel_progress.json"
class TestGetAvailableChunks:
"""Tests for get_available_chunks method."""
def test_returns_empty_without_plan(self):
"""Returns empty list when no plan loaded."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
assert coordinator.get_available_chunks() == []
def test_returns_pending_chunks_from_available_phases(self):
"""Returns pending chunks from phases with met dependencies."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
# Create plan with one phase (no dependencies)
plan = ImplementationPlan(
feature="Test",
workflow_type=WorkflowType.FEATURE,
services_involved=["backend"],
)
chunk = Chunk(
id="chunk-1",
description="Test chunk",
status=ChunkStatus.PENDING,
)
phase = Phase(
phase=1,
name="Test Phase",
chunks=[chunk],
depends_on=[],
)
plan.phases = [phase]
coordinator.plan = plan
available = coordinator.get_available_chunks()
assert len(available) == 1
assert available[0] == (phase, chunk)
def test_excludes_completed_chunks(self):
"""Excludes chunks that are already completed."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
plan = ImplementationPlan(
feature="Test",
workflow_type=WorkflowType.FEATURE,
services_involved=["backend"],
)
chunk = Chunk(
id="chunk-1",
description="Test chunk",
status=ChunkStatus.COMPLETED,
)
phase = Phase(
phase=1,
name="Test Phase",
chunks=[chunk],
depends_on=[],
)
plan.phases = [phase]
coordinator.plan = plan
available = coordinator.get_available_chunks()
assert len(available) == 0
def test_excludes_chunks_with_claimed_files(self):
"""Excludes chunks whose files are already claimed."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
plan = ImplementationPlan(
feature="Test",
workflow_type=WorkflowType.FEATURE,
services_involved=["backend"],
)
chunk = Chunk(
id="chunk-1",
description="Test chunk",
files_to_modify=["app.py"],
status=ChunkStatus.PENDING,
)
phase = Phase(
phase=1,
name="Test Phase",
chunks=[chunk],
depends_on=[],
)
plan.phases = [phase]
coordinator.plan = plan
# Claim the file
coordinator.claimed_files["app.py"] = "other-worker"
available = coordinator.get_available_chunks()
assert len(available) == 0
class TestClaimChunk:
"""Tests for claim_chunk method."""
def test_claims_chunk_successfully(self):
"""Successfully claims an unclaimed chunk."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
plan = ImplementationPlan(
feature="Test",
workflow_type=WorkflowType.FEATURE,
services_involved=["backend"],
)
chunk = Chunk(
id="chunk-1",
description="Test chunk",
files_to_modify=["file1.py"],
files_to_create=["file2.py"],
status=ChunkStatus.PENDING,
)
phase = Phase(
phase=1,
name="Test Phase",
chunks=[chunk],
)
plan.phases = [phase]
coordinator.plan = plan
worktree_path = Path("/tmp/worktree")
branch_name = "worker-1/chunk-1"
success = coordinator.claim_chunk(
"worker-1", phase, chunk, worktree_path, branch_name
)
assert success is True
assert "file1.py" in coordinator.claimed_files
assert "file2.py" in coordinator.claimed_files
assert coordinator.claimed_files["file1.py"] == "worker-1"
assert chunk.status == ChunkStatus.IN_PROGRESS
def test_fails_to_claim_already_claimed_chunk(self):
"""Fails to claim a chunk that's already claimed."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
plan = ImplementationPlan(
feature="Test",
workflow_type=WorkflowType.FEATURE,
services_involved=["backend"],
)
chunk = Chunk(
id="chunk-1",
description="Test chunk",
files_to_modify=["file1.py"],
status=ChunkStatus.PENDING,
)
phase = Phase(
phase=1,
name="Test Phase",
chunks=[chunk],
)
plan.phases = [phase]
coordinator.plan = plan
# First worker claims
coordinator.claim_chunk(
"worker-1", phase, chunk, Path("/tmp/wt1"), "branch-1"
)
# Second worker tries to claim same chunk
success = coordinator.claim_chunk(
"worker-2", phase, chunk, Path("/tmp/wt2"), "branch-2"
)
assert success is False
def test_fails_when_files_already_claimed(self):
"""Fails when chunk's files are already claimed by another worker."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
plan = ImplementationPlan(
feature="Test",
workflow_type=WorkflowType.FEATURE,
services_involved=["backend"],
)
chunk = Chunk(
id="chunk-1",
description="Test chunk",
files_to_modify=["shared.py"],
status=ChunkStatus.PENDING,
)
phase = Phase(
phase=1,
name="Test Phase",
chunks=[chunk],
)
plan.phases = [phase]
coordinator.plan = plan
# Pre-claim the file
coordinator.claimed_files["shared.py"] = "other-worker"
success = coordinator.claim_chunk(
"worker-1", phase, chunk, Path("/tmp/wt"), "branch-1"
)
assert success is False
def test_creates_worker_assignment(self):
"""Creates WorkerAssignment when claiming chunk."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
plan = ImplementationPlan(
feature="Test",
workflow_type=WorkflowType.FEATURE,
services_involved=["backend"],
)
chunk = Chunk(
id="chunk-1",
description="Test chunk",
status=ChunkStatus.PENDING,
)
phase = Phase(
phase=1,
name="Test Phase",
chunks=[chunk],
)
plan.phases = [phase]
coordinator.plan = plan
worktree_path = Path("/tmp/worktree")
branch_name = "worker-1/chunk-1"
coordinator.claim_chunk("worker-1", phase, chunk, worktree_path, branch_name)
assert "worker-1" in coordinator.workers
assignment = coordinator.workers["worker-1"]
assert assignment.worker_id == "worker-1"
assert assignment.chunk_id == "chunk-1"
assert assignment.branch_name == branch_name
assert assignment.worktree_path == worktree_path
assert assignment.status == WorkerStatus.WORKING
class TestReleaseChunk:
"""Tests for release_chunk method."""
def test_releases_files_on_success(self):
"""Releases claimed files when chunk completes successfully."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
plan = ImplementationPlan(
feature="Test",
workflow_type=WorkflowType.FEATURE,
services_involved=["backend"],
)
chunk = Chunk(
id="chunk-1",
description="Test chunk",
files_to_modify=["file1.py"],
status=ChunkStatus.PENDING,
)
phase = Phase(
phase=1,
name="Test Phase",
chunks=[chunk],
)
plan.phases = [phase]
coordinator.plan = plan
# Claim chunk
coordinator.claim_chunk(
"worker-1", phase, chunk, Path("/tmp/wt"), "branch-1"
)
# Release chunk
coordinator.release_chunk("worker-1", "chunk-1", success=True)
assert "file1.py" not in coordinator.claimed_files
assert "worker-1" not in coordinator.workers
assert chunk.status == ChunkStatus.COMPLETED
def test_marks_chunk_failed_on_failure(self):
"""Marks chunk as failed when released with success=False."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
plan = ImplementationPlan(
feature="Test",
workflow_type=WorkflowType.FEATURE,
services_involved=["backend"],
)
chunk = Chunk(
id="chunk-1",
description="Test chunk",
status=ChunkStatus.PENDING,
)
phase = Phase(
phase=1,
name="Test Phase",
chunks=[chunk],
)
plan.phases = [phase]
coordinator.plan = plan
coordinator.claim_chunk(
"worker-1", phase, chunk, Path("/tmp/wt"), "branch-1"
)
coordinator.release_chunk("worker-1", "chunk-1", success=False, output="Error!")
assert chunk.status == ChunkStatus.FAILED
def test_ignores_unknown_worker(self):
"""Does nothing when releasing unknown worker."""
coordinator = SwarmCoordinator(
spec_dir=Path("/tmp/test-spec"),
project_dir=Path("/tmp/test-project"),
)
# Should not raise
coordinator.release_chunk("unknown-worker", "chunk-1", success=True)
class TestParallelGroup:
"""Tests for ParallelGroup dataclass."""
def test_validates_no_file_overlap(self):
"""Raises error when phases have overlapping files."""
phase1 = Phase(
phase=1,
name="Phase 1",
chunks=[
Chunk(
id="c1",
description="Chunk 1",
files_to_modify=["shared.py"],
)
],
)
phase2 = Phase(
phase=2,
name="Phase 2",
chunks=[
Chunk(
id="c2",
description="Chunk 2",
files_to_modify=["shared.py"], # Same file!
)
],
)
import pytest
with pytest.raises(ValueError, match="cannot run in parallel"):
ParallelGroup(phases=[phase1, phase2], all_dependencies_met=True)
def test_allows_non_overlapping_files(self):
"""Allows phases with different files."""
phase1 = Phase(
phase=1,
name="Phase 1",
chunks=[
Chunk(
id="c1",
description="Chunk 1",
files_to_modify=["file1.py"],
)
],
)
phase2 = Phase(
phase=2,
name="Phase 2",
chunks=[
Chunk(
id="c2",
description="Chunk 2",
files_to_modify=["file2.py"],
)
],
)
# Should not raise
group = ParallelGroup(phases=[phase1, phase2], all_dependencies_met=True)
assert len(group.phases) == 2
class TestWorkerAssignment:
"""Tests for WorkerAssignment dataclass."""
def test_to_dict(self):
"""Converts to dictionary correctly."""
assignment = WorkerAssignment(
worker_id="worker-1",
phase_id=1,
chunk_id="chunk-1",
branch_name="worker-1/chunk-1",
worktree_path=Path("/tmp/worktree"),
status=WorkerStatus.WORKING,
started_at="2024-01-01T10:00:00",
)
result = assignment.to_dict()
assert result["worker_id"] == "worker-1"
assert result["phase_id"] == 1
assert result["chunk_id"] == "chunk-1"
assert result["branch_name"] == "worker-1/chunk-1"
assert result["worktree_path"] == "/tmp/worktree"
assert result["status"] == "working"
assert result["started_at"] == "2024-01-01T10:00:00"
assert result["completed_at"] is None
class TestWorkerStatus:
"""Tests for WorkerStatus enum."""
def test_status_values(self):
"""Has expected status values."""
assert WorkerStatus.IDLE.value == "idle"
assert WorkerStatus.WORKING.value == "working"
assert WorkerStatus.COMPLETED.value == "completed"
assert WorkerStatus.FAILED.value == "failed"
# Legacy test functions for backwards compatibility
def test_coordinator_initialization():
"""Test coordinator can be initialized."""
spec_dir = Path("/tmp/test-spec")
project_dir = Path("/tmp/test-project")
coordinator = SwarmCoordinator(
spec_dir=spec_dir,
project_dir=project_dir,
max_workers=2,
)
assert coordinator.max_workers == 2
assert coordinator.spec_dir == spec_dir
assert coordinator.project_dir == project_dir
assert len(coordinator.workers) == 0
assert len(coordinator.claimed_files) == 0
def run_tests():
"""Run all tests."""
print("\nTesting Multi-Agent Parallelism Coordinator")
print("=" * 60)
try:
test_coordinator_initialization()
print("=" * 60)
print("✓ All tests passed!")
return 0
except AssertionError as e:
print(f"\n✗ Test failed: {e}")
import traceback
traceback.print_exc()
return 1
except Exception as e:
print(f"\n✗ Error: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(run_tests())
+49 -19
View File
@@ -22,10 +22,13 @@ from workspace import (
get_current_branch,
get_existing_build_worktree,
setup_workspace,
STAGING_WORKTREE_NAME,
)
from worktree import WorktreeManager
# Test constant - in the new per-spec architecture, each spec has its own worktree
# named after the spec itself. This constant is used for test assertions.
TEST_SPEC_NAME = "test-spec"
class TestWorkspaceMode:
"""Tests for WorkspaceMode enum."""
@@ -121,11 +124,11 @@ class TestGetExistingBuildWorktree:
def test_existing_worktree(self, temp_git_repo: Path):
"""Returns path when worktree exists."""
# Create the worktree directory structure
worktree_path = temp_git_repo / ".worktrees" / STAGING_WORKTREE_NAME
# Create the worktree directory structure (per-spec architecture)
worktree_path = temp_git_repo / ".worktrees" / TEST_SPEC_NAME
worktree_path.mkdir(parents=True)
result = get_existing_build_worktree(temp_git_repo, "test-spec")
result = get_existing_build_worktree(temp_git_repo, TEST_SPEC_NAME)
assert result == worktree_path
@@ -147,14 +150,15 @@ class TestSetupWorkspace:
"""Isolated mode creates worktree and returns manager."""
working_dir, manager = setup_workspace(
temp_git_repo,
"test-spec",
TEST_SPEC_NAME,
WorkspaceMode.ISOLATED,
)
assert working_dir != temp_git_repo
assert manager is not None
assert working_dir.exists()
assert working_dir.name == STAGING_WORKTREE_NAME
# Per-spec architecture: worktree is named after the spec
assert working_dir.name == TEST_SPEC_NAME
def test_setup_isolated_creates_worktrees_dir(self, temp_git_repo: Path):
"""Isolated mode creates .worktrees directory."""
@@ -170,11 +174,18 @@ class TestSetupWorkspace:
class TestWorkspaceUtilities:
"""Tests for workspace utility functions."""
def test_staging_worktree_name_constant(self):
"""STAGING_WORKTREE_NAME is defined."""
from workspace import STAGING_WORKTREE_NAME
assert STAGING_WORKTREE_NAME is not None
assert len(STAGING_WORKTREE_NAME) > 0
def test_per_spec_worktree_naming(self, temp_git_repo: Path):
"""Per-spec architecture uses spec name for worktree directory."""
spec_name = "my-spec-001"
working_dir, manager = setup_workspace(
temp_git_repo,
spec_name,
WorkspaceMode.ISOLATED,
)
# Worktree should be named after the spec
assert working_dir.name == spec_name
assert working_dir.parent.name == ".worktrees"
class TestWorkspaceIntegration:
@@ -325,23 +336,42 @@ class TestWorkspaceErrors:
)
class TestStagingWorktreeName:
"""Tests for staging worktree naming."""
class TestPerSpecWorktreeName:
"""Tests for per-spec worktree naming (new architecture)."""
def test_staging_name_consistent(self, temp_git_repo: Path):
"""Staging worktree name is consistent across setups."""
from workspace import STAGING_WORKTREE_NAME
def test_worktree_named_after_spec(self, temp_git_repo: Path):
"""Worktree is named after the spec."""
spec_name = "spec-1"
working_dir, _ = setup_workspace(
temp_git_repo,
spec_name,
WorkspaceMode.ISOLATED,
)
# Per-spec architecture: worktree directory matches spec name
assert working_dir.name == spec_name
def test_different_specs_get_different_worktrees(self, temp_git_repo: Path):
"""Different specs create separate worktrees."""
working_dir1, _ = setup_workspace(
temp_git_repo,
"spec-1",
WorkspaceMode.ISOLATED,
)
assert working_dir1.name == STAGING_WORKTREE_NAME
working_dir2, _ = setup_workspace(
temp_git_repo,
"spec-2",
WorkspaceMode.ISOLATED,
)
def test_staging_path_in_worktrees_dir(self, temp_git_repo: Path):
"""Staging worktree is created in .worktrees directory."""
# Each spec has its own worktree
assert working_dir1.name == "spec-1"
assert working_dir2.name == "spec-2"
assert working_dir1 != working_dir2
def test_worktree_path_in_worktrees_dir(self, temp_git_repo: Path):
"""Worktree is created in .worktrees directory."""
working_dir, _ = setup_workspace(
temp_git_repo,
"test-spec",