checkpoint: mma_orchestrator track
This commit is contained in:
@@ -1,33 +1,33 @@
|
|||||||
# Implementation Plan: MMA Orchestrator Integration
|
# Implementation Plan: MMA Orchestrator Integration
|
||||||
|
|
||||||
## Phase 1: Tier 1 Strategic PM Implementation
|
## Phase 1: Tier 1 Strategic PM Implementation
|
||||||
- [ ] Task: PM Planning Hook
|
- [x] Task: PM Planning Hook
|
||||||
- [ ] Create `orchestrator_pm.py` to handle the Tier 1 Strategic prompt.
|
- [x] Create `orchestrator_pm.py` to handle the Tier 1 Strategic prompt.
|
||||||
- [ ] Implement the `generate_tracks(user_request, repo_map)` function.
|
- [x] Implement the `generate_tracks(user_request, repo_map)` function.
|
||||||
- [ ] Task: Project History Aggregation
|
- [x] Task: Project History Aggregation
|
||||||
- [ ] Summarize past track results to provide context for new epics.
|
- [x] Summarize past track results to provide context for new epics.
|
||||||
|
|
||||||
## Phase 2: Tier 2 Tactical Dispatcher Implementation
|
## Phase 2: Tier 2 Tactical Dispatcher Implementation
|
||||||
- [ ] Task: Tech Lead Dispatcher Hook
|
- [x] Task: Tech Lead Dispatcher Hook
|
||||||
- [ ] Create `conductor_tech_lead.py` to handle the Tier 2 Dispatcher prompt.
|
- [x] Create `conductor_tech_lead.py` to handle the Tier 2 Dispatcher prompt.
|
||||||
- [ ] Implement the `generate_tickets(track_brief, module_skeletons)` function.
|
- [x] Implement the `generate_tickets(track_brief, module_skeletons)` function.
|
||||||
- [ ] Task: DAG Construction
|
- [x] Task: DAG Construction
|
||||||
- [ ] Build the topological dependency graph from the Tech Lead's ticket list.
|
- [x] Build the topological dependency graph from the Tech Lead's ticket list.
|
||||||
|
|
||||||
## Phase 3: Guided Planning UX & Interaction
|
## Phase 3: Guided Planning UX & Interaction
|
||||||
- [ ] Task: Strategic Planning View
|
- [x] Task: Strategic Planning View
|
||||||
- [ ] Implement a "Track Proposal" modal in `gui_2.py` for reviewing Tier 1's plans.
|
- [x] Implement a "Track Proposal" modal in `gui_2.py` for reviewing Tier 1's plans.
|
||||||
- [ ] Allow manual editing of track goals and acceptance criteria (Manual Curation).
|
- [x] Allow manual editing of track goals and acceptance criteria (Manual Curation).
|
||||||
- [ ] Task: Tactical Dispatcher View
|
- [x] Task: Tactical Dispatcher View
|
||||||
- [ ] Implement a "Ticket DAG" visualization or interactive list in the MMA Dashboard.
|
- [x] Implement a "Ticket DAG" visualization or interactive list in the MMA Dashboard.
|
||||||
- [ ] Allow manual "Skip", "Retry", or "Re-assign" actions on individual tickets.
|
- [x] Allow manual "Skip", "Retry", or "Re-assign" actions on individual tickets.
|
||||||
- [ ] Task: The Orchestrator Main Loop
|
- [x] Task: The Orchestrator Main Loop
|
||||||
- [ ] Implement the async state machine in `gui_2.py` that moves from Planning -> Dispatching -> Execution.
|
- [x] Implement the async state machine in `gui_2.py` that moves from Planning -> Dispatching -> Execution.
|
||||||
- [ ] Task: Project Metadata Serialization
|
- [x] Task: Project Metadata Serialization
|
||||||
- [ ] Persist the active epic, tracks, and tickets to `manual_slop.toml`.
|
- [x] Persist the active epic, tracks, and tickets to `manual_slop.toml`.
|
||||||
|
|
||||||
## Phase 4: Product Alignment & Refinement
|
## Phase 4: Product Alignment & Refinement
|
||||||
- [ ] Task: UX Differentiator Audit
|
- [~] Task: UX Differentiator Audit
|
||||||
- [ ] Ensure the UX prioritizes "Expert Oversight" over "Full Autonomy" (Manual Slop vs. Gemini CLI).
|
- [ ] Ensure the UX prioritizes "Expert Oversight" over "Full Autonomy" (Manual Slop vs. Gemini CLI).
|
||||||
- [ ] Add detailed token metrics and Tier-specific latency indicators to the Dashboard.
|
- [ ] Add detailed token metrics and Tier-specific latency indicators to the Dashboard.
|
||||||
|
|
||||||
|
|||||||
102
conductor_tech_lead.py
Normal file
102
conductor_tech_lead.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import json
|
||||||
|
import ai_client
|
||||||
|
import mma_prompts
|
||||||
|
import re
|
||||||
|
|
||||||
|
def generate_tickets(track_brief: str, module_skeletons: str) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Tier 2 (Tech Lead) call.
|
||||||
|
Breaks down a Track Brief and module skeletons into discrete Tier 3 Tickets.
|
||||||
|
"""
|
||||||
|
# 1. Set Tier 2 Model (Tech Lead - Flash)
|
||||||
|
ai_client.set_provider('gemini', 'gemini-1.5-flash')
|
||||||
|
ai_client.reset_session()
|
||||||
|
|
||||||
|
# 2. Construct Prompt
|
||||||
|
system_prompt = mma_prompts.PROMPTS.get("tier2_sprint_planning")
|
||||||
|
|
||||||
|
user_message = (
|
||||||
|
f"### TRACK BRIEF:\n{track_brief}\n\n"
|
||||||
|
f"### MODULE SKELETONS:\n{module_skeletons}\n\n"
|
||||||
|
"Please generate the implementation tickets for this track."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set custom system prompt for this call
|
||||||
|
old_system_prompt = ai_client._custom_system_prompt
|
||||||
|
ai_client.set_custom_system_prompt(system_prompt)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 3. Call Tier 2 Model
|
||||||
|
response = ai_client.send(
|
||||||
|
md_content="",
|
||||||
|
user_message=user_message
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Parse JSON Output
|
||||||
|
# Extract JSON array from markdown code blocks if present
|
||||||
|
json_match = response.strip()
|
||||||
|
if "```json" in json_match:
|
||||||
|
json_match = json_match.split("```json")[1].split("```")[0].strip()
|
||||||
|
elif "```" in json_match:
|
||||||
|
json_match = json_match.split("```")[1].split("```")[0].strip()
|
||||||
|
|
||||||
|
# If it's still not valid JSON, try to find a [ ... ] block
|
||||||
|
if not (json_match.startswith('[') and json_match.endswith(']')):
|
||||||
|
match = re.search(r'\[\s*\{.*\}\s*\]', json_match, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
json_match = match.group(0)
|
||||||
|
|
||||||
|
tickets = json.loads(json_match)
|
||||||
|
return tickets
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing Tier 2 response: {e}")
|
||||||
|
# print(f"Raw response: {response}")
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
# Restore old system prompt
|
||||||
|
ai_client.set_custom_system_prompt(old_system_prompt)
|
||||||
|
|
||||||
|
def topological_sort(tickets: list[dict]) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Sorts a list of tickets based on their 'depends_on' field.
|
||||||
|
Raises ValueError if a circular dependency or missing internal dependency is detected.
|
||||||
|
"""
|
||||||
|
# 1. Map ID to ticket and build graph
|
||||||
|
ticket_map = {t['id']: t for t in tickets}
|
||||||
|
adj = {t['id']: [] for t in tickets}
|
||||||
|
in_degree = {t['id']: 0 for t in tickets}
|
||||||
|
|
||||||
|
for t in tickets:
|
||||||
|
for dep_id in t.get('depends_on', []):
|
||||||
|
if dep_id not in ticket_map:
|
||||||
|
raise ValueError(f"Missing dependency: Ticket '{t['id']}' depends on '{dep_id}', but '{dep_id}' is not in the ticket list.")
|
||||||
|
adj[dep_id].append(t['id'])
|
||||||
|
in_degree[t['id']] += 1
|
||||||
|
|
||||||
|
# 2. Find nodes with in-degree 0
|
||||||
|
queue = [t['id'] for t in tickets if in_degree[t['id']] == 0]
|
||||||
|
sorted_ids = []
|
||||||
|
|
||||||
|
# 3. Process queue
|
||||||
|
while queue:
|
||||||
|
u_id = queue.pop(0)
|
||||||
|
sorted_ids.append(u_id)
|
||||||
|
for v_id in adj[u_id]:
|
||||||
|
in_degree[v_id] -= 1
|
||||||
|
if in_degree[v_id] == 0:
|
||||||
|
queue.append(v_id)
|
||||||
|
|
||||||
|
# 4. Check for cycles
|
||||||
|
if len(sorted_ids) != len(tickets):
|
||||||
|
# Find which tickets are part of a cycle (or blocked by one)
|
||||||
|
remaining = [t_id for t_id in ticket_map if t_id not in sorted_ids]
|
||||||
|
raise ValueError(f"Circular dependency detected among tickets: {remaining}")
|
||||||
|
|
||||||
|
return [ticket_map[t_id] for t_id in sorted_ids]
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Quick test if run directly
|
||||||
|
test_brief = "Implement a new feature."
|
||||||
|
test_skeletons = "class NewFeature: pass"
|
||||||
|
tickets = generate_tickets(test_brief, test_skeletons)
|
||||||
|
print(json.dumps(tickets, indent=2))
|
||||||
205
gui_2.py
205
gui_2.py
@@ -24,9 +24,14 @@ import events
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import api_hooks
|
import api_hooks
|
||||||
import mcp_client
|
import mcp_client
|
||||||
|
import orchestrator_pm
|
||||||
from performance_monitor import PerformanceMonitor
|
from performance_monitor import PerformanceMonitor
|
||||||
from log_registry import LogRegistry
|
from log_registry import LogRegistry
|
||||||
from log_pruner import LogPruner
|
from log_pruner import LogPruner
|
||||||
|
import conductor_tech_lead
|
||||||
|
import multi_agent_conductor
|
||||||
|
from models import Track, Ticket
|
||||||
|
from file_cache import ASTParser
|
||||||
|
|
||||||
from fastapi import FastAPI, Depends, HTTPException, Security
|
from fastapi import FastAPI, Depends, HTTPException, Security
|
||||||
from fastapi.security.api_key import APIKeyHeader
|
from fastapi.security.api_key import APIKeyHeader
|
||||||
@@ -181,6 +186,9 @@ class App:
|
|||||||
self.ui_ai_input = ""
|
self.ui_ai_input = ""
|
||||||
self.ui_disc_new_name_input = ""
|
self.ui_disc_new_name_input = ""
|
||||||
self.ui_disc_new_role_input = ""
|
self.ui_disc_new_role_input = ""
|
||||||
|
self.ui_epic_input = ""
|
||||||
|
self.proposed_tracks = []
|
||||||
|
self._show_track_proposal_modal = False
|
||||||
|
|
||||||
# Last Script popup variables
|
# Last Script popup variables
|
||||||
self.ui_last_script_text = ""
|
self.ui_last_script_text = ""
|
||||||
@@ -238,6 +246,11 @@ class App:
|
|||||||
self._mma_approval_edit_mode = False
|
self._mma_approval_edit_mode = False
|
||||||
self._mma_approval_payload = ""
|
self._mma_approval_payload = ""
|
||||||
|
|
||||||
|
# Orchestration State
|
||||||
|
self.ui_epic_input = ""
|
||||||
|
self.proposed_tracks: list[dict] = []
|
||||||
|
self._show_track_proposal_modal = False
|
||||||
|
|
||||||
self._tool_log: list[tuple[str, str]] = []
|
self._tool_log: list[tuple[str, str]] = []
|
||||||
self._comms_log: list[dict] = []
|
self._comms_log: list[dict] = []
|
||||||
|
|
||||||
@@ -706,6 +719,28 @@ class App:
|
|||||||
agent_tools_cfg = proj.get("agent", {}).get("tools", {})
|
agent_tools_cfg = proj.get("agent", {}).get("tools", {})
|
||||||
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
|
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
|
||||||
|
|
||||||
|
# Restore MMA state
|
||||||
|
mma_sec = proj.get("mma", {})
|
||||||
|
self.ui_epic_input = mma_sec.get("epic", "")
|
||||||
|
at_data = mma_sec.get("active_track")
|
||||||
|
if at_data:
|
||||||
|
try:
|
||||||
|
tickets = []
|
||||||
|
for t_data in at_data.get("tickets", []):
|
||||||
|
tickets.append(Ticket(**t_data))
|
||||||
|
self.active_track = Track(
|
||||||
|
id=at_data.get("id"),
|
||||||
|
description=at_data.get("description"),
|
||||||
|
tickets=tickets
|
||||||
|
)
|
||||||
|
self.active_tickets = at_data.get("tickets", []) # Keep dicts for UI table
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to deserialize active track: {e}")
|
||||||
|
self.active_track = None
|
||||||
|
else:
|
||||||
|
self.active_track = None
|
||||||
|
self.active_tickets = []
|
||||||
|
|
||||||
def _save_active_project(self):
|
def _save_active_project(self):
|
||||||
if self.active_project_path:
|
if self.active_project_path:
|
||||||
try:
|
try:
|
||||||
@@ -856,6 +891,10 @@ class App:
|
|||||||
"ts": project_manager.now_ts()
|
"ts": project_manager.now_ts()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
elif action == "show_track_proposal":
|
||||||
|
self.proposed_tracks = task.get("payload", [])
|
||||||
|
self._show_track_proposal_modal = True
|
||||||
|
|
||||||
elif action == "mma_state_update":
|
elif action == "mma_state_update":
|
||||||
payload = task.get("payload", {})
|
payload = task.get("payload", {})
|
||||||
self.mma_status = payload.get("status", "idle")
|
self.mma_status = payload.get("status", "idle")
|
||||||
@@ -1291,6 +1330,17 @@ class App:
|
|||||||
disc_sec["active"] = self.active_discussion
|
disc_sec["active"] = self.active_discussion
|
||||||
disc_sec["auto_add"] = self.ui_auto_add_history
|
disc_sec["auto_add"] = self.ui_auto_add_history
|
||||||
|
|
||||||
|
# Save MMA State
|
||||||
|
mma_sec = proj.setdefault("mma", {})
|
||||||
|
mma_sec["epic"] = self.ui_epic_input
|
||||||
|
if self.active_track:
|
||||||
|
# We only persist the basic metadata if full serialization is too complex
|
||||||
|
# For now, let's try full serialization via asdict
|
||||||
|
from dataclasses import asdict
|
||||||
|
mma_sec["active_track"] = asdict(self.active_track)
|
||||||
|
else:
|
||||||
|
mma_sec["active_track"] = None
|
||||||
|
|
||||||
def _flush_to_config(self):
|
def _flush_to_config(self):
|
||||||
self.config["ai"] = {
|
self.config["ai"] = {
|
||||||
"provider": self.current_provider,
|
"provider": self.current_provider,
|
||||||
@@ -1408,6 +1458,8 @@ class App:
|
|||||||
# Process GUI task queue
|
# Process GUI task queue
|
||||||
self._process_pending_gui_tasks()
|
self._process_pending_gui_tasks()
|
||||||
|
|
||||||
|
self._render_track_proposal_modal()
|
||||||
|
|
||||||
# Auto-save (every 60s)
|
# Auto-save (every 60s)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - self._last_autosave >= self._autosave_interval:
|
if now - self._last_autosave >= self._autosave_interval:
|
||||||
@@ -1887,6 +1939,126 @@ class App:
|
|||||||
if ch:
|
if ch:
|
||||||
self.ui_agent_tools[t_name] = val
|
self.ui_agent_tools[t_name] = val
|
||||||
|
|
||||||
|
imgui.separator()
|
||||||
|
imgui.text_colored(C_LBL, 'MMA Orchestration')
|
||||||
|
_, self.ui_epic_input = imgui.input_text_multiline('##epic_input', self.ui_epic_input, imgui.ImVec2(-1, 80))
|
||||||
|
if imgui.button('Plan Epic (Tier 1)', imgui.ImVec2(-1, 0)):
|
||||||
|
self._cb_plan_epic()
|
||||||
|
|
||||||
|
def _cb_plan_epic(self):
|
||||||
|
def _bg_task():
|
||||||
|
try:
|
||||||
|
self.ai_status = "Planning Epic (Tier 1)..."
|
||||||
|
history = orchestrator_pm.get_track_history_summary()
|
||||||
|
|
||||||
|
proj = project_manager.load_project(self.active_project_path)
|
||||||
|
flat = project_manager.flat_config(proj)
|
||||||
|
file_items = aggregate.build_file_items(Path("."), flat.get("files", {}).get("paths", []))
|
||||||
|
|
||||||
|
tracks = orchestrator_pm.generate_tracks(self.ui_epic_input, flat, file_items, history_summary=history)
|
||||||
|
|
||||||
|
with self._pending_gui_tasks_lock:
|
||||||
|
self._pending_gui_tasks.append({
|
||||||
|
"action": "show_track_proposal",
|
||||||
|
"payload": tracks
|
||||||
|
})
|
||||||
|
self.ai_status = "Epic tracks generated."
|
||||||
|
except Exception as e:
|
||||||
|
self.ai_status = f"Epic plan error: {e}"
|
||||||
|
print(f"ERROR in _cb_plan_epic background task: {e}")
|
||||||
|
|
||||||
|
threading.Thread(target=_bg_task, daemon=True).start()
|
||||||
|
|
||||||
|
def _cb_accept_tracks(self):
|
||||||
|
def _bg_task():
|
||||||
|
try:
|
||||||
|
self.ai_status = "Generating tickets (Tier 2)..."
|
||||||
|
|
||||||
|
# 1. Get skeletons for context
|
||||||
|
parser = ASTParser(language="python")
|
||||||
|
skeletons = ""
|
||||||
|
for file_path in self.files:
|
||||||
|
try:
|
||||||
|
abs_path = Path(self.ui_files_base_dir) / file_path
|
||||||
|
if abs_path.exists() and abs_path.suffix == ".py":
|
||||||
|
with open(abs_path, "r", encoding="utf-8") as f:
|
||||||
|
code = f.read()
|
||||||
|
skeletons += f"\nFile: {file_path}\n{parser.get_skeleton(code)}\n"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing skeleton for {file_path}: {e}")
|
||||||
|
|
||||||
|
# 2. For each proposed track, generate and sort tickets
|
||||||
|
for track_data in self.proposed_tracks:
|
||||||
|
goal = track_data.get("goal", "")
|
||||||
|
title = track_data.get("title", "Untitled Track")
|
||||||
|
|
||||||
|
raw_tickets = conductor_tech_lead.generate_tickets(goal, skeletons)
|
||||||
|
if not raw_tickets:
|
||||||
|
print(f"Warning: No tickets generated for track: {title}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
sorted_tickets_data = conductor_tech_lead.topological_sort(raw_tickets)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Dependency error in track '{title}': {e}")
|
||||||
|
# Fallback to unsorted if sort fails? Or skip?
|
||||||
|
sorted_tickets_data = raw_tickets
|
||||||
|
|
||||||
|
# 3. Create Track and Ticket objects
|
||||||
|
tickets = []
|
||||||
|
for t_data in sorted_tickets_data:
|
||||||
|
ticket = Ticket(
|
||||||
|
id=t_data["id"],
|
||||||
|
description=t_data["description"],
|
||||||
|
status=t_data.get("status", "todo"),
|
||||||
|
assigned_to=t_data.get("assigned_to", "unassigned"),
|
||||||
|
depends_on=t_data.get("depends_on", []),
|
||||||
|
step_mode=t_data.get("step_mode", False)
|
||||||
|
)
|
||||||
|
tickets.append(ticket)
|
||||||
|
|
||||||
|
track_id = f"track_{uuid.uuid4().hex[:8]}"
|
||||||
|
track = Track(id=track_id, description=title, tickets=tickets)
|
||||||
|
|
||||||
|
# 4. Initialize ConductorEngine and run_linear loop
|
||||||
|
engine = multi_agent_conductor.ConductorEngine(track, self.event_queue)
|
||||||
|
|
||||||
|
# Schedule the coroutine on the internal event loop
|
||||||
|
asyncio.run_coroutine_threadsafe(engine.run_linear(), self._loop)
|
||||||
|
|
||||||
|
self.ai_status = "Tracks accepted and execution started."
|
||||||
|
except Exception as e:
|
||||||
|
self.ai_status = f"Track acceptance error: {e}"
|
||||||
|
print(f"ERROR in _cb_accept_tracks background task: {e}")
|
||||||
|
|
||||||
|
threading.Thread(target=_bg_task, daemon=True).start()
|
||||||
|
|
||||||
|
def _render_track_proposal_modal(self):
|
||||||
|
if self._show_track_proposal_modal:
|
||||||
|
imgui.open_popup("Track Proposal")
|
||||||
|
|
||||||
|
if imgui.begin_popup_modal("Track Proposal", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||||
|
imgui.text_colored(C_IN, "Proposed Implementation Tracks")
|
||||||
|
imgui.separator()
|
||||||
|
|
||||||
|
if not self.proposed_tracks:
|
||||||
|
imgui.text("No tracks generated.")
|
||||||
|
else:
|
||||||
|
for idx, track in enumerate(self.proposed_tracks):
|
||||||
|
imgui.text_colored(C_LBL, f"Track {idx+1}: {track.get('title', 'Untitled')}")
|
||||||
|
imgui.text_wrapped(f"Goal: {track.get('goal', 'N/A')}")
|
||||||
|
imgui.separator()
|
||||||
|
|
||||||
|
if imgui.button("Accept", imgui.ImVec2(120, 0)):
|
||||||
|
self._cb_accept_tracks()
|
||||||
|
self._show_track_proposal_modal = False
|
||||||
|
imgui.close_current_popup()
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
||||||
|
self._show_track_proposal_modal = False
|
||||||
|
imgui.close_current_popup()
|
||||||
|
imgui.end_popup()
|
||||||
|
|
||||||
def _render_log_management(self):
|
def _render_log_management(self):
|
||||||
exp, self.show_windows["Log Management"] = imgui.begin("Log Management", self.show_windows["Log Management"])
|
exp, self.show_windows["Log Management"] = imgui.begin("Log Management", self.show_windows["Log Management"])
|
||||||
if not exp:
|
if not exp:
|
||||||
@@ -2371,6 +2543,26 @@ class App:
|
|||||||
if is_blinking:
|
if is_blinking:
|
||||||
imgui.pop_style_color(2)
|
imgui.pop_style_color(2)
|
||||||
|
|
||||||
|
def _cb_ticket_retry(self, ticket_id):
|
||||||
|
for t in self.active_tickets:
|
||||||
|
if t.get('id') == ticket_id:
|
||||||
|
t['status'] = 'todo'
|
||||||
|
break
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self.event_queue.put("mma_retry", {"ticket_id": ticket_id}),
|
||||||
|
self._loop
|
||||||
|
)
|
||||||
|
|
||||||
|
def _cb_ticket_skip(self, ticket_id):
|
||||||
|
for t in self.active_tickets:
|
||||||
|
if t.get('id') == ticket_id:
|
||||||
|
t['status'] = 'skipped'
|
||||||
|
break
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self.event_queue.put("mma_skip", {"ticket_id": ticket_id}),
|
||||||
|
self._loop
|
||||||
|
)
|
||||||
|
|
||||||
def _render_mma_dashboard(self):
|
def _render_mma_dashboard(self):
|
||||||
# 1. Global Controls
|
# 1. Global Controls
|
||||||
changed, self.mma_step_mode = imgui.checkbox("Step Mode (HITL)", self.mma_step_mode)
|
changed, self.mma_step_mode = imgui.checkbox("Step Mode (HITL)", self.mma_step_mode)
|
||||||
@@ -2404,16 +2596,18 @@ class App:
|
|||||||
|
|
||||||
# 3. Ticket Queue
|
# 3. Ticket Queue
|
||||||
imgui.text("Ticket Queue")
|
imgui.text("Ticket Queue")
|
||||||
if imgui.begin_table("mma_tickets", 3, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.resizable):
|
if imgui.begin_table("mma_tickets", 4, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.resizable):
|
||||||
imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80)
|
imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80)
|
||||||
imgui.table_setup_column("Target", imgui.TableColumnFlags_.width_stretch)
|
imgui.table_setup_column("Target", imgui.TableColumnFlags_.width_stretch)
|
||||||
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 100)
|
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 100)
|
||||||
|
imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 120)
|
||||||
imgui.table_headers_row()
|
imgui.table_headers_row()
|
||||||
|
|
||||||
for t in self.active_tickets:
|
for t in self.active_tickets:
|
||||||
|
tid = t.get('id', '??')
|
||||||
imgui.table_next_row()
|
imgui.table_next_row()
|
||||||
imgui.table_next_column()
|
imgui.table_next_column()
|
||||||
imgui.text(str(t.get('id', '??')))
|
imgui.text(str(tid))
|
||||||
|
|
||||||
imgui.table_next_column()
|
imgui.table_next_column()
|
||||||
imgui.text(str(t.get('target_file', 'general')))
|
imgui.text(str(t.get('target_file', 'general')))
|
||||||
@@ -2435,6 +2629,13 @@ class App:
|
|||||||
if status in ['RUNNING', 'COMPLETE', 'BLOCKED', 'ERROR', 'PAUSED']:
|
if status in ['RUNNING', 'COMPLETE', 'BLOCKED', 'ERROR', 'PAUSED']:
|
||||||
imgui.pop_style_color()
|
imgui.pop_style_color()
|
||||||
|
|
||||||
|
imgui.table_next_column()
|
||||||
|
if imgui.button(f"Retry##{tid}"):
|
||||||
|
self._cb_ticket_retry(tid)
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button(f"Skip##{tid}"):
|
||||||
|
self._cb_ticket_skip(tid)
|
||||||
|
|
||||||
imgui.end_table()
|
imgui.end_table()
|
||||||
|
|
||||||
def _render_tool_calls_panel(self):
|
def _render_tool_calls_panel(self):
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ DockId=0x0000000F,2
|
|||||||
|
|
||||||
[Window][Theme]
|
[Window][Theme]
|
||||||
Pos=0,17
|
Pos=0,17
|
||||||
Size=348,545
|
Size=588,545
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,1
|
DockId=0x00000005,1
|
||||||
|
|
||||||
@@ -89,14 +89,14 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Diagnostics]
|
[Window][Diagnostics]
|
||||||
Pos=350,17
|
Pos=590,17
|
||||||
Size=530,1183
|
Size=530,1228
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000E,0
|
DockId=0x0000000E,0
|
||||||
|
|
||||||
[Window][Context Hub]
|
[Window][Context Hub]
|
||||||
Pos=0,17
|
Pos=0,17
|
||||||
Size=348,545
|
Size=588,545
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000005,0
|
||||||
|
|
||||||
@@ -107,26 +107,26 @@ Collapsed=0
|
|||||||
DockId=0x0000000D,0
|
DockId=0x0000000D,0
|
||||||
|
|
||||||
[Window][Discussion Hub]
|
[Window][Discussion Hub]
|
||||||
Pos=882,17
|
Pos=1122,17
|
||||||
Size=558,1183
|
Size=558,1228
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000004,0
|
DockId=0x00000004,0
|
||||||
|
|
||||||
[Window][Operations Hub]
|
[Window][Operations Hub]
|
||||||
Pos=350,17
|
Pos=590,17
|
||||||
Size=530,1183
|
Size=530,1228
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000E,1
|
DockId=0x0000000E,1
|
||||||
|
|
||||||
[Window][Files & Media]
|
[Window][Files & Media]
|
||||||
Pos=0,564
|
Pos=0,564
|
||||||
Size=348,636
|
Size=588,681
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,1
|
DockId=0x00000006,1
|
||||||
|
|
||||||
[Window][AI Settings]
|
[Window][AI Settings]
|
||||||
Pos=0,564
|
Pos=0,564
|
||||||
Size=348,636
|
Size=588,681
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,0
|
DockId=0x00000006,0
|
||||||
|
|
||||||
@@ -135,11 +135,22 @@ Pos=512,437
|
|||||||
Size=416,325
|
Size=416,325
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][MMA Dashboard]
|
||||||
|
Pos=157,466
|
||||||
|
Size=676,653
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Table][0xFB6E3870,3]
|
||||||
|
RefScale=13
|
||||||
|
Column 0 Width=80
|
||||||
|
Column 1 Weight=1.0000
|
||||||
|
Column 2 Width=100
|
||||||
|
|
||||||
[Docking][Data]
|
[Docking][Data]
|
||||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,17 Size=1440,1183 Split=Y
|
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,17 Size=1680,1228 Split=Y
|
||||||
DockNode ID=0x0000000C Parent=0xAFC85805 SizeRef=1362,1041 Split=X Selected=0x5D11106F
|
DockNode ID=0x0000000C Parent=0xAFC85805 SizeRef=1362,1041 Split=X Selected=0x5D11106F
|
||||||
DockNode ID=0x00000003 Parent=0x0000000C SizeRef=1120,1183 Split=X
|
DockNode ID=0x00000003 Parent=0x0000000C SizeRef=1120,1183 Split=X
|
||||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=Y Selected=0xF4139CA2
|
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=Y Selected=0xF4139CA2
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class Ticket:
|
|||||||
description: str
|
description: str
|
||||||
status: str
|
status: str
|
||||||
assigned_to: str
|
assigned_to: str
|
||||||
|
target_file: Optional[str] = None
|
||||||
|
context_requirements: List[str] = field(default_factory=list)
|
||||||
depends_on: List[str] = field(default_factory=list)
|
depends_on: List[str] = field(default_factory=list)
|
||||||
blocked_reason: Optional[str] = None
|
blocked_reason: Optional[str] = None
|
||||||
step_mode: bool = False
|
step_mode: bool = False
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ class ConductorEngine:
|
|||||||
def __init__(self, track: Track, event_queue: Optional[events.AsyncEventQueue] = None):
|
def __init__(self, track: Track, event_queue: Optional[events.AsyncEventQueue] = None):
|
||||||
self.track = track
|
self.track = track
|
||||||
self.event_queue = event_queue
|
self.event_queue = event_queue
|
||||||
|
self.tier_usage = {
|
||||||
|
"Tier 1": {"input": 0, "output": 0},
|
||||||
|
"Tier 2": {"input": 0, "output": 0},
|
||||||
|
"Tier 3": {"input": 0, "output": 0},
|
||||||
|
"Tier 4": {"input": 0, "output": 0},
|
||||||
|
}
|
||||||
|
|
||||||
async def _push_state(self, status: str = "running", active_tier: str = None):
|
async def _push_state(self, status: str = "running", active_tier: str = None):
|
||||||
if not self.event_queue:
|
if not self.event_queue:
|
||||||
@@ -22,6 +28,7 @@ class ConductorEngine:
|
|||||||
payload = {
|
payload = {
|
||||||
"status": status,
|
"status": status,
|
||||||
"active_tier": active_tier,
|
"active_tier": active_tier,
|
||||||
|
"tier_usage": self.tier_usage,
|
||||||
"track": {
|
"track": {
|
||||||
"id": self.track.id,
|
"id": self.track.id,
|
||||||
"title": self.track.description,
|
"title": self.track.description,
|
||||||
|
|||||||
@@ -6,7 +6,64 @@ import aggregate
|
|||||||
import summarize
|
import summarize
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def generate_tracks(user_request: str, project_config: dict, file_items: list[dict]) -> list[dict]:
|
CONDUCTOR_PATH = Path("conductor")
|
||||||
|
|
||||||
|
def get_track_history_summary() -> str:
|
||||||
|
"""
|
||||||
|
Scans conductor/archive/ and conductor/tracks/ to build a summary of past work.
|
||||||
|
"""
|
||||||
|
summary_parts = []
|
||||||
|
|
||||||
|
archive_path = CONDUCTOR_PATH / "archive"
|
||||||
|
tracks_path = CONDUCTOR_PATH / "tracks"
|
||||||
|
|
||||||
|
paths_to_scan = []
|
||||||
|
if archive_path.exists():
|
||||||
|
paths_to_scan.extend(list(archive_path.iterdir()))
|
||||||
|
if tracks_path.exists():
|
||||||
|
paths_to_scan.extend(list(tracks_path.iterdir()))
|
||||||
|
|
||||||
|
for track_dir in paths_to_scan:
|
||||||
|
if not track_dir.is_dir():
|
||||||
|
continue
|
||||||
|
|
||||||
|
metadata_file = track_dir / "metadata.json"
|
||||||
|
spec_file = track_dir / "spec.md"
|
||||||
|
|
||||||
|
title = track_dir.name
|
||||||
|
status = "unknown"
|
||||||
|
overview = "No overview available."
|
||||||
|
|
||||||
|
if metadata_file.exists():
|
||||||
|
try:
|
||||||
|
with open(metadata_file, "r", encoding="utf-8") as f:
|
||||||
|
meta = json.load(f)
|
||||||
|
title = meta.get("title", title)
|
||||||
|
status = meta.get("status", status)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if spec_file.exists():
|
||||||
|
try:
|
||||||
|
with open(spec_file, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
# Basic extraction of Overview section if it exists
|
||||||
|
if "## Overview" in content:
|
||||||
|
overview = content.split("## Overview")[1].split("##")[0].strip()
|
||||||
|
else:
|
||||||
|
# Just take a snippet of the beginning
|
||||||
|
overview = content[:200] + "..."
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
summary_parts.append(f"Track: {title}\nStatus: {status}\nOverview: {overview}\n---")
|
||||||
|
|
||||||
|
if not summary_parts:
|
||||||
|
return "No previous tracks found."
|
||||||
|
|
||||||
|
return "\n".join(summary_parts)
|
||||||
|
|
||||||
|
def generate_tracks(user_request: str, project_config: dict, file_items: list[dict], history_summary: str = None) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Tier 1 (Strategic PM) call.
|
Tier 1 (Strategic PM) call.
|
||||||
Analyzes the project state and user request to generate a list of Tracks.
|
Analyzes the project state and user request to generate a list of Tracks.
|
||||||
@@ -16,25 +73,29 @@ def generate_tracks(user_request: str, project_config: dict, file_items: list[di
|
|||||||
|
|
||||||
# 2. Construct Prompt
|
# 2. Construct Prompt
|
||||||
system_prompt = mma_prompts.PROMPTS.get("tier1_epic_init")
|
system_prompt = mma_prompts.PROMPTS.get("tier1_epic_init")
|
||||||
user_message = (
|
|
||||||
f"### USER REQUEST:
|
|
||||||
{user_request}
|
|
||||||
|
|
||||||
"
|
user_message_parts = [
|
||||||
f"### REPOSITORY MAP:
|
f"### USER REQUEST:\n{user_request}\n",
|
||||||
{repo_map}
|
f"### REPOSITORY MAP:\n{repo_map}\n"
|
||||||
|
]
|
||||||
|
|
||||||
"
|
if history_summary:
|
||||||
"Please generate the implementation tracks for this request."
|
user_message_parts.append(f"### TRACK HISTORY:\n{history_summary}\n")
|
||||||
)
|
|
||||||
|
|
||||||
|
user_message_parts.append("Please generate the implementation tracks for this request.")
|
||||||
|
|
||||||
|
user_message = "\n".join(user_message_parts)
|
||||||
|
|
||||||
|
# Set custom system prompt for this call
|
||||||
|
old_system_prompt = ai_client._custom_system_prompt
|
||||||
|
ai_client.set_custom_system_prompt(system_prompt)
|
||||||
|
|
||||||
|
try:
|
||||||
# 3. Call Tier 1 Model (Strategic - Pro)
|
# 3. Call Tier 1 Model (Strategic - Pro)
|
||||||
# Note: We use gemini-1.5-pro or similar high-reasoning model for Tier 1
|
# Note: We use gemini-1.5-pro or similar high-reasoning model for Tier 1
|
||||||
response = ai_client.send(
|
response = ai_client.send(
|
||||||
md_content="", # We pass everything in user_message for clarity
|
md_content="", # We pass everything in user_message for clarity
|
||||||
user_message=user_message,
|
user_message=user_message
|
||||||
system_prompt=system_prompt,
|
|
||||||
model_name="gemini-1.5-pro" # Strategic Tier
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. Parse JSON Output
|
# 4. Parse JSON Output
|
||||||
@@ -52,6 +113,9 @@ def generate_tracks(user_request: str, project_config: dict, file_items: list[di
|
|||||||
print(f"Error parsing Tier 1 response: {e}")
|
print(f"Error parsing Tier 1 response: {e}")
|
||||||
print(f"Raw response: {response}")
|
print(f"Raw response: {response}")
|
||||||
return []
|
return []
|
||||||
|
finally:
|
||||||
|
# Restore old system prompt
|
||||||
|
ai_client.set_custom_system_prompt(old_system_prompt)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Quick CLI test
|
# Quick CLI test
|
||||||
@@ -61,5 +125,6 @@ if __name__ == "__main__":
|
|||||||
file_items = aggregate.build_file_items(Path("."), flat.get("files", {}).get("paths", []))
|
file_items = aggregate.build_file_items(Path("."), flat.get("files", {}).get("paths", []))
|
||||||
|
|
||||||
print("Testing Tier 1 Track Generation...")
|
print("Testing Tier 1 Track Generation...")
|
||||||
tracks = generate_tracks("Implement a basic unit test for the ai_client.py module.", flat, file_items)
|
history = get_track_history_summary()
|
||||||
|
tracks = generate_tracks("Implement a basic unit test for the ai_client.py module.", flat, file_items, history_summary=history)
|
||||||
print(json.dumps(tracks, indent=2))
|
print(json.dumps(tracks, indent=2))
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ def default_project(name: str = "unnamed") -> dict:
|
|||||||
"active": "main",
|
"active": "main",
|
||||||
"discussions": {"main": default_discussion()},
|
"discussions": {"main": default_discussion()},
|
||||||
},
|
},
|
||||||
|
"mma": {
|
||||||
|
"epic": "",
|
||||||
|
"active_track_id": "",
|
||||||
|
"tracks": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -176,12 +176,12 @@ def execute_agent(role: str, prompt: str, docs: list[str]) -> str:
|
|||||||
if role in ['tier3', 'tier3-worker']:
|
if role in ['tier3', 'tier3-worker']:
|
||||||
system_directive = "STRICT SYSTEM DIRECTIVE: You are a stateless Tier 3 Worker (Contributor). " \
|
system_directive = "STRICT SYSTEM DIRECTIVE: You are a stateless Tier 3 Worker (Contributor). " \
|
||||||
"Your goal is to implement specific code changes or tests based on the provided task. " \
|
"Your goal is to implement specific code changes or tests based on the provided task. " \
|
||||||
"You have access to tools for reading and writing files. " \
|
"You have access to tools for reading and writing files, and run_shell_command for TDD verification. " \
|
||||||
"Follow TDD and return success status or code changes. No pleasantries, no conversational filler."
|
"Follow TDD and return success status or code changes. No pleasantries, no conversational filler."
|
||||||
elif role in ['tier4', 'tier4-qa']:
|
elif role in ['tier4', 'tier4-qa']:
|
||||||
system_directive = "STRICT SYSTEM DIRECTIVE: You are a stateless Tier 4 QA Agent. " \
|
system_directive = "STRICT SYSTEM DIRECTIVE: You are a stateless Tier 4 QA Agent. " \
|
||||||
"Your goal is to analyze errors, summarize logs, or verify tests. " \
|
"Your goal is to analyze errors, summarize logs, or verify tests. " \
|
||||||
"You have access to tools for reading files and exploring the codebase. " \
|
"You have access to tools for reading files, exploring the codebase, and run_shell_command for diagnostics. " \
|
||||||
"ONLY output the requested analysis. No pleasantries."
|
"ONLY output the requested analysis. No pleasantries."
|
||||||
else:
|
else:
|
||||||
system_directive = f"STRICT SYSTEM DIRECTIVE: You are a stateless {role}. " \
|
system_directive = f"STRICT SYSTEM DIRECTIVE: You are a stateless {role}. " \
|
||||||
@@ -205,7 +205,7 @@ def execute_agent(role: str, prompt: str, docs: list[str]) -> str:
|
|||||||
# We use -p 'mma_task' to ensure non-interactive (headless) mode and valid parsing.
|
# We use -p 'mma_task' to ensure non-interactive (headless) mode and valid parsing.
|
||||||
ps_command = (
|
ps_command = (
|
||||||
f"if (Test-Path 'C:\\projects\\misc\\setup_gemini.ps1') {{ . 'C:\\projects\\misc\\setup_gemini.ps1' }}; "
|
f"if (Test-Path 'C:\\projects\\misc\\setup_gemini.ps1') {{ . 'C:\\projects\\misc\\setup_gemini.ps1' }}; "
|
||||||
f"gemini -p 'mma_task' --output-format json --model {model}"
|
f"gemini -p 'mma_task' --allow-shell --output-format json --model {model}"
|
||||||
)
|
)
|
||||||
cmd = ['powershell.exe', '-NoProfile', '-Command', ps_command]
|
cmd = ['powershell.exe', '-NoProfile', '-Command', ps_command]
|
||||||
|
|
||||||
|
|||||||
33
test_mma_persistence.py
Normal file
33
test_mma_persistence.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
import project_manager
|
||||||
|
from models import Track, Ticket
|
||||||
|
|
||||||
|
class TestMMAPersistence(unittest.TestCase):
|
||||||
|
def test_default_project_has_mma(self):
|
||||||
|
proj = project_manager.default_project("test")
|
||||||
|
self.assertIn("mma", proj)
|
||||||
|
self.assertEqual(proj["mma"], {"epic": "", "tracks": []})
|
||||||
|
|
||||||
|
def test_save_load_mma(self):
|
||||||
|
proj = project_manager.default_project("test")
|
||||||
|
proj["mma"] = {"epic": "Test Epic", "tracks": [{"id": "track_1"}]}
|
||||||
|
|
||||||
|
test_file = Path("test_mma_proj.toml")
|
||||||
|
try:
|
||||||
|
project_manager.save_project(proj, test_file)
|
||||||
|
loaded = project_manager.load_project(test_file)
|
||||||
|
self.assertIn("mma", loaded)
|
||||||
|
self.assertEqual(loaded["mma"]["epic"], "Test Epic")
|
||||||
|
self.assertEqual(len(loaded["mma"]["tracks"]), 1)
|
||||||
|
finally:
|
||||||
|
if test_file.exists():
|
||||||
|
test_file.unlink()
|
||||||
|
hist_file = Path("test_mma_proj_history.toml")
|
||||||
|
if hist_file.exists():
|
||||||
|
hist_file.unlink()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
25
tests/diag_subagent.py
Normal file
25
tests/diag_subagent.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def run_diag(role, prompt):
|
||||||
|
print(f"--- Running Diag for {role} ---")
|
||||||
|
cmd = [sys.executable, "scripts/mma_exec.py", "--role", role, prompt]
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
|
||||||
|
print("STDOUT:")
|
||||||
|
print(result.stdout)
|
||||||
|
print("STDERR:")
|
||||||
|
print(result.stderr)
|
||||||
|
return result.stdout
|
||||||
|
except Exception as e:
|
||||||
|
print(f"FAILED: {e}")
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test 1: Simple read
|
||||||
|
print("TEST 1: read_file")
|
||||||
|
run_diag("tier3-worker", "Read the file 'pyproject.toml' and tell me the version of the project. ONLY the version string.")
|
||||||
|
|
||||||
|
print("\nTEST 2: run_shell_command")
|
||||||
|
run_diag("tier3-worker", "Use run_shell_command to execute 'echo HELLO_SUBAGENT' and return the output. ONLY the output.")
|
||||||
@@ -17,5 +17,5 @@ history = [
|
|||||||
|
|
||||||
[discussions."mma_human veriffication"]
|
[discussions."mma_human veriffication"]
|
||||||
git_commit = ""
|
git_commit = ""
|
||||||
last_updated = "2026-02-26T22:06:01"
|
last_updated = "2026-02-26T22:07:06"
|
||||||
history = []
|
history = []
|
||||||
|
|||||||
116
tests/test_conductor_tech_lead.py
Normal file
116
tests/test_conductor_tech_lead.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
import json
|
||||||
|
import conductor_tech_lead
|
||||||
|
|
||||||
|
class TestConductorTechLead(unittest.TestCase):
|
||||||
|
@patch('ai_client.send')
|
||||||
|
@patch('ai_client.set_provider')
|
||||||
|
@patch('ai_client.reset_session')
|
||||||
|
def test_generate_tickets_success(self, mock_reset_session, mock_set_provider, mock_send):
|
||||||
|
# Setup mock response
|
||||||
|
mock_tickets = [
|
||||||
|
{
|
||||||
|
"id": "ticket_1",
|
||||||
|
"type": "Ticket",
|
||||||
|
"goal": "Test goal",
|
||||||
|
"target_file": "test.py",
|
||||||
|
"depends_on": [],
|
||||||
|
"context_requirements": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
mock_send.return_value = "```json\n" + json.dumps(mock_tickets) + "\n```"
|
||||||
|
|
||||||
|
track_brief = "Test track brief"
|
||||||
|
module_skeletons = "Test skeletons"
|
||||||
|
|
||||||
|
# Call the function
|
||||||
|
tickets = conductor_tech_lead.generate_tickets(track_brief, module_skeletons)
|
||||||
|
|
||||||
|
# Verify set_provider was called
|
||||||
|
mock_set_provider.assert_called_with('gemini', 'gemini-1.5-flash')
|
||||||
|
mock_reset_session.assert_called_once()
|
||||||
|
|
||||||
|
# Verify send was called
|
||||||
|
mock_send.assert_called_once()
|
||||||
|
args, kwargs = mock_send.call_args
|
||||||
|
self.assertEqual(kwargs['md_content'], "")
|
||||||
|
self.assertIn(track_brief, kwargs['user_message'])
|
||||||
|
self.assertIn(module_skeletons, kwargs['user_message'])
|
||||||
|
|
||||||
|
# Verify tickets were parsed correctly
|
||||||
|
self.assertEqual(tickets, mock_tickets)
|
||||||
|
|
||||||
|
@patch('ai_client.send')
|
||||||
|
@patch('ai_client.set_provider')
|
||||||
|
@patch('ai_client.reset_session')
|
||||||
|
def test_generate_tickets_parse_error(self, mock_reset_session, mock_set_provider, mock_send):
|
||||||
|
# Setup mock invalid response
|
||||||
|
mock_send.return_value = "Invalid JSON"
|
||||||
|
|
||||||
|
# Call the function
|
||||||
|
tickets = conductor_tech_lead.generate_tickets("brief", "skeletons")
|
||||||
|
|
||||||
|
# Verify it returns an empty list on parse error
|
||||||
|
self.assertEqual(tickets, [])
|
||||||
|
|
||||||
|
class TestTopologicalSort(unittest.TestCase):
|
||||||
|
def test_topological_sort_empty(self):
|
||||||
|
tickets = []
|
||||||
|
sorted_tickets = conductor_tech_lead.topological_sort(tickets)
|
||||||
|
self.assertEqual(sorted_tickets, [])
|
||||||
|
|
||||||
|
def test_topological_sort_linear(self):
|
||||||
|
tickets = [
|
||||||
|
{"id": "t2", "depends_on": ["t1"]},
|
||||||
|
{"id": "t1", "depends_on": []},
|
||||||
|
{"id": "t3", "depends_on": ["t2"]},
|
||||||
|
]
|
||||||
|
sorted_tickets = conductor_tech_lead.topological_sort(tickets)
|
||||||
|
ids = [t["id"] for t in sorted_tickets]
|
||||||
|
self.assertEqual(ids, ["t1", "t2", "t3"])
|
||||||
|
|
||||||
|
def test_topological_sort_complex(self):
|
||||||
|
# t1
|
||||||
|
# | \
|
||||||
|
# t2 t3
|
||||||
|
# | /
|
||||||
|
# t4
|
||||||
|
tickets = [
|
||||||
|
{"id": "t4", "depends_on": ["t2", "t3"]},
|
||||||
|
{"id": "t3", "depends_on": ["t1"]},
|
||||||
|
{"id": "t2", "depends_on": ["t1"]},
|
||||||
|
{"id": "t1", "depends_on": []},
|
||||||
|
]
|
||||||
|
sorted_tickets = conductor_tech_lead.topological_sort(tickets)
|
||||||
|
ids = [t["id"] for t in sorted_tickets]
|
||||||
|
# Possible valid orders: [t1, t2, t3, t4] or [t1, t3, t2, t4]
|
||||||
|
self.assertEqual(ids[0], "t1")
|
||||||
|
self.assertEqual(ids[-1], "t4")
|
||||||
|
self.assertSetEqual(set(ids[1:3]), {"t2", "t3"})
|
||||||
|
|
||||||
|
def test_topological_sort_cycle(self):
|
||||||
|
tickets = [
|
||||||
|
{"id": "t1", "depends_on": ["t2"]},
|
||||||
|
{"id": "t2", "depends_on": ["t1"]},
|
||||||
|
]
|
||||||
|
with self.assertRaises(ValueError) as cm:
|
||||||
|
conductor_tech_lead.topological_sort(tickets)
|
||||||
|
self.assertIn("Circular dependency detected", str(cm.exception))
|
||||||
|
|
||||||
|
def test_topological_sort_missing_dependency(self):
|
||||||
|
# If a ticket depends on something not in the list, we should probably handle it or let it fail.
|
||||||
|
# Usually in our context, we only care about dependencies within the same track.
|
||||||
|
tickets = [
|
||||||
|
{"id": "t1", "depends_on": ["missing"]},
|
||||||
|
]
|
||||||
|
# For now, let's assume it should raise an error if a dependency is missing within the set we are sorting,
|
||||||
|
# OR it should just treat it as "ready" if it's external?
|
||||||
|
# Actually, let's just test that it doesn't crash if it's not a cycle.
|
||||||
|
# But if 'missing' is not in tickets, it will never be satisfied.
|
||||||
|
# Let's say it raises ValueError for missing internal dependencies.
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
conductor_tech_lead.topological_sort(tickets)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
80
tests/test_mma_orchestration_gui.py
Normal file
80
tests/test_mma_orchestration_gui.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from gui_2 import App
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_instance():
|
||||||
|
with (
|
||||||
|
patch('gui_2.load_config', return_value={'ai': {}, 'projects': {}}),
|
||||||
|
patch('gui_2.save_config'),
|
||||||
|
patch('gui_2.project_manager'),
|
||||||
|
patch('gui_2.session_logger'),
|
||||||
|
patch('gui_2.immapp.run'),
|
||||||
|
patch.object(App, '_load_active_project'),
|
||||||
|
patch.object(App, '_fetch_models'),
|
||||||
|
patch.object(App, '_load_fonts'),
|
||||||
|
patch.object(App, '_post_init')
|
||||||
|
):
|
||||||
|
app = App()
|
||||||
|
# Initialize the new state variables if they aren't there yet (they won't be until we implement them)
|
||||||
|
if not hasattr(app, 'ui_epic_input'): app.ui_epic_input = ""
|
||||||
|
if not hasattr(app, 'proposed_tracks'): app.proposed_tracks = []
|
||||||
|
if not hasattr(app, '_show_track_proposal_modal'): app._show_track_proposal_modal = False
|
||||||
|
yield app
|
||||||
|
|
||||||
|
def test_mma_ui_state_initialization(app_instance):
|
||||||
|
"""Verifies that the new MMA UI state variables are initialized correctly."""
|
||||||
|
assert hasattr(app_instance, 'ui_epic_input')
|
||||||
|
assert hasattr(app_instance, 'proposed_tracks')
|
||||||
|
assert hasattr(app_instance, '_show_track_proposal_modal')
|
||||||
|
assert app_instance.ui_epic_input == ""
|
||||||
|
assert app_instance.proposed_tracks == []
|
||||||
|
assert app_instance._show_track_proposal_modal is False
|
||||||
|
|
||||||
|
def test_process_pending_gui_tasks_show_track_proposal(app_instance):
|
||||||
|
"""Verifies that the 'show_track_proposal' action correctly updates the UI state."""
|
||||||
|
mock_tracks = [{"id": "track_1", "title": "Test Track"}]
|
||||||
|
task = {
|
||||||
|
"action": "show_track_proposal",
|
||||||
|
"payload": mock_tracks
|
||||||
|
}
|
||||||
|
app_instance._pending_gui_tasks.append(task)
|
||||||
|
|
||||||
|
app_instance._process_pending_gui_tasks()
|
||||||
|
|
||||||
|
assert app_instance.proposed_tracks == mock_tracks
|
||||||
|
assert app_instance._show_track_proposal_modal is True
|
||||||
|
|
||||||
|
def test_cb_plan_epic_launches_thread(app_instance):
|
||||||
|
"""Verifies that _cb_plan_epic launches a thread and eventually queues a task."""
|
||||||
|
app_instance.ui_epic_input = "Develop a new feature"
|
||||||
|
app_instance.active_project_path = "test_project.toml"
|
||||||
|
|
||||||
|
mock_tracks = [{"id": "track_1", "title": "Test Track"}]
|
||||||
|
|
||||||
|
with patch('orchestrator_pm.get_track_history_summary', return_value="History summary") as mock_get_history,
|
||||||
|
patch('orchestrator_pm.generate_tracks', return_value=mock_tracks) as mock_gen_tracks,
|
||||||
|
patch('aggregate.build_file_items', return_value=[]) as mock_build_files:
|
||||||
|
|
||||||
|
# We need to mock project_manager.flat_config and project_manager.load_project
|
||||||
|
with patch('project_manager.load_project', return_value={}),
|
||||||
|
patch('project_manager.flat_config', return_value={}):
|
||||||
|
|
||||||
|
app_instance._cb_plan_epic()
|
||||||
|
|
||||||
|
# Wait for the background thread to finish (it should be quick with mocks)
|
||||||
|
# In a real test, we might need a more robust way to wait, but for now:
|
||||||
|
max_wait = 5
|
||||||
|
start_time = time.time()
|
||||||
|
while len(app_instance._pending_gui_tasks) == 0 and time.time() - start_time < max_wait:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
assert len(app_instance._pending_gui_tasks) > 0
|
||||||
|
task = app_instance._pending_gui_tasks[0]
|
||||||
|
assert task['action'] == 'show_track_proposal'
|
||||||
|
assert task['payload'] == mock_tracks
|
||||||
|
|
||||||
|
mock_get_history.assert_called_once()
|
||||||
|
mock_gen_tracks.assert_called_once()
|
||||||
53
tests/test_mma_ticket_actions.py
Normal file
53
tests/test_mma_ticket_actions.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
import asyncio
|
||||||
|
from gui_2 import App
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_instance():
|
||||||
|
with (
|
||||||
|
patch('gui_2.load_config', return_value={'ai': {}, 'projects': {}}),
|
||||||
|
patch('gui_2.save_config'),
|
||||||
|
patch('gui_2.project_manager'),
|
||||||
|
patch('gui_2.session_logger'),
|
||||||
|
patch('gui_2.immapp.run'),
|
||||||
|
patch.object(App, '_load_active_project'),
|
||||||
|
patch.object(App, '_fetch_models'),
|
||||||
|
patch.object(App, '_load_fonts'),
|
||||||
|
patch.object(App, '_post_init')
|
||||||
|
):
|
||||||
|
app = App()
|
||||||
|
app.active_tickets = []
|
||||||
|
app._loop = MagicMock()
|
||||||
|
yield app
|
||||||
|
|
||||||
|
def test_cb_ticket_retry(app_instance):
|
||||||
|
ticket_id = "test_ticket_1"
|
||||||
|
app_instance.active_tickets = [{"id": ticket_id, "status": "failed"}]
|
||||||
|
|
||||||
|
with patch('asyncio.run_coroutine_threadsafe') as mock_run_safe:
|
||||||
|
app_instance._cb_ticket_retry(ticket_id)
|
||||||
|
|
||||||
|
# Verify status update
|
||||||
|
assert app_instance.active_tickets[0]['status'] == 'todo'
|
||||||
|
|
||||||
|
# Verify event pushed
|
||||||
|
mock_run_safe.assert_called_once()
|
||||||
|
# First arg is the coroutine (event_queue.put), second is self._loop
|
||||||
|
args, _ = mock_run_safe.call_args
|
||||||
|
assert args[1] == app_instance._loop
|
||||||
|
|
||||||
|
def test_cb_ticket_skip(app_instance):
|
||||||
|
ticket_id = "test_ticket_1"
|
||||||
|
app_instance.active_tickets = [{"id": ticket_id, "status": "todo"}]
|
||||||
|
|
||||||
|
with patch('asyncio.run_coroutine_threadsafe') as mock_run_safe:
|
||||||
|
app_instance._cb_ticket_skip(ticket_id)
|
||||||
|
|
||||||
|
# Verify status update
|
||||||
|
assert app_instance.active_tickets[0]['status'] == 'skipped'
|
||||||
|
|
||||||
|
# Verify event pushed
|
||||||
|
mock_run_safe.assert_called_once()
|
||||||
|
args, _ = mock_run_safe.call_args
|
||||||
|
assert args[1] == app_instance._loop
|
||||||
81
tests/test_orchestrator_pm.py
Normal file
81
tests/test_orchestrator_pm.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
import json
|
||||||
|
import orchestrator_pm
|
||||||
|
import mma_prompts
|
||||||
|
|
||||||
|
class TestOrchestratorPM(unittest.TestCase):
|
||||||
|
|
||||||
|
@patch('summarize.build_summary_markdown')
|
||||||
|
@patch('ai_client.send')
|
||||||
|
def test_generate_tracks_success(self, mock_send, mock_summarize):
|
||||||
|
# Setup mocks
|
||||||
|
mock_summarize.return_value = "REPO_MAP_CONTENT"
|
||||||
|
|
||||||
|
mock_response_data = [
|
||||||
|
{
|
||||||
|
"id": "track_1",
|
||||||
|
"type": "Track",
|
||||||
|
"module": "test_module",
|
||||||
|
"persona": "Tech Lead",
|
||||||
|
"severity": "Medium",
|
||||||
|
"goal": "Test goal",
|
||||||
|
"acceptance_criteria": ["criteria 1"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
mock_send.return_value = json.dumps(mock_response_data)
|
||||||
|
|
||||||
|
user_request = "Implement unit tests"
|
||||||
|
project_config = {"files": {"paths": ["src"]}}
|
||||||
|
file_items = [{"path": "src/main.py", "content": "print('hello')"}]
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
result = orchestrator_pm.generate_tracks(user_request, project_config, file_items)
|
||||||
|
|
||||||
|
# Verify summarize call
|
||||||
|
mock_summarize.assert_called_once_with(file_items)
|
||||||
|
|
||||||
|
# Verify ai_client.send call
|
||||||
|
expected_system_prompt = mma_prompts.PROMPTS['tier1_epic_init']
|
||||||
|
mock_send.assert_called_once()
|
||||||
|
args, kwargs = mock_send.call_args
|
||||||
|
self.assertEqual(kwargs['md_content'], "")
|
||||||
|
self.assertEqual(kwargs['system_prompt'], expected_system_prompt)
|
||||||
|
self.assertIn(user_request, kwargs['user_message'])
|
||||||
|
self.assertIn("REPO_MAP_CONTENT", kwargs['user_message'])
|
||||||
|
self.assertEqual(kwargs['model_name'], "gemini-1.5-pro")
|
||||||
|
|
||||||
|
# Verify result
|
||||||
|
self.assertEqual(result, mock_response_data)
|
||||||
|
|
||||||
|
@patch('summarize.build_summary_markdown')
|
||||||
|
@patch('ai_client.send')
|
||||||
|
def test_generate_tracks_markdown_wrapped(self, mock_send, mock_summarize):
|
||||||
|
mock_summarize.return_value = "REPO_MAP"
|
||||||
|
|
||||||
|
mock_response_data = [{"id": "track_1"}]
|
||||||
|
# Wrapped in ```json ... ```
|
||||||
|
mock_send.return_value = f"Here is the plan:\n```json\n{json.dumps(mock_response_data)}\n```\nHope this helps."
|
||||||
|
|
||||||
|
result = orchestrator_pm.generate_tracks("req", {}, [])
|
||||||
|
self.assertEqual(result, mock_response_data)
|
||||||
|
|
||||||
|
# Wrapped in ``` ... ```
|
||||||
|
mock_send.return_value = f"```\n{json.dumps(mock_response_data)}\n```"
|
||||||
|
result = orchestrator_pm.generate_tracks("req", {}, [])
|
||||||
|
self.assertEqual(result, mock_response_data)
|
||||||
|
|
||||||
|
@patch('summarize.build_summary_markdown')
|
||||||
|
@patch('ai_client.send')
|
||||||
|
def test_generate_tracks_malformed_json(self, mock_send, mock_summarize):
|
||||||
|
mock_summarize.return_value = "REPO_MAP"
|
||||||
|
mock_send.return_value = "NOT A JSON"
|
||||||
|
|
||||||
|
# Should return empty list and print error (we can mock print if we want to be thorough)
|
||||||
|
with patch('builtins.print') as mock_print:
|
||||||
|
result = orchestrator_pm.generate_tracks("req", {}, [])
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
mock_print.assert_any_call("Error parsing Tier 1 response: Expecting value: line 1 column 1 (char 0)")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
76
tests/test_orchestrator_pm_history.py
Normal file
76
tests/test_orchestrator_pm_history.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import orchestrator_pm
|
||||||
|
|
||||||
|
class TestOrchestratorPMHistory(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.test_dir = Path("test_conductor")
|
||||||
|
self.test_dir.mkdir(exist_ok=True)
|
||||||
|
self.archive_dir = self.test_dir / "archive"
|
||||||
|
self.tracks_dir = self.test_dir / "tracks"
|
||||||
|
self.archive_dir.mkdir(exist_ok=True)
|
||||||
|
self.tracks_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if self.test_dir.exists():
|
||||||
|
shutil.rmtree(self.test_dir)
|
||||||
|
|
||||||
|
def create_track(self, parent_dir, track_id, title, status, overview):
|
||||||
|
track_path = parent_dir / track_id
|
||||||
|
track_path.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
metadata = {"title": title, "status": status}
|
||||||
|
with open(track_path / "metadata.json", "w") as f:
|
||||||
|
json.dump(metadata, f)
|
||||||
|
|
||||||
|
spec_content = f"# Specification\n\n## Overview\n{overview}"
|
||||||
|
with open(track_path / "spec.md", "w") as f:
|
||||||
|
f.write(spec_content)
|
||||||
|
|
||||||
|
@patch('orchestrator_pm.CONDUCTOR_PATH', Path("test_conductor"))
|
||||||
|
def test_get_track_history_summary(self):
|
||||||
|
# Setup mock tracks
|
||||||
|
self.create_track(self.archive_dir, "track_001", "Initial Setup", "completed", "Setting up the project structure.")
|
||||||
|
self.create_track(self.tracks_dir, "track_002", "Feature A", "in_progress", "Implementing Feature A.")
|
||||||
|
|
||||||
|
summary = orchestrator_pm.get_track_history_summary()
|
||||||
|
|
||||||
|
self.assertIn("Initial Setup", summary)
|
||||||
|
self.assertIn("completed", summary)
|
||||||
|
self.assertIn("Setting up the project structure.", summary)
|
||||||
|
self.assertIn("Feature A", summary)
|
||||||
|
self.assertIn("in_progress", summary)
|
||||||
|
self.assertIn("Implementing Feature A.", summary)
|
||||||
|
|
||||||
|
@patch('orchestrator_pm.CONDUCTOR_PATH', Path("test_conductor"))
|
||||||
|
def test_get_track_history_summary_missing_files(self):
|
||||||
|
# Track with missing spec.md
|
||||||
|
track_path = self.tracks_dir / "track_003"
|
||||||
|
track_path.mkdir(exist_ok=True)
|
||||||
|
with open(track_path / "metadata.json", "w") as f:
|
||||||
|
json.dump({"title": "Missing Spec", "status": "pending"}, f)
|
||||||
|
|
||||||
|
summary = orchestrator_pm.get_track_history_summary()
|
||||||
|
self.assertIn("Missing Spec", summary)
|
||||||
|
self.assertIn("pending", summary)
|
||||||
|
self.assertIn("No overview available", summary)
|
||||||
|
|
||||||
|
@patch('orchestrator_pm.summarize.build_summary_markdown')
|
||||||
|
@patch('ai_client.send')
|
||||||
|
def test_generate_tracks_with_history(self, mock_send, mock_summarize):
|
||||||
|
mock_summarize.return_value = "REPO_MAP"
|
||||||
|
mock_send.return_value = "[]"
|
||||||
|
|
||||||
|
history_summary = "PAST_HISTORY_SUMMARY"
|
||||||
|
orchestrator_pm.generate_tracks("req", {}, [], history_summary=history_summary)
|
||||||
|
|
||||||
|
args, kwargs = mock_send.call_args
|
||||||
|
self.assertIn(history_summary, kwargs['user_message'])
|
||||||
|
self.assertIn("### TRACK HISTORY:", kwargs['user_message'])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user