Compare commits

..

18 Commits

Author SHA1 Message Date
Ed_
6c887e498d checkpoint 2026-02-27 20:24:16 -05:00
Ed_
bf1faac4ea checkpoint! 2026-02-27 20:21:52 -05:00
Ed_
a744b39e4f chore(conductor): Archive track 'MMA Data Architecture & DAG Engine' 2026-02-27 20:21:21 -05:00
Ed_
c2c0b41571 chore(conductor): Mark 'Tiered Context Scoping & HITL Approval' as in-progress 2026-02-27 20:20:41 -05:00
Ed_
5f748c4de3 conductor(plan): Mark task 'Apply review suggestions' as complete 2026-02-27 20:20:09 -05:00
Ed_
6548ce6496 fix(conductor): Apply review suggestions for track 'mma_data_architecture_dag_engine' 2026-02-27 20:20:01 -05:00
Ed_
c15e8b8d1f docs(conductor): Synchronize docs for track 'MMA Data Architecture & DAG Engine' 2026-02-27 20:13:25 -05:00
Ed_
2d355d4461 chore(conductor): Mark track 'MMA Data Architecture & DAG Engine' as complete 2026-02-27 20:12:50 -05:00
Ed_
a9436cbdad conductor(plan): Mark Phase 3 'Execution State Machine' as complete 2026-02-27 20:12:42 -05:00
Ed_
2429b7c1b4 feat(mma): Connect ExecutionEngine to ConductorEngine and Tech Lead 2026-02-27 20:12:23 -05:00
Ed_
154957fe57 feat(mma): Implement ExecutionEngine with auto-queue and step-mode support 2026-02-27 20:11:11 -05:00
Ed_
f85ec9d06f feat(mma): Add topological sorting to TrackDAG with cycle detection 2026-02-27 20:04:04 -05:00
Ed_
a3cfeff9d8 feat(mma): Implement TrackDAG for dependency resolution and cycle detection 2026-02-27 19:58:10 -05:00
Ed_
3c0d412219 checkpoint 2026-02-27 19:54:12 -05:00
Ed_
46e11bccdc conductor(plan): Mark task 'Ensure Tier 2 history is scoped' as complete 2026-02-27 19:51:28 -05:00
Ed_
b845b89543 feat(mma): Implement track-scoped history and optimized sub-agent toolsets 2026-02-27 19:51:13 -05:00
Ed_
134a11cdc2 conductor(plan): Mark task 'Update project_manager.py' as complete 2026-02-27 19:45:36 -05:00
Ed_
e1a3712d9a feat(mma): Implement track-scoped state persistence and configure sub-agents 2026-02-27 19:45:21 -05:00
29 changed files with 857 additions and 135 deletions

View File

@@ -0,0 +1,18 @@
---
name: tier1-orchestrator
description: Tier 1 Orchestrator for product alignment and high-level planning.
model: gemini-3.1-pro-preview
tools:
- read_file
- list_directory
- glob
- grep_search
- google_web_search
- web_fetch
- codebase_investigator
- activate_skill
- discovered_tool_run_powershell
---
STRICT SYSTEM DIRECTIVE: You are a Tier 1 Orchestrator.
Focused on product alignment, high-level planning, and track initialization.
ONLY output the requested text. No pleasantries.

View File

@@ -0,0 +1,20 @@
---
name: tier2-tech-lead
description: Tier 2 Tech Lead for architectural design and execution.
model: gemini-3-flash-preview
tools:
- read_file
- write_file
- replace
- list_directory
- glob
- grep_search
- google_web_search
- web_fetch
- codebase_investigator
- activate_skill
- discovered_tool_run_powershell
---
STRICT SYSTEM DIRECTIVE: You are a Tier 2 Tech Lead.
Focused on architectural design and track execution.
ONLY output the requested text. No pleasantries.

View File

@@ -0,0 +1,22 @@
---
name: tier3-worker
description: Stateless Tier 3 Worker for code implementation and TDD.
model: gemini-3-flash-preview
tools:
- read_file
- write_file
- replace
- list_directory
- glob
- grep_search
- google_web_search
- web_fetch
- codebase_investigator
- activate_skill
- discovered_tool_run_powershell
---
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.
You have access to tools for reading and writing files, codebase investigation, and web tools.
You CAN execute PowerShell scripts or run shell commands via discovered_tool_run_powershell for verification and testing.
Follow TDD and return success status or code changes. No pleasantries, no conversational filler.

View File

@@ -0,0 +1,20 @@
---
name: tier4-qa
description: Stateless Tier 4 QA Agent for log analysis and diagnostics.
model: gemini-2.5-flash-lite
tools:
- read_file
- list_directory
- glob
- grep_search
- google_web_search
- web_fetch
- codebase_investigator
- activate_skill
- discovered_tool_run_powershell
---
STRICT SYSTEM DIRECTIVE: You are a stateless Tier 4 QA Agent.
Your goal is to analyze errors, summarize logs, or verify tests.
You have access to tools for reading files, exploring the codebase, and web tools.
You CAN execute PowerShell scripts or run shell commands via discovered_tool_run_powershell for diagnostics.
ONLY output the requested analysis. No pleasantries.

View File

@@ -0,0 +1,22 @@
[[rule]]
toolName = [
"read_file",
"write_file",
"replace",
"list_directory",
"glob",
"grep_search",
"search_files",
"get_file_summary",
"google_web_search",
"web_fetch",
"codebase_investigator",
"cli_help",
"activate_skill",
"run_shell_command",
"run_powershell",
"discovered_tool_run_powershell"
]
decision = "allow"
priority = 900
description = "Allow all MMA tools for sub-agents in headless mode."

View File

@@ -1,4 +1,7 @@
{ {
"experimental": {
"enableAgents": true
},
"tools": { "tools": {
"discoveryCommand": "python C:/projects/manual_slop/scripts/tool_discovery.py", "discoveryCommand": "python C:/projects/manual_slop/scripts/tool_discovery.py",
"whitelist": [ "whitelist": [

View File

@@ -0,0 +1,19 @@
# Implementation Plan: MMA Data Architecture & DAG Engine
## Phase 1: Track-Scoped State Management
- [x] Task: Define the data schema for a Track (Metadata, Discussion History, Task List). [2efe80e]
- [x] Task: Update `project_manager.py` to create and read from `tracks/<track_id>/state.toml`. [e1a3712]
- [x] Task: Ensure Tier 2 (Tech Lead) history is securely scoped to the active track's state file. [b845b89]
## Phase 2: Python DAG Engine
- [x] Task: Create a `Task` class with `status` (Blocked, Ready, In Progress, Review, Done) and `depends_on` fields. [a3cfeff]
- [x] Task: Implement a topological sorting algorithm to resolve execution order. [f85ec9d]
- [x] Task: Write robust unit tests verifying cycle detection and dependency resolution. [f85ec9d]
## Phase 3: Execution State Machine
- [x] Task: Implement the core loop that evaluates the DAG and identifies "Ready" tasks. [154957f]
- [x] Task: Create configuration settings for "Auto-Queue" vs "Manual Step" execution modes. [154957f]
- [x] Task: Connect the state machine to the backend dispatcher, preparing it for GUI integration. [2429b7c]
## Phase: Review Fixes
- [x] Task: Apply review suggestions [6548ce6]

View File

@@ -15,7 +15,10 @@ To serve as an expert-level utility for personal developer use on small projects
- **Tier 2 (Tech Lead):** Technical oversight and track execution (`/conductor:implement`) using `gemini-2.5-flash`. Maintains persistent context throughout implementation. - **Tier 2 (Tech Lead):** Technical oversight and track execution (`/conductor:implement`) using `gemini-2.5-flash`. Maintains persistent context throughout implementation.
- **Tier 3 (Worker):** Surgical code implementation and TDD using `gemini-2.5-flash` or `deepseek-v3`. Operates statelessly with tool access and dependency skeletons. - **Tier 3 (Worker):** Surgical code implementation and TDD using `gemini-2.5-flash` or `deepseek-v3`. Operates statelessly with tool access and dependency skeletons.
- **Tier 4 (QA):** Error analysis and diagnostics using `gemini-2.5-flash` or `deepseek-v3`. Operates statelessly with tool access. - **Tier 4 (QA):** Error analysis and diagnostics using `gemini-2.5-flash` or `deepseek-v3`. Operates statelessly with tool access.
- **MMA Delegation Engine:** Utilizes the `mma-exec` CLI and `mma.ps1` helper to route tasks, ensuring role-scoped context and detailed observability via timestamped sub-agent logs. Supports dynamic ticket creation and dependency resolution via an automated Dispatcher Loop. - **MMA Delegation Engine:** Route tasks, ensuring role-scoped context and detailed observability via timestamped sub-agent logs. Supports dynamic ticket creation and dependency resolution via an automated Dispatcher Loop.
- **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files (e.g., `conductor/tracks/<track_id>/state.toml`). This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective.
- **Native DAG Execution Engine:** Employs a Python-based Directed Acyclic Graph (DAG) engine to manage complex task dependencies, supporting automated topological sorting and robust cycle detection.
- **Programmable Execution State Machine:** Governing the transition between "Auto-Queue" (autonomous worker spawning) and "Step Mode" (explicit manual approval for each task transition).
- **Role-Scoped Documentation:** Automated mapping of foundational documents to specific tiers to prevent token bloat and maintain high-signal context. - **Role-Scoped Documentation:** Automated mapping of foundational documents to specific tiers to prevent token bloat and maintain high-signal context.
- **Strict Memory Siloing:** Employs tree-sitter AST-based interface extraction (Skeleton View, Curated View) and "Context Amnesia" to provide workers only with the absolute minimum context required, preventing hallucination loops. - **Strict Memory Siloing:** Employs tree-sitter AST-based interface extraction (Skeleton View, Curated View) and "Context Amnesia" to provide workers only with the absolute minimum context required, preventing hallucination loops.
- **Explicit Execution Control:** All AI-generated PowerShell scripts require explicit human confirmation via interactive UI dialogs before execution, supported by a global "Linear Execution Clutch" for deterministic debugging. - **Explicit Execution Control:** All AI-generated PowerShell scripts require explicit human confirmation via interactive UI dialogs before execution, supported by a global "Linear Execution Clutch" for deterministic debugging.

View File

@@ -37,6 +37,7 @@
- **pytest:** For unit and integration testing, leveraging custom fixtures for live GUI verification. - **pytest:** For unit and integration testing, leveraging custom fixtures for live GUI verification.
- **ApiHookClient:** A dedicated IPC client for automated GUI interaction and state inspection. - **ApiHookClient:** A dedicated IPC client for automated GUI interaction and state inspection.
- **mma-exec / mma.ps1:** Python-based execution engine and PowerShell wrapper for managing the 4-Tier MMA hierarchy and automated documentation mapping. - **mma-exec / mma.ps1:** Python-based execution engine and PowerShell wrapper for managing the 4-Tier MMA hierarchy and automated documentation mapping.
- **dag_engine.py:** A native Python utility implementing `TrackDAG` and `ExecutionEngine` for dependency resolution, cycle detection, and programmable task execution loops.
## Architectural Patterns ## Architectural Patterns

View File

@@ -20,12 +20,7 @@ This file tracks all major tracks for the project. Each track has its own detail
--- ---
- [ ] **Track: MMA Data Architecture & DAG Engine** - [~] **Track: Tiered Context Scoping & HITL Approval**
*Link: [./tracks/mma_data_architecture_dag_engine/](./tracks/mma_data_architecture_dag_engine/)*
---
- [ ] **Track: Tiered Context Scoping & HITL Approval**
*Link: [./tracks/tiered_context_scoping_hitl_approval/](./tracks/tiered_context_scoping_hitl_approval/)* *Link: [./tracks/tiered_context_scoping_hitl_approval/](./tracks/tiered_context_scoping_hitl_approval/)*
--- ---

View File

@@ -1,16 +0,0 @@
# Implementation Plan: MMA Data Architecture & DAG Engine
## Phase 1: Track-Scoped State Management
- [x] Task: Define the data schema for a Track (Metadata, Discussion History, Task List). [2efe80e]
- [ ] Task: Update `project_manager.py` to create and read from `tracks/<track_id>/state.toml`.
- [ ] Task: Ensure Tier 2 (Tech Lead) history is securely scoped to the active track's state file.
## Phase 2: Python DAG Engine
- [ ] Task: Create a `Task` class with `status` (Blocked, Ready, In Progress, Review, Done) and `depends_on` fields.
- [ ] Task: Implement a topological sorting algorithm to resolve execution order.
- [ ] Task: Write robust unit tests verifying cycle detection and dependency resolution.
## Phase 3: Execution State Machine
- [ ] Task: Implement the core loop that evaluates the DAG and identifies "Ready" tasks.
- [ ] Task: Create configuration settings for "Auto-Queue" vs "Manual Step" execution modes.
- [ ] Task: Connect the state machine to the backend dispatcher, preparing it for GUI integration.

View File

@@ -56,43 +56,29 @@ def generate_tickets(track_brief: str, module_skeletons: str) -> list[dict]:
# Restore old system prompt # Restore old system prompt
ai_client.set_custom_system_prompt(old_system_prompt) ai_client.set_custom_system_prompt(old_system_prompt)
from dag_engine import TrackDAG
from models import Ticket
def topological_sort(tickets: list[dict]) -> list[dict]: def topological_sort(tickets: list[dict]) -> list[dict]:
""" """
Sorts a list of tickets based on their 'depends_on' field. Sorts a list of tickets based on their 'depends_on' field.
Raises ValueError if a circular dependency or missing internal dependency is detected. Raises ValueError if a circular dependency or missing internal dependency is detected.
""" """
# 1. Map ID to ticket and build graph # 1. Convert to Ticket objects for TrackDAG
ticket_objs = []
for t_data in tickets:
ticket_objs.append(Ticket.from_dict(t_data))
# 2. Use TrackDAG for validation and sorting
dag = TrackDAG(ticket_objs)
try:
sorted_ids = dag.topological_sort()
except ValueError as e:
raise ValueError(f"DAG Validation Error: {e}")
# 3. Return sorted dictionaries
ticket_map = {t['id']: t for t in tickets} ticket_map = {t['id']: t for t in tickets}
adj = {t['id']: [] for t in tickets} return [ticket_map[tid] for tid in sorted_ids]
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__": if __name__ == "__main__":
# Quick test if run directly # Quick test if run directly

161
dag_engine.py Normal file
View File

@@ -0,0 +1,161 @@
from typing import List, Optional
from models import Ticket
class TrackDAG:
"""
Manages a Directed Acyclic Graph of implementation tickets.
Provides methods for dependency resolution, cycle detection, and topological sorting.
"""
def __init__(self, tickets: List[Ticket]):
"""
Initializes the TrackDAG with a list of Ticket objects.
Args:
tickets: A list of Ticket instances defining the graph nodes and edges.
"""
self.tickets = tickets
self.ticket_map = {t.id: t for t in tickets}
def get_ready_tasks(self) -> List[Ticket]:
"""
Returns a list of tickets that are in 'todo' status and whose dependencies are all 'completed'.
Returns:
A list of Ticket objects ready for execution.
"""
ready = []
for ticket in self.tickets:
if ticket.status == 'todo':
# Check if all dependencies exist and are completed
all_done = True
for dep_id in ticket.depends_on:
dep = self.ticket_map.get(dep_id)
if not dep or dep.status != 'completed':
all_done = False
break
if all_done:
ready.append(ticket)
return ready
def has_cycle(self) -> bool:
"""
Performs a Depth-First Search to detect cycles in the dependency graph.
Returns:
True if a cycle is detected, False otherwise.
"""
visited = set()
rec_stack = set()
def is_cyclic(ticket_id: str) -> bool:
"""Internal recursive helper for cycle detection."""
if ticket_id in rec_stack:
return True
if ticket_id in visited:
return False
visited.add(ticket_id)
rec_stack.add(ticket_id)
ticket = self.ticket_map.get(ticket_id)
if ticket:
for neighbor in ticket.depends_on:
if is_cyclic(neighbor):
return True
rec_stack.remove(ticket_id)
return False
for ticket in self.tickets:
if ticket.id not in visited:
if is_cyclic(ticket.id):
return True
return False
def topological_sort(self) -> List[str]:
"""
Returns a list of ticket IDs in topological order (dependencies before dependents).
Returns:
A list of ticket ID strings.
Raises:
ValueError: If a dependency cycle is detected.
"""
if self.has_cycle():
raise ValueError("Dependency cycle detected")
visited = set()
stack = []
def visit(ticket_id: str):
"""Internal recursive helper for topological sorting."""
if ticket_id in visited:
return
visited.add(ticket_id)
ticket = self.ticket_map.get(ticket_id)
if ticket:
for dep_id in ticket.depends_on:
visit(dep_id)
stack.append(ticket_id)
for ticket in self.tickets:
visit(ticket.id)
return stack
class ExecutionEngine:
"""
A state machine that governs the progression of tasks within a TrackDAG.
Handles automatic queueing and manual task approval.
"""
def __init__(self, dag: TrackDAG, auto_queue: bool = False):
"""
Initializes the ExecutionEngine.
Args:
dag: The TrackDAG instance to manage.
auto_queue: If True, ready tasks will automatically move to 'in_progress'.
"""
self.dag = dag
self.auto_queue = auto_queue
def tick(self) -> List[Ticket]:
"""
Evaluates the DAG and returns a list of tasks that are currently 'ready' for execution.
If auto_queue is enabled, tasks without 'step_mode' will be marked as 'in_progress'.
Returns:
A list of ready Ticket objects.
"""
ready = self.dag.get_ready_tasks()
if self.auto_queue:
for ticket in ready:
if not ticket.step_mode:
ticket.status = "in_progress"
return ready
def approve_task(self, task_id: str):
"""
Manually transitions a task from 'todo' to 'in_progress' if its dependencies are met.
Args:
task_id: The ID of the task to approve.
"""
ticket = self.dag.ticket_map.get(task_id)
if ticket and ticket.status == "todo":
# Check if dependencies are met first
all_done = True
for dep_id in ticket.depends_on:
dep = self.dag.ticket_map.get(dep_id)
if not dep or dep.status != "completed":
all_done = False
break
if all_done:
ticket.status = "in_progress"
def update_task_status(self, task_id: str, status: str):
"""
Force-updates the status of a specific task.
Args:
task_id: The ID of the task.
status: The new status string (e.g., 'todo', 'in_progress', 'completed', 'blocked').
"""
ticket = self.dag.ticket_map.get(task_id)
if ticket:
ticket.status = status

View File

@@ -754,6 +754,12 @@ class App:
self.active_track = None self.active_track = None
self.active_tickets = [] self.active_tickets = []
# Load track-scoped history if track is active
if self.active_track:
track_history = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir)
if track_history:
self.disc_entries = _parse_history_entries(track_history, self.disc_roles)
def _save_active_project(self): def _save_active_project(self):
if self.active_project_path: if self.active_project_path:
try: try:
@@ -790,6 +796,10 @@ class App:
def _flush_disc_entries_to_project(self): def _flush_disc_entries_to_project(self):
history_strings = [project_manager.entry_to_str(e) for e in self.disc_entries] history_strings = [project_manager.entry_to_str(e) for e in self.disc_entries]
if self.active_track:
project_manager.save_track_history(self.active_track.id, history_strings, self.ui_files_base_dir)
return
disc_sec = self.project.setdefault("discussion", {}) disc_sec = self.project.setdefault("discussion", {})
discussions = disc_sec.setdefault("discussions", {}) discussions = disc_sec.setdefault("discussions", {})
disc_data = discussions.setdefault(self.active_discussion, project_manager.default_discussion()) disc_data = discussions.setdefault(self.active_discussion, project_manager.default_discussion())
@@ -1398,7 +1408,8 @@ class App:
self._save_active_project() self._save_active_project()
self._flush_to_config() self._flush_to_config()
save_config(self.config) save_config(self.config)
flat = project_manager.flat_config(self.project, self.active_discussion) track_id = self.active_track.id if self.active_track else None
flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id)
full_md, path, file_items = aggregate.run(flat) full_md, path, file_items = aggregate.run(flat)
# Build stable markdown (no history) for Gemini caching # Build stable markdown (no history) for Gemini caching
screenshot_base_dir = Path(flat.get("screenshots", {}).get("base_dir", ".")) screenshot_base_dir = Path(flat.get("screenshots", {}).get("base_dir", "."))
@@ -2077,11 +2088,24 @@ class App:
track_id = f"track_{uuid.uuid4().hex[:8]}" track_id = f"track_{uuid.uuid4().hex[:8]}"
track = Track(id=track_id, description=title, tickets=tickets) track = Track(id=track_id, description=title, tickets=tickets)
# 4. Initialize ConductorEngine and run_linear loop # Initialize track state in the filesystem
from models import TrackState, Metadata
from datetime import datetime
now = datetime.now()
meta = Metadata(id=track_id, name=title, status="todo", created_at=now, updated_at=now)
state = TrackState(metadata=meta, discussion=[], tasks=tickets)
project_manager.save_track_state(track_id, state, self.ui_files_base_dir)
# 4. Initialize ConductorEngine and run loop
engine = multi_agent_conductor.ConductorEngine(track, self.event_queue) engine = multi_agent_conductor.ConductorEngine(track, self.event_queue)
# Use current full markdown context for the track execution
track_id_param = track.id
flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id_param)
full_md, _, _ = aggregate.run(flat)
# Schedule the coroutine on the internal event loop # Schedule the coroutine on the internal event loop
asyncio.run_coroutine_threadsafe(engine.run_linear(), self._loop) asyncio.run_coroutine_threadsafe(engine.run(md_content=full_md), self._loop)
except Exception as e: except Exception as e:
self.ai_status = f"Track start error: {e}" self.ai_status = f"Track start error: {e}"
print(f"ERROR in _start_track_logic: {e}") print(f"ERROR in _start_track_logic: {e}")

View File

@@ -78,8 +78,8 @@ Collapsed=0
DockId=0x0000000F,2 DockId=0x0000000F,2
[Window][Theme] [Window][Theme]
Pos=0,21 Pos=0,17
Size=639,824 Size=393,824
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=641,21 Pos=395,17
Size=1092,908 Size=734,545
Collapsed=0 Collapsed=0
DockId=0x00000010,0 DockId=0x00000010,0
[Window][Context Hub] [Window][Context Hub]
Pos=0,21 Pos=0,17
Size=639,824 Size=393,824
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=1735,21 Pos=1131,17
Size=783,1586 Size=549,1183
Collapsed=0 Collapsed=0
DockId=0x00000004,0 DockId=0x00000004,0
[Window][Operations Hub] [Window][Operations Hub]
Pos=641,21 Pos=395,17
Size=1092,908 Size=734,545
Collapsed=0 Collapsed=0
DockId=0x00000010,1 DockId=0x00000010,1
[Window][Files & Media] [Window][Files & Media]
Pos=0,847 Pos=0,843
Size=639,760 Size=393,357
Collapsed=0 Collapsed=0
DockId=0x00000006,1 DockId=0x00000006,1
[Window][AI Settings] [Window][AI Settings]
Pos=0,847 Pos=0,843
Size=639,760 Size=393,357
Collapsed=0 Collapsed=0
DockId=0x00000006,0 DockId=0x00000006,0
@@ -136,14 +136,14 @@ Size=416,325
Collapsed=0 Collapsed=0
[Window][MMA Dashboard] [Window][MMA Dashboard]
Pos=641,931 Pos=395,564
Size=1092,676 Size=734,636
Collapsed=0 Collapsed=0
DockId=0x00000011,0 DockId=0x00000011,0
[Window][Log Management] [Window][Log Management]
Pos=1735,21 Pos=1131,17
Size=783,1586 Size=549,1183
Collapsed=0 Collapsed=0
DockId=0x00000004,1 DockId=0x00000004,1
@@ -167,20 +167,20 @@ Column 6 Weight=1.0079
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,21 Size=2518,1586 Split=Y DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,17 Size=1680,1183 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=1733,1183 Split=X DockNode ID=0x00000003 Parent=0x0000000C SizeRef=1129,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
DockNode ID=0x00000002 Parent=0x0000000B SizeRef=1029,1119 Split=X Selected=0xF4139CA2 DockNode ID=0x00000002 Parent=0x0000000B SizeRef=1029,1119 Split=X Selected=0xF4139CA2
DockNode ID=0x00000007 Parent=0x00000002 SizeRef=639,858 Split=Y Selected=0x8CA2375C DockNode ID=0x00000007 Parent=0x00000002 SizeRef=393,858 Split=Y Selected=0x8CA2375C
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=295,824 Selected=0xF4139CA2 DockNode ID=0x00000005 Parent=0x00000007 SizeRef=295,824 Selected=0xF4139CA2
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=295,724 CentralNode=1 Selected=0x7BD57D6A DockNode ID=0x00000006 Parent=0x00000007 SizeRef=295,724 CentralNode=1 Selected=0x7BD57D6A
DockNode ID=0x0000000E Parent=0x00000002 SizeRef=1092,858 Split=Y Selected=0x418C7449 DockNode ID=0x0000000E Parent=0x00000002 SizeRef=734,858 Split=Y Selected=0x418C7449
DockNode ID=0x00000010 Parent=0x0000000E SizeRef=868,887 Selected=0x418C7449 DockNode ID=0x00000010 Parent=0x0000000E SizeRef=868,545 Selected=0xB4CBF21A
DockNode ID=0x00000011 Parent=0x0000000E SizeRef=868,661 Selected=0x3AEC3498 DockNode ID=0x00000011 Parent=0x0000000E SizeRef=868,636 Selected=0x3AEC3498
DockNode ID=0x00000001 Parent=0x0000000B SizeRef=1029,775 Selected=0x8B4EBFA6 DockNode ID=0x00000001 Parent=0x0000000B SizeRef=1029,775 Selected=0x8B4EBFA6
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
DockNode ID=0x00000004 Parent=0x0000000C SizeRef=783,1183 Selected=0x6F2B5B04 DockNode ID=0x00000004 Parent=0x0000000C SizeRef=549,1183 Selected=0x6F2B5B04
DockNode ID=0x0000000F Parent=0xAFC85805 SizeRef=1362,451 Selected=0xDD6419BC DockNode ID=0x0000000F Parent=0xAFC85805 SizeRef=1362,451 Selected=0xDD6419BC
;;;<<<Layout_655921752_Default>>>;;; ;;;<<<Layout_655921752_Default>>>;;;

View File

@@ -99,8 +99,19 @@ def _is_allowed(path: Path) -> bool:
rp = path.resolve(strict=True) rp = path.resolve(strict=True)
except (OSError, ValueError): except (OSError, ValueError):
rp = path.resolve() rp = path.resolve()
if rp in _allowed_paths: if rp in _allowed_paths:
return True return True
# Allow current working directory and subpaths by default if no base_dirs
cwd = Path.cwd().resolve()
if not _base_dirs:
try:
rp.relative_to(cwd)
return True
except ValueError:
pass
for bd in _base_dirs: for bd in _base_dirs:
try: try:
rp.relative_to(bd) rp.relative_to(bd)

View File

@@ -26,12 +26,13 @@ If you run a test or command that fails with a significant error or large traceb
1. **DO NOT** analyze the raw logs in your own context window. 1. **DO NOT** analyze the raw logs in your own context window.
2. **DO** spawn a stateless Tier 4 agent to diagnose the failure. 2. **DO** spawn a stateless Tier 4 agent to diagnose the failure.
3. *Command:* `uv run python scripts/mma_exec.py --role tier4-qa "Analyze this failure and summarize the root cause: [LOG_DATA]"` 3. *Command:* `uv run python scripts/mma_exec.py --role tier4-qa "Analyze this failure and summarize the root cause: [LOG_DATA]"`
4. Avoid direct reads to files, use file summaries or ast skeletons for files if they are code and we have a tool for parsing them.
## 3. Persistent Tech Lead Memory (Tier 2) ## 3. Persistent Tech Lead Memory (Tier 2)
Unlike the stateless sub-agents (Tiers 3 & 4), the **Tier 2 Tech Lead** maintains persistent context throughout the implementation of a track. Do NOT apply "Context Amnesia" to your own session during track implementation. You are responsible for the continuity of the technical strategy. Unlike the stateless sub-agents (Tiers 3 & 4), the **Tier 2 Tech Lead** maintains persistent context throughout the implementation of a track. Do NOT apply "Context Amnesia" to your own session during track implementation. You are responsible for the continuity of the technical strategy.
## 4. AST Skeleton Views ## 4. AST Skeleton Views
To minimize context bloat for Tier 3, use "Skeleton Views" of dependencies (extracted via `mcp_client.py` or similar) instead of full file contents, unless the Tier 3 worker is explicitly modifying that specific file. To minimize context bloat for Tier 2 & 3, use "Skeleton Views" of dependencies (extracted via `mcp_client.py` or similar) instead of full file contents, unless the Tier 3 worker is explicitly modifying that specific file.
<examples> <examples>
### Example 1: Spawning a Tier 4 QA Agent ### Example 1: Spawning a Tier 4 QA Agent

View File

@@ -7,11 +7,13 @@ import events
from models import Ticket, Track, WorkerContext from models import Ticket, Track, WorkerContext
from file_cache import ASTParser from file_cache import ASTParser
from dag_engine import TrackDAG, ExecutionEngine
class ConductorEngine: class ConductorEngine:
""" """
Orchestrates the execution of tickets within a track. Orchestrates the execution of tickets within a track.
""" """
def __init__(self, track: Track, event_queue: Optional[events.AsyncEventQueue] = None): def __init__(self, track: Track, event_queue: Optional[events.AsyncEventQueue] = None, auto_queue: bool = False):
self.track = track self.track = track
self.event_queue = event_queue self.event_queue = event_queue
self.tier_usage = { self.tier_usage = {
@@ -20,6 +22,8 @@ class ConductorEngine:
"Tier 3": {"input": 0, "output": 0}, "Tier 3": {"input": 0, "output": 0},
"Tier 4": {"input": 0, "output": 0}, "Tier 4": {"input": 0, "output": 0},
} }
self.dag = TrackDAG(self.track.tickets)
self.engine = ExecutionEngine(self.dag, auto_queue=auto_queue)
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:
@@ -59,58 +63,83 @@ class ConductorEngine:
step_mode=ticket_data.get("step_mode", False) step_mode=ticket_data.get("step_mode", False)
) )
self.track.tickets.append(ticket) self.track.tickets.append(ticket)
# Rebuild DAG and Engine after parsing new tickets
self.dag = TrackDAG(self.track.tickets)
self.engine = ExecutionEngine(self.dag, auto_queue=self.engine.auto_queue)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print(f"Error parsing JSON tickets: {e}") print(f"Error parsing JSON tickets: {e}")
except KeyError as e: except KeyError as e:
print(f"Missing required field in ticket definition: {e}") print(f"Missing required field in ticket definition: {e}")
async def run_linear(self): async def run(self, md_content: str = ""):
""" """
Executes tickets sequentially according to their dependencies. Main execution loop using the DAG engine.
Iterates through the track's executable tickets until no more can be run. Args:
Supports dynamic execution as tickets added during runtime will be picked up md_content: The full markdown context (history + files) for AI workers.
in the next iteration of the main loop.
""" """
await self._push_state(status="running", active_tier="Tier 2 (Tech Lead)") await self._push_state(status="running", active_tier="Tier 2 (Tech Lead)")
while True: while True:
executable = self.track.get_executable_tickets() # 1. Identify ready tasks
if not executable: ready_tasks = self.engine.tick()
# Check if we are finished or blocked
# 2. Check for completion or blockage
if not ready_tasks:
all_done = all(t.status == "completed" for t in self.track.tickets) all_done = all(t.status == "completed" for t in self.track.tickets)
if all_done: if all_done:
print("Track completed successfully.") print("Track completed successfully.")
await self._push_state(status="done", active_tier=None) await self._push_state(status="done", active_tier=None)
else: else:
# If we have no executable tickets but some are not completed, we might be blocked # Check if any tasks are in-progress or could be ready
# or there are simply no more tickets to run at this moment. if any(t.status == "in_progress" for t in self.track.tickets):
incomplete = [t for t in self.track.tickets if t.status != "completed"] # Wait for async tasks to complete
if not incomplete: await asyncio.sleep(1)
print("Track completed successfully.") continue
await self._push_state(status="done", active_tier=None)
else: print("No more executable tickets. Track is blocked or finished.")
print(f"No more executable tickets. {len(incomplete)} tickets remain incomplete.") await self._push_state(status="blocked", active_tier=None)
await self._push_state(status="blocked", active_tier=None)
break break
for ticket in executable:
# We re-check status in case it was modified by a parallel/dynamic process
# (though run_linear is currently single-threaded)
if ticket.status != "todo":
continue
print(f"Executing ticket {ticket.id}: {ticket.description}") # 3. Process ready tasks
ticket.status = "running" loop = asyncio.get_event_loop()
await self._push_state(active_tier=f"Tier 3 (Worker): {ticket.id}") for ticket in ready_tasks:
# If auto_queue is on and step_mode is off, engine.tick() already marked it 'in_progress'
# but we need to verify and handle the lifecycle.
if ticket.status == "in_progress" or (not ticket.step_mode and self.engine.auto_queue):
ticket.status = "in_progress"
print(f"Executing ticket {ticket.id}: {ticket.description}")
await self._push_state(active_tier=f"Tier 3 (Worker): {ticket.id}")
context = WorkerContext(
ticket_id=ticket.id,
model_name="gemini-2.5-flash-lite",
messages=[]
)
# Offload the blocking lifecycle call to a thread to avoid blocking the async event loop.
# We pass the md_content so the worker has full context.
context_files = ticket.context_requirements if ticket.context_requirements else None
await loop.run_in_executor(
None,
run_worker_lifecycle,
ticket,
context,
context_files,
self.event_queue,
self,
md_content
)
await self._push_state(active_tier="Tier 2 (Tech Lead)")
# For now, we use a default model name or take it from config elif ticket.status == "todo" and (ticket.step_mode or not self.engine.auto_queue):
context = WorkerContext( # Task is ready but needs approval
ticket_id=ticket.id, print(f"Ticket {ticket.id} is ready and awaiting approval.")
model_name="gemini-2.5-flash-lite", await self._push_state(active_tier=f"Awaiting Approval: {ticket.id}")
messages=[] # In a real UI, this would wait for a user event.
) # For now, we'll treat it as a pause point if not auto-queued.
run_worker_lifecycle(ticket, context, event_queue=self.event_queue, engine=self) pass
await self._push_state(active_tier="Tier 2 (Tech Lead)")
def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_id: str) -> bool: def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_id: str) -> bool:
""" """
@@ -152,10 +181,17 @@ def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_
return False return False
def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: List[str] = None, event_queue: events.AsyncEventQueue = None, engine: Optional['ConductorEngine'] = None): def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: List[str] = None, event_queue: events.AsyncEventQueue = None, engine: Optional['ConductorEngine'] = None, md_content: str = ""):
""" """
Simulates the lifecycle of a single agent working on a ticket. Simulates the lifecycle of a single agent working on a ticket.
Calls the AI client and updates the ticket status based on the response. Calls the AI client and updates the ticket status based on the response.
Args:
ticket: The ticket to process.
context: The worker context.
context_files: List of files to include in the context.
event_queue: Queue for pushing state updates and receiving approvals.
engine: The conductor engine.
md_content: The markdown context (history + files) for AI workers.
""" """
# Enforce Context Amnesia: each ticket starts with a clean slate. # Enforce Context Amnesia: each ticket starts with a clean slate.
ai_client.reset_session() ai_client.reset_session()
@@ -165,6 +201,11 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
parser = ASTParser(language="python") parser = ASTParser(language="python")
for i, file_path in enumerate(context_files): for i, file_path in enumerate(context_files):
try: try:
abs_path = Path(file_path)
if not abs_path.is_absolute() and engine:
# Resolve relative to project base if possible
# (This is a bit simplified, but helps)
pass
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
if i == 0: if i == 0:
@@ -188,8 +229,6 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
"start your response with 'BLOCKED' and explain why." "start your response with 'BLOCKED' and explain why."
) )
# In a real scenario, we would pass md_content from the aggregator
# and manage the conversation history in the context.
# HITL Clutch: pass the queue and ticket_id to confirm_execution # HITL Clutch: pass the queue and ticket_id to confirm_execution
def clutch_callback(payload: str) -> bool: def clutch_callback(payload: str) -> bool:
if not event_queue: if not event_queue:
@@ -197,7 +236,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
return confirm_execution(payload, event_queue, ticket.id) return confirm_execution(payload, event_queue, ticket.id)
response = ai_client.send( response = ai_client.send(
md_content="", md_content=md_content,
user_message=user_message, user_message=user_message,
base_dir=".", base_dir=".",
pre_tool_callback=clutch_callback if ticket.step_mode else None, pre_tool_callback=clutch_callback if ticket.step_mode else None,

View File

@@ -225,11 +225,17 @@ def migrate_from_legacy_config(cfg: dict) -> dict:
# ── flat config for aggregate.run() ───────────────────────────────────────── # ── flat config for aggregate.run() ─────────────────────────────────────────
def flat_config(proj: dict, disc_name: str | None = None) -> dict: def flat_config(proj: dict, disc_name: str | None = None, track_id: str | None = None) -> dict:
"""Return a flat config dict compatible with aggregate.run().""" """Return a flat config dict compatible with aggregate.run()."""
disc_sec = proj.get("discussion", {}) disc_sec = proj.get("discussion", {})
name = disc_name or disc_sec.get("active", "main")
disc_data = disc_sec.get("discussions", {}).get(name, {}) if track_id:
history = load_track_history(track_id, proj.get("files", {}).get("base_dir", "."))
else:
name = disc_name or disc_sec.get("active", "main")
disc_data = disc_sec.get("discussions", {}).get(name, {})
history = disc_data.get("history", [])
return { return {
"project": proj.get("project", {}), "project": proj.get("project", {}),
"output": proj.get("output", {}), "output": proj.get("output", {}),
@@ -237,6 +243,69 @@ def flat_config(proj: dict, disc_name: str | None = None) -> dict:
"screenshots": proj.get("screenshots", {}), "screenshots": proj.get("screenshots", {}),
"discussion": { "discussion": {
"roles": disc_sec.get("roles", []), "roles": disc_sec.get("roles", []),
"history": disc_data.get("history", []), "history": history,
}, },
} }
# ── track state persistence ─────────────────────────────────────────────────
def save_track_state(track_id: str, state: 'TrackState', base_dir: str | Path = "."):
"""
Saves a TrackState object to conductor/tracks/<track_id>/state.toml.
"""
track_dir = Path(base_dir) / "conductor" / "tracks" / track_id
track_dir.mkdir(parents=True, exist_ok=True)
state_file = track_dir / "state.toml"
data = clean_nones(state.to_dict())
with open(state_file, "wb") as f:
tomli_w.dump(data, f)
def load_track_state(track_id: str, base_dir: str | Path = ".") -> 'TrackState':
"""
Loads a TrackState object from conductor/tracks/<track_id>/state.toml.
"""
from models import TrackState
state_file = Path(base_dir) / "conductor" / "tracks" / track_id / "state.toml"
if not state_file.exists():
return None
with open(state_file, "rb") as f:
data = tomllib.load(f)
return TrackState.from_dict(data)
def load_track_history(track_id: str, base_dir: str | Path = ".") -> list:
"""
Loads the discussion history for a specific track from its state.toml.
Returns a list of entry strings formatted with @timestamp.
"""
from models import TrackState
state = load_track_state(track_id, base_dir)
if not state:
return []
history = []
for entry in state.discussion:
e = dict(entry)
ts = e.get("ts")
if isinstance(ts, datetime.datetime):
e["ts"] = ts.strftime(TS_FMT)
history.append(entry_to_str(e))
return history
def save_track_history(track_id: str, history: list, base_dir: str | Path = "."):
"""
Saves the discussion history for a specific track to its state.toml.
'history' is expected to be a list of formatted strings.
"""
from models import TrackState
state = load_track_state(track_id, base_dir)
if not state:
return
roles = ["User", "AI", "Vendor API", "System", "Reasoning"]
entries = [str_to_entry(h, roles) for h in history]
state.discussion = entries
save_track_state(track_id, state, base_dir)

36
reproduce_issue.py Normal file
View File

@@ -0,0 +1,36 @@
import pytest
from models import Ticket
from dag_engine import TrackDAG, ExecutionEngine
def test_auto_queue_and_step_mode():
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker")
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", step_mode=True)
dag = TrackDAG([t1, t2])
# Expectation: ExecutionEngine takes auto_queue parameter
try:
engine = ExecutionEngine(dag, auto_queue=True)
except TypeError:
pytest.fail("ExecutionEngine does not accept auto_queue parameter")
# Tick 1: T1 should be 'in-progress' because auto_queue=True
# T2 should remain 'todo' because step_mode=True
engine.tick()
assert t1.status == "in_progress"
assert t2.status == "todo"
# Approve T2
try:
engine.approve_task("T2")
except AttributeError:
pytest.fail("ExecutionEngine does not have approve_task method")
assert t2.status == "in_progress"
if __name__ == "__main__":
try:
test_auto_queue_and_step_mode()
print("Test passed (unexpectedly)")
except Exception as e:
print(f"Test failed as expected: {e}")

View File

@@ -176,17 +176,16 @@ 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 (e.g., read_file, write_file, replace). " \ "You have access to tools for reading and writing files (e.g., read_file, write_file, replace), " \
"CRITICAL: You CANNOT execute PowerShell scripts or run shell commands directly. " \ "codebase investigation (codebase_investigator), and web tools (google_web_search, web_fetch). " \
"If you need to verify code or run tests, output the full PowerShell script inside a " \ "You CAN execute PowerShell scripts via discovered_tool_run_powershell for verification and testing. " \
"markdown code block (e.g., ```powershell) and state that it needs to be executed. " \
"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 (codebase_investigator), " \
"CRITICAL: You CANNOT execute PowerShell scripts or run shell commands directly. " \ "and web tools (google_web_search, web_fetch). " \
"If you need to run diagnostics, output the PowerShell script and request execution. " \ "You CAN execute PowerShell scripts via discovered_tool_run_powershell 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}. " \
@@ -209,7 +208,8 @@ def execute_agent(role: str, prompt: str, docs: list[str]) -> str:
# Use subprocess with input to pipe the prompt via stdin, avoiding WinError 206. # Use subprocess with input to pipe the prompt via stdin, avoiding WinError 206.
# 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.
# Whitelist tools to ensure they are available to the model in headless mode. # Whitelist tools to ensure they are available to the model in headless mode.
allowed_tools = "read_file,write_file,replace,list_directory,glob,grep_search,search_files,get_file_summary" # Using 'discovered_tool_run_powershell' as it's the confirmed name for shell access.
allowed_tools = "read_file,write_file,replace,list_directory,glob,grep_search,search_files,get_file_summary,discovered_tool_run_powershell,activate_skill,codebase_investigator,google_web_search,web_fetch"
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' --allowed-tools {allowed_tools} --output-format json --model {model}" f"gemini -p 'mma_task' --allowed-tools {allowed_tools} --output-format json --model {model}"

View File

@@ -18,7 +18,7 @@ history = [
[discussions.AutoDisc] [discussions.AutoDisc]
git_commit = "" git_commit = ""
last_updated = "2026-02-27T19:23:11" last_updated = "2026-02-27T19:27:19"
history = [ history = [
"@2026-02-27T19:08:37\nSystem:\n[PERFORMANCE ALERT] Frame time high: 62.2ms. Please consider optimizing recent changes or reducing load.", "@2026-02-27T19:08:37\nSystem:\n[PERFORMANCE ALERT] Frame time high: 62.2ms. Please consider optimizing recent changes or reducing load.",
] ]

84
tests/test_dag_engine.py Normal file
View File

@@ -0,0 +1,84 @@
import pytest
from models import Ticket
from dag_engine import TrackDAG
def test_get_ready_tasks_linear():
t1 = Ticket(id="T1", description="Task 1", status="completed", assigned_to="worker")
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"])
t3 = Ticket(id="T3", description="Task 3", status="todo", assigned_to="worker", depends_on=["T2"])
dag = TrackDAG([t1, t2, t3])
ready = dag.get_ready_tasks()
assert len(ready) == 1
assert ready[0].id == "T2"
def test_get_ready_tasks_branching():
t1 = Ticket(id="T1", description="Task 1", status="completed", assigned_to="worker")
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"])
t3 = Ticket(id="T3", description="Task 3", status="todo", assigned_to="worker", depends_on=["T1"])
dag = TrackDAG([t1, t2, t3])
ready = dag.get_ready_tasks()
assert len(ready) == 2
ready_ids = {t.id for t in ready}
assert ready_ids == {"T2", "T3"}
def test_has_cycle_no_cycle():
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker")
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"])
dag = TrackDAG([t1, t2])
assert not dag.has_cycle()
def test_has_cycle_direct_cycle():
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker", depends_on=["T2"])
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"])
dag = TrackDAG([t1, t2])
assert dag.has_cycle()
def test_has_cycle_indirect_cycle():
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker", depends_on=["T2"])
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T3"])
t3 = Ticket(id="T3", description="Task 3", status="todo", assigned_to="worker", depends_on=["T1"])
dag = TrackDAG([t1, t2, t3])
assert dag.has_cycle()
def test_has_cycle_complex_no_cycle():
# T1 -> T2, T1 -> T3, T2 -> T4, T3 -> T4
t1 = Ticket(id="T1", description="T1", status="todo", assigned_to="worker", depends_on=["T2", "T3"])
t2 = Ticket(id="T2", description="T2", status="todo", assigned_to="worker", depends_on=["T4"])
t3 = Ticket(id="T3", description="T3", status="todo", assigned_to="worker", depends_on=["T4"])
t4 = Ticket(id="T4", description="T4", status="todo", assigned_to="worker")
dag = TrackDAG([t1, t2, t3, t4])
assert not dag.has_cycle()
def test_get_ready_tasks_multiple_deps():
t1 = Ticket(id="T1", description="T1", status="completed", assigned_to="worker")
t2 = Ticket(id="T2", description="T2", status="completed", assigned_to="worker")
t3 = Ticket(id="T3", description="T3", status="todo", assigned_to="worker", depends_on=["T1", "T2"])
dag = TrackDAG([t1, t2, t3])
assert [t.id for t in dag.get_ready_tasks()] == ["T3"]
t2.status = "todo"
assert [t.id for t in dag.get_ready_tasks()] == ["T2"]
def test_topological_sort():
t1 = Ticket(id="T1", description="T1", status="todo", assigned_to="worker")
t2 = Ticket(id="T2", description="T2", status="todo", assigned_to="worker", depends_on=["T1"])
t3 = Ticket(id="T3", description="T3", status="todo", assigned_to="worker", depends_on=["T2"])
dag = TrackDAG([t1, t2, t3])
sort = dag.topological_sort()
assert sort == ["T1", "T2", "T3"]
def test_topological_sort_cycle():
t1 = Ticket(id="T1", description="T1", status="todo", assigned_to="worker", depends_on=["T2"])
t2 = Ticket(id="T2", description="T2", status="todo", assigned_to="worker", depends_on=["T1"])
dag = TrackDAG([t1, t2])
with pytest.raises(ValueError, match="Dependency cycle detected"):
dag.topological_sort()

View File

@@ -0,0 +1,123 @@
import pytest
from models import Ticket
from dag_engine import TrackDAG, ExecutionEngine
def test_execution_engine_basic_flow():
# Setup tickets with dependencies
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker")
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"])
t3 = Ticket(id="T3", description="Task 3", status="todo", assigned_to="worker", depends_on=["T1"])
t4 = Ticket(id="T4", description="Task 4", status="todo", assigned_to="worker", depends_on=["T2", "T3"])
dag = TrackDAG([t1, t2, t3, t4])
engine = ExecutionEngine(dag)
# Tick 1: Only T1 should be ready
ready = engine.tick()
assert len(ready) == 1
assert ready[0].id == "T1"
# Complete T1
engine.update_task_status("T1", "completed")
# Tick 2: T2 and T3 should be ready
ready = engine.tick()
assert len(ready) == 2
ids = {t.id for t in ready}
assert ids == {"T2", "T3"}
# Complete T2
engine.update_task_status("T2", "completed")
# Tick 3: Only T3 should be ready (T4 depends on T2 AND T3)
ready = engine.tick()
assert len(ready) == 1
assert ready[0].id == "T3"
# Complete T3
engine.update_task_status("T3", "completed")
# Tick 4: T4 should be ready
ready = engine.tick()
assert len(ready) == 1
assert ready[0].id == "T4"
# Complete T4
engine.update_task_status("T4", "completed")
# Tick 5: Nothing ready
ready = engine.tick()
assert len(ready) == 0
def test_execution_engine_update_nonexistent_task():
dag = TrackDAG([])
engine = ExecutionEngine(dag)
# Should not raise error, or handle gracefully
engine.update_task_status("NONEXISTENT", "completed")
def test_execution_engine_status_persistence():
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker")
dag = TrackDAG([t1])
engine = ExecutionEngine(dag)
engine.update_task_status("T1", "in_progress")
assert t1.status == "in_progress"
ready = engine.tick()
assert len(ready) == 0 # Only 'todo' tasks should be returned by tick() if they are ready
def test_execution_engine_auto_queue():
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker")
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"])
dag = TrackDAG([t1, t2])
engine = ExecutionEngine(dag, auto_queue=True)
# Tick 1: T1 is ready and should be automatically marked as 'in_progress'
ready = engine.tick()
assert len(ready) == 1
assert ready[0].id == "T1"
assert t1.status == "in_progress"
# Tick 2: T1 is in_progress, so T2 is NOT ready yet (T1 must be 'completed')
ready = engine.tick()
assert len(ready) == 0
assert t2.status == "todo"
# Complete T1
engine.update_task_status("T1", "completed")
# Tick 3: T2 is now ready and should be automatically marked as 'in_progress'
ready = engine.tick()
assert len(ready) == 1
assert ready[0].id == "T2"
assert t2.status == "in_progress"
def test_execution_engine_step_mode():
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker", step_mode=True)
dag = TrackDAG([t1])
engine = ExecutionEngine(dag, auto_queue=True)
# Tick 1: T1 is ready, but step_mode=True, so it should NOT be automatically marked as 'in_progress'
ready = engine.tick()
assert len(ready) == 1
assert ready[0].id == "T1"
assert t1.status == "todo"
# Manual approval
engine.approve_task("T1")
assert t1.status == "in_progress"
# Tick 2: T1 is already in_progress, should not be returned by tick() (it's not 'ready'/todo)
ready = engine.tick()
assert len(ready) == 0
def test_execution_engine_approve_task():
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker")
dag = TrackDAG([t1])
engine = ExecutionEngine(dag, auto_queue=False)
# Should be able to approve even if auto_queue is False
engine.approve_task("T1")
assert t1.status == "in_progress"

View File

@@ -0,0 +1,81 @@
import pytest
from pathlib import Path
from datetime import datetime
import os
# Import the real models
from models import TrackState, Metadata, Ticket
# Import the persistence functions from project_manager
from project_manager import save_track_state, load_track_state
def test_track_state_persistence(tmp_path):
"""
Tests saving and loading a TrackState object to/from a TOML file.
1. Create a TrackState object with sample metadata, discussion, and tasks.
2. Call save_track_state('test_track', state, base_dir).
3. Verify that base_dir/conductor/tracks/test_track/state.toml exists.
4. Call load_track_state('test_track', base_dir) and verify it returns an identical TrackState object.
"""
base_dir = tmp_path
track_id = "test-track-999" # Metadata internal ID
track_folder_name = "test_track" # Folder name used in persistence
# 1. Create a TrackState object with sample data
metadata = Metadata(
id=track_id,
name="Test Track",
status="in_progress",
created_at=datetime(2023, 1, 1, 12, 0, 0),
updated_at=datetime(2023, 1, 2, 13, 0, 0)
)
discussion = [
{"role": "User", "content": "Hello", "ts": datetime(2023, 1, 1, 12, 0, 0)},
{"role": "AI", "content": "Hi there!", "ts": datetime(2023, 1, 1, 12, 0, 5)}
]
tasks = [
Ticket(id="task-1", description="First task", status="completed", assigned_to="worker-1"),
Ticket(id="task-2", description="Second task", status="todo", assigned_to="worker-2")
]
original_state = TrackState(
metadata=metadata,
discussion=discussion,
tasks=tasks
)
# 2. Call save_track_state('test_track', state, base_dir)
save_track_state(track_folder_name, original_state, base_dir)
# 3. Verify that base_dir/conductor/tracks/test_track/state.toml exists
state_file_path = base_dir / "conductor" / "tracks" / track_folder_name / "state.toml"
assert state_file_path.exists(), f"State file should exist at {state_file_path}"
# 4. Call load_track_state('test_track', base_dir) and verify it returns an identical TrackState object
loaded_state = load_track_state(track_folder_name, base_dir)
assert loaded_state is not None, "load_track_state returned None"
# Verify equality
assert loaded_state.metadata.id == original_state.metadata.id
assert loaded_state.metadata.name == original_state.metadata.name
assert loaded_state.metadata.status == original_state.metadata.status
assert loaded_state.metadata.created_at == original_state.metadata.created_at
assert loaded_state.metadata.updated_at == original_state.metadata.updated_at
assert len(loaded_state.tasks) == len(original_state.tasks)
for i in range(len(original_state.tasks)):
assert loaded_state.tasks[i].id == original_state.tasks[i].id
assert loaded_state.tasks[i].description == original_state.tasks[i].description
assert loaded_state.tasks[i].status == original_state.tasks[i].status
assert loaded_state.tasks[i].assigned_to == original_state.tasks[i].assigned_to
assert len(loaded_state.discussion) == len(original_state.discussion)
for i in range(len(original_state.discussion)):
assert loaded_state.discussion[i]["role"] == original_state.discussion[i]["role"]
assert loaded_state.discussion[i]["content"] == original_state.discussion[i]["content"]
assert loaded_state.discussion[i]["ts"] == original_state.discussion[i]["ts"]
# Final check: deep equality of dataclasses
assert loaded_state == original_state

0
verify_pm_changes.py Normal file
View File