import os import json import datetime import re # ========================================== # 1. Constants & Configuration (Ported from lib/core & lib/pdca) # ========================================== PDCA_PHASES = { 'PLAN': {'order': 1, 'name': 'Plan', 'icon': 'πŸ“‹'}, 'DESIGN': {'order': 2, 'name': 'Design', 'icon': 'πŸ“'}, 'DO': {'order': 3, 'name': 'Do', 'icon': 'πŸ”¨'}, 'CHECK': {'order': 4, 'name': 'Check', 'icon': 'πŸ”'}, 'ACT': {'order': 5, 'name': 'Act', 'icon': 'πŸ”„'}, 'REPORT': {'order': 6, 'name': 'Report', 'icon': 'πŸ“Š'} } LEVEL_PHASE_MAP = { "Starter": { "required": ["PLAN", "DO", "CHECK"], "optional": ["DESIGN"], "skip": ["PHASE-1", "PHASE-2", "PHASE-4", "PHASE-7", "PHASE-9"] }, "Dynamic": { "required": ["PLAN", "DESIGN", "DO", "CHECK", "REPORT"], "optional": ["PHASE-3", "PHASE-5", "PHASE-6"], "skip": ["PHASE-9"] }, "Enterprise": { "required": ["PLAN", "DESIGN", "DO", "CHECK", "REPORT", "PHASE-1", "PHASE-2", "PHASE-3", "PHASE-4", "PHASE-5", "PHASE-6", "PHASE-7", "PHASE-8", "PHASE-9"], "optional": [], "skip": [] } } TEAM_STRATEGIES = { "Starter": None, "Dynamic": { "teammates": 3, "cto_agent": "cto-lead", "roles": [ {"name": "developer", "agents": ["bkend-expert"], "phases": ["DO", "ACT"]}, {"name": "frontend", "agents": ["frontend-architect"], "phases": ["DESIGN", "DO"]}, {"name": "qa", "agents": ["qa-monitor", "gap-detector"], "phases": ["CHECK"]} ] }, "Enterprise": { "teammates": 5, "cto_agent": "cto-lead", "roles": [ {"name": "architect", "agents": ["enterprise-expert", "infra-architect"], "phases": ["DESIGN"]}, {"name": "developer", "agents": ["bkend-expert", "frontend-architect"], "phases": ["DO", "ACT"]}, {"name": "qa", "agents": ["qa-strategist", "qa-monitor", "gap-detector"], "phases": ["CHECK"]}, {"name": "reviewer", "agents": ["code-analyzer", "design-validator"], "phases": ["CHECK", "ACT"]}, {"name": "security", "agents": ["security-architect"], "phases": ["DESIGN", "CHECK"]} ] } } # ========================================== # 2. State Engine Logic # ========================================== class ContextManager: """ Manages global state, feature context, and project level detection. Ported from lib/task/context.js & lib/pdca/level.js """ def __init__(self, state_file_path): self.state_file_path = state_file_path self.state = self._load_state() def _load_state(self): if not os.path.exists(self.state_file_path): return { "project_name": "Antigravity Project", "project_level": "Dynamic", # Default "current_feature": None, "features": {}, "team_config": { "enabled": False, "max_teammates": 4 }, "last_bootup": None } try: with open(self.state_file_path, 'r', encoding='utf-8') as f: return json.load(f) except json.JSONDecodeError: return self._load_state() # Recursive retry with default if corrupt (handled by external reset usually) def _save_state(self): with open(self.state_file_path, 'w', encoding='utf-8') as f: json.dump(self.state, f, indent=2, ensure_ascii=False) def detect_project_level(self, project_root="."): """ Ported from lib/pdca/level.js: detectLevel() """ try: files = os.listdir(project_root) files_str = " ".join(files).lower() # Enterprise Markers if any(m in files_str for m in ['kubernetes', 'k8s', 'terraform', 'microservices', 'docker-compose']): self.state['project_level'] = "Enterprise" return "Enterprise" # Dynamic Markers if any(m in files for m in ['package.json', 'requirements.txt', 'go.mod', 'Cargo.toml', 'pom.xml']): self.state['project_level'] = "Dynamic" return "Dynamic" except Exception: pass self.state['project_level'] = "Starter" return "Starter" def set_active_feature(self, feature_name): self.state['current_feature'] = feature_name if feature_name and feature_name not in self.state['features']: self.state['features'][feature_name] = { "phase": "PLAN", "match_rate": 0, "history": [] } self._save_state() def get_active_feature(self): return self.state.get('current_feature') def get_feature_status(self, feature_name): return self.state.get('features', {}).get(feature_name) class PDCAStatus: """ Manages Phase Transitions and Deliverable Checks. Ported from lib/pdca/phase.js """ def __init__(self, context_manager): self.ctx = context_manager def validate_transition(self, feature_name, to_phase): """ Ported from lib/pdca/phase.js: validatePdcaTransition() NOW INCLUDES LEVEL-BASED VALIDATION """ feature = self.ctx.get_feature_status(feature_name) if not feature: return {"valid": True, "reason": "New feature"} from_phase = feature.get('phase', 'PLAN').upper() to_phase = to_phase.upper() from_order = PDCA_PHASES.get(from_phase, {}).get('order', 0) to_order = PDCA_PHASES.get(to_phase, {}).get('order', 0) # Allow going back if to_order < from_order: return {"valid": True, "reason": "Returning to earlier phase"} # Check Level Requirements level = self.ctx.state.get("project_level", "Dynamic") level_config = LEVEL_PHASE_MAP.get(level, LEVEL_PHASE_MAP["Dynamic"]) # Iterate phases between from_phase and to_phase # e.g. PLAN(1) -> DO(3). Check if DESIGN(2) is required. for i in range(from_order + 1, to_order): intermediate_phase_name = "" for name, info in PDCA_PHASES.items(): if info['order'] == i: intermediate_phase_name = name break if intermediate_phase_name and intermediate_phase_name in level_config['required']: return {"valid": False, "reason": f"Cannot skip required phase: {intermediate_phase_name} ({level} Level)"} # Cannot skip more than 1 phase generally (sanity check) if to_order > from_order + 1: # But allow if intermediate is NOT required # The loop above handles the "Required" check. # If we are here, it means intermediate phases were optional or skipped. pass return {"valid": True, "reason": "Transition allowed"} def update_phase(self, feature_name, phase, match_rate=None): phase = phase.upper() # Normalize # Validate first validation = self.validate_transition(feature_name, phase) if not validation['valid']: print(f"Warning: Invalid transition - {validation['reason']}") # We enforce it anyway for now as agent override, but log it feature = self.ctx.get_feature_status(feature_name) if feature: feature['phase'] = phase if match_rate is not None: feature['match_rate'] = match_rate entry = { "timestamp": datetime.datetime.now().isoformat(), "action": "phase_update", "phase": phase, "match_rate": match_rate } feature['history'].append(entry) self.ctx._save_state() class Guardrails: """ Implements safety checks like Loop Detection. Ported from hmcmkit (Hybrid JS -> Python approach) """ def __init__(self, context_manager): self.ctx = context_manager def check_loop(self, feature_name): feature = self.ctx.get_feature_status(feature_name) if not feature or 'history' not in feature: return {"safe": True} recent = feature['history'][-10:] if len(feature['history']) >= 10 else feature['history'] check_act_count = sum(1 for h in recent if h.get('phase', '') in ['CHECK', 'ACT']) if check_act_count >= 8: # 4 cycles of check-act return {"safe": False, "reason": "λ¬΄ν•œ 루프 감지: CHECK/ACT 반볡이 4회λ₯Ό μ΄ˆκ³Όν–ˆμŠ΅λ‹ˆλ‹€. μžλ™ μˆ˜μ •μ„ λ©ˆμΆ”κ³  μ‚¬μš©μžμ—κ²Œ λ³΄κ³ ν•˜μ„Έμš”."} return {"safe": True} class TeamCoordinator: """ Manages Team Strategy and Roles. Ported from lib/team/strategy.js & lib/team/coordinator.js """ def __init__(self, context_manager): self.ctx = context_manager def get_team_strategy(self): level = self.ctx.state.get("project_level", "Dynamic") return TEAM_STRATEGIES.get(level, TEAM_STRATEGIES["Dynamic"]) def suggest_team_mode(self, user_message): """ Ported from lib/team/coordinator.js: suggestTeamMode() """ level = self.ctx.state.get("project_level", "Dynamic") if level == "Starter": return {"suggest": False, "reason": "Starter level does not support teams"} # Logic: Message > 1000 chars or Keyword trigger if len(user_message) >= 1000: return {"suggest": True, "reason": "Long message detected (Major Feature)"} keywords = ["team", "project lead", "cto", "role", "assign"] if any(k in user_message.lower() for k in keywords): return {"suggest": True, "reason": "Team keywords detected"} return {"suggest": False} def get_roles_for_phase(self, phase): strategy = self.get_team_strategy() if not strategy: return [] phase = phase.upper() active_roles = [] for role in strategy['roles']: if phase in role['phases']: active_roles.append(role) return active_roles if __name__ == "__main__": # Test Suite # mgr = ContextManager(".hmcmkit/state.json") pass