/** * Team Store - Multi-Agent Team Collaboration State Management * * Manages team orchestration, task assignment, Dev↔QA loops, * and real-time collaboration state. * * @module store/teamStore */ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { Team, TeamMember, TeamTask, TeamTaskStatus, TeamMemberRole, DevQALoop, DevQALoopState, CreateTeamRequest, AddTeamTaskRequest, TeamMetrics, CollaborationEvent, ReviewFeedback, TaskDeliverable, } from '../types/team'; // === Store State === interface TeamStoreState { // Data teams: Team[]; activeTeam: Team | null; metrics: TeamMetrics | null; // UI State isLoading: boolean; error: string | null; selectedTaskId: string | null; selectedMemberId: string | null; // Real-time events recentEvents: CollaborationEvent[]; // Actions - Team Management loadTeams: () => Promise; createTeam: (request: CreateTeamRequest) => Promise; deleteTeam: (teamId: string) => Promise; setActiveTeam: (team: Team | null) => void; // Actions - Member Management addMember: (teamId: string, agentId: string, role: TeamMemberRole) => Promise; removeMember: (teamId: string, memberId: string) => Promise; updateMemberRole: (teamId: string, memberId: string, role: TeamMemberRole) => Promise; // Actions - Task Management addTask: (request: AddTeamTaskRequest) => Promise; updateTaskStatus: (teamId: string, taskId: string, status: TeamTaskStatus) => Promise; assignTask: (teamId: string, taskId: string, memberId: string) => Promise; submitDeliverable: (teamId: string, taskId: string, deliverable: TaskDeliverable) => Promise; // Actions - Dev↔QA Loop startDevQALoop: (teamId: string, taskId: string, developerId: string, reviewerId: string) => Promise; submitReview: (teamId: string, loopId: string, feedback: Omit) => Promise; updateLoopState: (teamId: string, loopId: string, state: DevQALoopState) => Promise; // Actions - Events addEvent: (event: CollaborationEvent) => void; clearEvents: () => void; // Actions - UI setSelectedTask: (taskId: string | null) => void; setSelectedMember: (memberId: string | null) => void; clearError: () => void; } // === Helper Functions === const generateId = () => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const calculateMetrics = (team: Team): TeamMetrics => { const completedTasks = team.tasks.filter(t => t.status === 'completed'); const totalTasks = team.tasks.length; const reviewedTasks = completedTasks.filter(t => t.reviewFeedback); const avgCompletionTime = completedTasks.length > 0 ? completedTasks.reduce((sum, t) => { if (t.startedAt && t.completedAt) { return sum + (new Date(t.completedAt).getTime() - new Date(t.startedAt).getTime()); } return sum; }, 0) / completedTasks.length : 0; const approvedReviews = reviewedTasks.filter(t => t.reviewFeedback?.verdict === 'approved'); const passRate = reviewedTasks.length > 0 ? (approvedReviews.length / reviewedTasks.length) * 100 : 0; const totalIterations = team.activeLoops.reduce((sum, loop) => sum + loop.iterationCount, 0); const avgIterations = team.activeLoops.length > 0 ? totalIterations / team.activeLoops.length : 0; const escalations = team.activeLoops.filter(loop => loop.state === 'escalated').length; const efficiency = totalTasks > 0 ? Math.min(100, (completedTasks.length / totalTasks) * 100 * (passRate / 100)) : 0; return { tasksCompleted: completedTasks.length, avgCompletionTime, passRate, avgIterations, escalations, efficiency, }; }; // === Store Implementation === export const useTeamStore = create()( persist( (set, get) => ({ // Initial State teams: [], activeTeam: null, metrics: null, isLoading: false, error: null, selectedTaskId: null, selectedMemberId: null, recentEvents: [], // Team Management loadTeams: async () => { set({ isLoading: true, error: null }); try { // For now, load from localStorage until API is available // Note: persist middleware stores data as { state: { teams: [...] }, version: ... } const stored = localStorage.getItem('zclaw-teams'); let teams: Team[] = []; if (stored) { const parsed = JSON.parse(stored); // Handle persist middleware format if (parsed?.state?.teams && Array.isArray(parsed.state.teams)) { teams = parsed.state.teams; } else if (Array.isArray(parsed)) { // Direct array format (legacy) teams = parsed; } } set({ teams, isLoading: false }); } catch (error) { console.error('[TeamStore] Failed to load teams:', error); set({ teams: [], isLoading: false }); } }, createTeam: async (request: CreateTeamRequest) => { set({ isLoading: true, error: null }); try { const now = new Date().toISOString(); const members: TeamMember[] = request.memberAgents.map((m) => ({ id: generateId(), agentId: m.agentId, name: `Agent-${m.agentId.slice(0, 4)}`, role: m.role, skills: [], workload: 0, status: 'idle', maxConcurrentTasks: m.role === 'orchestrator' ? 5 : 2, currentTasks: [], })); const team: Team = { id: generateId(), name: request.name, description: request.description, members, tasks: [], pattern: request.pattern, activeLoops: [], status: 'active', createdAt: now, updatedAt: now, }; set(state => { const teams = [...state.teams, team]; localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: team, isLoading: false }; }); return team; } catch (error) { set({ error: (error as Error).message, isLoading: false }); return null; } }, deleteTeam: async (teamId: string) => { set({ isLoading: true, error: null }); try { set(state => { const teams = state.teams.filter(t => t.id !== teamId); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === teamId ? null : state.activeTeam, isLoading: false }; }); return true; } catch (error) { set({ error: (error as Error).message, isLoading: false }); return false; } }, setActiveTeam: (team: Team | null) => { set(() => ({ activeTeam: team, metrics: team ? calculateMetrics(team) : null, })); }, // Member Management addMember: async (teamId: string, agentId: string, role: TeamMemberRole) => { const state = get(); const team = state.teams.find(t => t.id === teamId); if (!team) return null; const member: TeamMember = { id: generateId(), agentId, name: `Agent-${agentId.slice(0, 4)}`, role, skills: [], workload: 0, status: 'idle', maxConcurrentTasks: role === 'orchestrator' ? 5 : 2, currentTasks: [], }; const updatedTeam = { ...team, members: [...team.members, member], updatedAt: new Date().toISOString(), }; set(state => { const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam, }; }); return member; }, removeMember: async (teamId: string, memberId: string) => { const state = get(); const team = state.teams.find(t => t.id === teamId); if (!team) return false; const updatedTeam = { ...team, members: team.members.filter(m => m.id !== memberId), updatedAt: new Date().toISOString(), }; set(state => { const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam, }; }); return true; }, updateMemberRole: async (teamId: string, memberId: string, role: TeamMemberRole) => { const state = get(); const team = state.teams.find(t => t.id === teamId); if (!team) return false; const updatedTeam = { ...team, members: team.members.map(m => m.id === memberId ? { ...m, role, maxConcurrentTasks: role === 'orchestrator' ? 5 : 2 } : m ), updatedAt: new Date().toISOString(), }; set(state => { const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam, }; }); return true; }, // Task Management addTask: async (request: AddTeamTaskRequest) => { const state = get(); const team = state.teams.find(t => t.id === request.teamId); if (!team) return null; const now = new Date().toISOString(); const task: TeamTask = { id: generateId(), title: request.title, description: request.description, status: request.assigneeId ? 'assigned' : 'pending', priority: request.priority, assigneeId: request.assigneeId, dependencies: request.dependencies || [], type: request.type, estimate: request.estimate, createdAt: now, updatedAt: now, }; const updatedTeam = { ...team, tasks: [...team.tasks, task], updatedAt: now, }; set(state => { const teams = state.teams.map(t => t.id === request.teamId ? updatedTeam : t); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === request.teamId ? updatedTeam : state.activeTeam, metrics: state.activeTeam?.id === request.teamId ? calculateMetrics(updatedTeam) : state.metrics, }; }); return task; }, updateTaskStatus: async (teamId: string, taskId: string, status: TeamTaskStatus) => { const state = get(); const team = state.teams.find(t => t.id === teamId); if (!team) return false; const now = new Date().toISOString(); const updatedTeam = { ...team, tasks: team.tasks.map(t => { if (t.id !== taskId) return t; const updates: Partial = { status, updatedAt: now }; if (status === 'in_progress' && !t.startedAt) { updates.startedAt = now; } if (status === 'completed') { updates.completedAt = now; } return { ...t, ...updates }; }), updatedAt: now, }; set(state => { const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam, metrics: state.activeTeam?.id === teamId ? calculateMetrics(updatedTeam) : state.metrics, }; }); return true; }, assignTask: async (teamId: string, taskId: string, memberId: string) => { const state = get(); const team = state.teams.find(t => t.id === teamId); if (!team) return false; const now = new Date().toISOString(); const updatedTeam = { ...team, tasks: team.tasks.map(t => t.id === taskId ? { ...t, assigneeId: memberId, status: 'assigned' as TeamTaskStatus, updatedAt: now } : t ), members: team.members.map(m => m.id === memberId ? { ...m, currentTasks: [...m.currentTasks, taskId], workload: (m.workload + 25) } : m ), updatedAt: now, }; set(state => { const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam, }; }); return true; }, submitDeliverable: async (teamId: string, taskId: string, deliverable: TaskDeliverable) => { const state = get(); const team = state.teams.find(t => t.id === teamId); if (!team) return false; const now = new Date().toISOString(); const updatedTeam = { ...team, tasks: team.tasks.map(t => t.id === taskId ? { ...t, deliverable, status: 'review' as TeamTaskStatus, updatedAt: now } : t ), updatedAt: now, }; set(state => { const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam, }; }); return true; }, // Dev↔QA Loop startDevQALoop: async (teamId: string, taskId: string, developerId: string, reviewerId: string) => { const state = get(); const team = state.teams.find(t => t.id === teamId); if (!team) return null; const now = new Date().toISOString(); const loop: DevQALoop = { id: generateId(), developerId, reviewerId, taskId, state: 'developing', iterationCount: 0, maxIterations: 3, feedbackHistory: [], startedAt: now, lastUpdatedAt: now, }; const updatedTeam = { ...team, activeLoops: [...team.activeLoops, loop], updatedAt: now, }; set(state => { const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam, }; }); return loop; }, submitReview: async (teamId: string, loopId: string, feedback: Omit) => { const state = get(); const team = state.teams.find(t => t.id === teamId); if (!team) return false; const loop = team.activeLoops.find(l => l.id === loopId); if (!loop) return false; const now = new Date().toISOString(); const fullFeedback: ReviewFeedback = { ...feedback, reviewedAt: now, reviewerId: loop.reviewerId, }; let newState: DevQALoopState; let newIterationCount = loop.iterationCount; if (feedback.verdict === 'approved') { newState = 'approved'; } else if (newIterationCount >= loop.maxIterations - 1) { newState = 'escalated'; } else { newState = 'revising'; newIterationCount++; } const updatedTeam = { ...team, tasks: team.tasks.map(t => t.id === loop.taskId ? { ...t, reviewFeedback: fullFeedback, updatedAt: now } : t ), activeLoops: team.activeLoops.map(l => l.id === loopId ? { ...l, state: newState, iterationCount: newIterationCount, feedbackHistory: [...l.feedbackHistory, fullFeedback], lastUpdatedAt: now, } : l ), updatedAt: now, }; set(state => { const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam, metrics: state.activeTeam?.id === teamId ? calculateMetrics(updatedTeam) : state.metrics, }; }); return true; }, updateLoopState: async (teamId: string, loopId: string, state: DevQALoopState) => { const teamStore = get(); const team = teamStore.teams.find(t => t.id === teamId); if (!team) return false; const now = new Date().toISOString(); const updatedTeam = { ...team, activeLoops: team.activeLoops.map(l => l.id === loopId ? { ...l, state, lastUpdatedAt: now } : l ), updatedAt: now, }; set(state => { const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t); localStorage.setItem('zclaw-teams', JSON.stringify(teams)); return { teams, activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam, }; }); return true; }, // Events addEvent: (event: CollaborationEvent) => { set(state => ({ recentEvents: [event, ...state.recentEvents].slice(0, 100), })); }, clearEvents: () => { set({ recentEvents: [] }); }, // UI setSelectedTask: (taskId: string | null) => { set({ selectedTaskId: taskId }); }, setSelectedMember: (memberId: string | null) => { set({ selectedMemberId: memberId }); }, clearError: () => { set({ error: null }); }, }), { name: 'zclaw-teams', partialize: (state) => ({ teams: state.teams, activeTeam: state.activeTeam, }), }, ));