diff --git a/ai_client.py b/ai_client.py index 5f6bd6b..ea348a0 100644 --- a/ai_client.py +++ b/ai_client.py @@ -261,7 +261,7 @@ def set_provider(provider: str, model: str) -> None: if provider == "gemini_cli": valid_models = _list_gemini_cli_models() # If model is invalid or belongs to another provider (like deepseek), force default - if model not in valid_models or model.startswith("deepseek"): + if model != "mock" and (model not in valid_models or model.startswith("deepseek")): _model = "gemini-3-flash-preview" else: _model = model @@ -815,8 +815,8 @@ def _send_gemini_cli(md_content: str, user_message: str, base_dir: str, global _gemini_cli_adapter try: if _gemini_cli_adapter is None: - _gemini_cli_adapter = GeminiCliAdapter(binary_path="gemini") - adapter = _gemini_cli_adapter + _gemini_cli_adapter = GeminiCliAdapter(binary_path="gemini") + adapter = _gemini_cli_adapter mcp_client.configure(file_items or [], [base_dir]) # Construct the system instruction, combining the base system prompt and the current context. sys_instr = f"{_get_combined_system_prompt()}\n\n\n{md_content}\n" @@ -1621,16 +1621,16 @@ from typing import Any, Callable, Optional, List # and the _send_xxx functions are also defined at module level. def send( - md_content: str, - user_message: str, - base_dir: str = ".", - file_items: list[dict[str, Any]] | None = None, - discussion_history: str = "", - stream: bool = False, - pre_tool_callback: Optional[Callable[[str], bool]] = None, - qa_callback: Optional[Callable[[str], str]] = None, + md_content: str, + user_message: str, + base_dir: str = ".", + file_items: list[dict[str, Any]] | None = None, + discussion_history: str = "", + stream: bool = False, + pre_tool_callback: Optional[Callable[[str], bool]] = None, + qa_callback: Optional[Callable[[str], str]] = None, ) -> str: - """ + """ Send a message to the active provider. md_content : aggregated markdown string (for Gemini: stable content only, @@ -1645,73 +1645,39 @@ def send( pre_tool_callback : Optional callback (payload: str) -> bool called before tool execution qa_callback : Optional callback (stderr: str) -> str called for Tier 4 error analysis """ - # --- START MOCK LOGIC --- - # Assuming _model, _custom_system_prompt, and _system_prompt are module-level variables. - # If _model is not 'mock', proceed to original provider logic. - if _model == 'mock': - mock_response_content = None - # Use _custom_system_prompt for keyword detection - current_system_prompt = _custom_system_prompt # Assuming _custom_system_prompt is accessible and defined + # --- START MOCK LOGIC --- + if _model == 'mock': + import json + keyword = "unknown" + if 'Epic Initialization' in _custom_system_prompt: + keyword = "Epic Initialization" + mock_response_content = [ + {"id": "mock-track-1", "type": "Track", "module": "core", "persona": "Tech Lead", "severity": "Medium", "goal": "Mock Goal 1", "acceptance_criteria": ["criteria 1"]}, + {"id": "mock-track-2", "type": "Track", "module": "ui", "persona": "Frontend Lead", "severity": "Low", "goal": "Mock Goal 2", "acceptance_criteria": ["criteria 2"]} + ] + elif 'Sprint Planning' in _custom_system_prompt: + keyword = "Sprint Planning" + mock_response_content = [ + {"id": "mock-ticket-1", "type": "Ticket", "goal": "Mock Ticket 1", "target_file": "file1.py", "depends_on": [], "context_requirements": "req 1"}, + {"id": "mock-ticket-2", "type": "Ticket", "goal": "Mock Ticket 2", "target_file": "file2.py", "depends_on": ["mock-ticket-1"], "context_requirements": "req 2"} + ] + else: + mock_response_content = "Mock AI Response" - if 'tier1_epic_init' in current_system_prompt: - mock_response_content = [ - { - "id": "mock-track-1", - "type": "epic", - "module": "conductor", - "persona": "Tier 1 Orchestrator", - "severity": "high", - "goal": "Initialize a new track.", - "acceptance_criteria": "Track created successfully with required fields." - }, - { - "id": "mock-track-2", - "type": "epic", - "module": "conductor", - "persona": "Tier 1 Orchestrator", - "severity": "medium", - "goal": "Initialize another track.", - "acceptance_criteria": "Second track created successfully." - } - ] - elif 'tier2_sprint_planning' in current_system_prompt: - mock_response_content = [ - { - "id": "mock-ticket-1", - "type": "story", - "goal": "Implement feature X.", - "target_file": "src/feature_x.py", - "depends_on": [], - "context_requirements": ["requirements.txt", "main.py"] - }, - { - "id": "mock-ticket-2", - "type": "bug", - "goal": "Fix bug Y.", - "target_file": "src/bug_y.py", - "depends_on": ["mock-ticket-1"], - "context_requirements": ["tests/test_bug_y.py"] - } - ] - else: - mock_response_content = "Mock AI Response" - - # The function is typed to return 'str', so we return a JSON string. - # Ensure 'json' is imported at the module level. - return json.dumps(mock_response_content) - # --- END MOCK LOGIC --- - - with _send_lock: - if _provider == "gemini": - return _send_gemini(md_content, user_message, base_dir, file_items, discussion_history, pre_tool_callback, qa_callback) - elif _provider == "gemini_cli": - return _send_gemini_cli(md_content, user_message, base_dir, file_items, discussion_history, pre_tool_callback, qa_callback) - elif _provider == "anthropic": - return _send_anthropic(md_content, user_message, base_dir, file_items, discussion_history, pre_tool_callback, qa_callback) - elif _provider == "deepseek": - return _send_deepseek(md_content, user_message, base_dir, file_items, discussion_history, stream=stream, pre_tool_callback=pre_tool_callback, qa_callback=qa_callback) - raise ValueError(f"unknown provider: {_provider}") + print(f"[MOCK AI] Triggered for prompt keyword: {keyword}") + return json.dumps(mock_response_content) + # --- END MOCK LOGIC --- + with _send_lock: + if _provider == "gemini": + return _send_gemini(md_content, user_message, base_dir, file_items, discussion_history, pre_tool_callback, qa_callback) + elif _provider == "gemini_cli": + return _send_gemini_cli(md_content, user_message, base_dir, file_items, discussion_history, pre_tool_callback, qa_callback) + elif _provider == "anthropic": + return _send_anthropic(md_content, user_message, base_dir, file_items, discussion_history, pre_tool_callback, qa_callback) + elif _provider == "deepseek": + return _send_deepseek(md_content, user_message, base_dir, file_items, discussion_history, stream=stream, pre_tool_callback=pre_tool_callback, qa_callback=qa_callback) + raise ValueError(f"unknown provider: {_provider}") def get_history_bleed_stats(md_content: str | None = None) -> dict[str, Any]: """ Calculates how close the current conversation history is to the token limit. diff --git a/api_hooks.py b/api_hooks.py index 23d7e16..9a1e355 100644 --- a/api_hooks.py +++ b/api_hooks.py @@ -128,7 +128,8 @@ class HookHandler(BaseHTTPRequestHandler): result["active_track"] = getattr(app, "active_track", None) result["active_tickets"] = getattr(app, "active_tickets", []) result["mma_step_mode"] = getattr(app, "mma_step_mode", False) - result["pending_approval"] = app._pending_mma_approval is not None + result["pending_approval"] = (getattr(app, "_pending_mma_approval", None) is not None) or getattr(app, "_pending_ask_dialog", False) + result["pending_spawn"] = getattr(app, "_pending_mma_spawn", None) is not None # Added lines for tracks and proposed_tracks result["tracks"] = getattr(app, "tracks", []) result["proposed_tracks"] = getattr(app, "proposed_tracks", []) diff --git a/conductor/tracks/track_687e9551/state.toml b/conductor/tracks/track_687e9551/state.toml new file mode 100644 index 0000000..5578754 --- /dev/null +++ b/conductor/tracks/track_687e9551/state.toml @@ -0,0 +1,28 @@ +discussion = [] + +[metadata] +id = "track_687e9551" +name = "Mock Goal 1" +status = "todo" +created_at = "2026-02-28T22:02:39.923670" +updated_at = "2026-02-28T22:02:39.923670" + +[[tasks]] +id = "mock-ticket-1" +description = "Mock Ticket 1" +status = "todo" +assigned_to = "unassigned" +context_requirements = [] +depends_on = [] +step_mode = false + +[[tasks]] +id = "mock-ticket-2" +description = "Mock Ticket 2" +status = "todo" +assigned_to = "unassigned" +context_requirements = [] +depends_on = [ + "mock-ticket-1", +] +step_mode = false diff --git a/conductor/tracks/track_fac6038b/state.toml b/conductor/tracks/track_fac6038b/state.toml new file mode 100644 index 0000000..16b009c --- /dev/null +++ b/conductor/tracks/track_fac6038b/state.toml @@ -0,0 +1,28 @@ +discussion = [] + +[metadata] +id = "track_fac6038b" +name = "Mock Goal 2" +status = "todo" +created_at = "2026-02-28T22:02:39.949060" +updated_at = "2026-02-28T22:02:39.949060" + +[[tasks]] +id = "mock-ticket-1" +description = "Mock Ticket 1" +status = "todo" +assigned_to = "unassigned" +context_requirements = [] +depends_on = [] +step_mode = false + +[[tasks]] +id = "mock-ticket-2" +description = "Mock Ticket 2" +status = "todo" +assigned_to = "unassigned" +context_requirements = [] +depends_on = [ + "mock-ticket-1", +] +step_mode = false diff --git a/gui_2.py b/gui_2.py index e2c032e..72247b7 100644 --- a/gui_2.py +++ b/gui_2.py @@ -387,6 +387,8 @@ class App: 'btn_mma_plan_epic': self._cb_plan_epic, 'btn_mma_accept_tracks': self._cb_accept_tracks, 'btn_mma_start_track': self._cb_start_track, + 'btn_approve_tool': self._handle_approve_tool, + 'btn_approve_spawn': self._handle_approve_spawn, } self._predefined_callbacks: dict[str, Callable[..., Any]] = { '_test_callback_func_write_to_file': self._test_callback_func_write_to_file @@ -886,6 +888,8 @@ class App: if item in self._settable_fields: attr_name = self._settable_fields[item] setattr(self, attr_name, value) + if item == "current_provider" or item == "current_model": + ai_client.set_provider(self.current_provider, self.current_model) if item == "gcli_path": if not ai_client._gemini_cli_adapter: ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=value) @@ -984,6 +988,22 @@ class App: else: print("[DEBUG] No pending dialog to reject") + def _handle_approve_tool(self) -> None: + """Logic for approving a pending tool execution via API hooks.""" + print("[DEBUG] _handle_approve_tool called") + if self._pending_ask: + self._handle_approve_ask() + else: + print("[DEBUG] No pending tool approval found") + + def _handle_approve_spawn(self) -> None: + """Logic for approving a pending sub-agent spawn via API hooks.""" + print("[DEBUG] _handle_approve_spawn called") + if self._pending_mma_spawn: + self._handle_mma_respond(approved=True, prompt=self._mma_spawn_prompt, context_md=self._mma_spawn_context) + else: + print("[DEBUG] No pending spawn approval found") + def _handle_mma_respond(self, approved: bool, payload: str = None, abort: bool = False, prompt: str = None, context_md: str = None) -> None: if self._pending_mma_approval: dlg = self._pending_mma_approval.get("dialog_container", [None])[0] @@ -2054,7 +2074,7 @@ class App: step_mode=t_data.get("step_mode", False) ) tickets.append(ticket) - track_id = f"track_{uuid.uuid4().hex[:8]}" + track_id = f"track_{uuid.uuid5(uuid.NAMESPACE_DNS, f'{self.active_project_path}_{title}_{now.isoformat()}').hex[:12]}" track = Track(id=track_id, description=title, tickets=tickets) # Initialize track state in the filesystem from models import TrackState, Metadata diff --git a/scripts/mma_exec.py b/scripts/mma_exec.py index 2184a61..021bf4b 100644 --- a/scripts/mma_exec.py +++ b/scripts/mma_exec.py @@ -65,7 +65,7 @@ def get_model_for_role(role: str, failure_count: int = 0) -> str: return 'gemini-3-flash-preview' elif role == 'tier3-worker' or role == 'tier3': if failure_count > 1: - return 'gemini-3-flash' + return 'gemini-3-flash-preview' return 'gemini-2.5-flash-lite' elif role == 'tier4-qa' or role == 'tier4': return 'gemini-2.5-flash-lite' diff --git a/tests/temp_project_history.toml b/tests/temp_project_history.toml index 39bc435..9e7eeac 100644 --- a/tests/temp_project_history.toml +++ b/tests/temp_project_history.toml @@ -10,5 +10,8 @@ auto_add = true [discussions.main] git_commit = "" -last_updated = "2026-02-28T21:27:02" -history = [] +last_updated = "2026-02-28T22:08:57" +history = [ + "@2026-02-28T22:02:40\nSystem:\n[PERFORMANCE ALERT] CPU usage high: 83.5%. Please consider optimizing recent changes or reducing load.", + "@2026-02-28T22:03:10\nSystem:\n[PERFORMANCE ALERT] CPU usage high: 103.9%. Please consider optimizing recent changes or reducing load.", +] diff --git a/tests/visual_sim_mma_v2.py b/tests/visual_sim_mma_v2.py index 616712f..48e5741 100644 --- a/tests/visual_sim_mma_v2.py +++ b/tests/visual_sim_mma_v2.py @@ -32,8 +32,13 @@ def test_mma_complete_lifecycle(live_gui) -> None: for _ in range(60): # Poll for up to 60 seconds status = client.get_mma_status() print(f"Polling status: {status}") - # Assuming 'ai_status' might be a key within the status dictionary. If not, this needs adjustment. print(f"Polling ai_status: {status.get('ai_status', 'N/A')}") + if status and status.get('pending_spawn') is True: + print('[SIM] Worker spawn required. Clicking btn_approve_spawn...') + client.click('btn_approve_spawn') + elif status and status.get('pending_approval') is True: + print('[SIM] Tool approval required. Clicking btn_approve_tool...') + client.click('btn_approve_tool') if status and status.get('proposed_tracks') and len(status['proposed_tracks']) > 0: proposed_tracks_found = True break @@ -47,6 +52,12 @@ def test_mma_complete_lifecycle(live_gui) -> None: tracks_populated = False for _ in range(30): # Poll for up to 30 seconds status = client.get_mma_status() + if status and status.get('pending_spawn') is True: + print('[SIM] Worker spawn required. Clicking btn_approve_spawn...') + client.click('btn_approve_spawn') + elif status and status.get('pending_approval') is True: + print('[SIM] Tool approval required. Clicking btn_approve_tool...') + client.click('btn_approve_tool') if status and status.get('tracks') and len(status['tracks']) > 0: tracks_populated = True break