diff --git a/src/mcp_client.py b/src/mcp_client.py index e314a968..f68f5224 100644 --- a/src/mcp_client.py +++ b/src/mcp_client.py @@ -114,8 +114,8 @@ def configure(file_items: list[dict[str, Any]], extra_base_dirs: list[str] | Non [C: tests/conftest.py:reset_ai_client, tests/test_arch_boundary_phase1.py:TestArchBoundaryPhase1.test_mcp_client_whitelist_enforcement, tests/test_mcp_client_beads.py:test_bd_mcp_tools, tests/test_py_struct_tools.py:test_mcp_dispatch_errors, tests/test_py_struct_tools.py:test_mcp_dispatch_integration] """ global _allowed_paths, _base_dirs, _primary_base_dir - _allowed_paths = set() - _base_dirs = set() + _allowed_paths = set() + _base_dirs = set() _primary_base_dir = Path(extra_base_dirs[0]).resolve() if extra_base_dirs else Path.cwd() for item in file_items: p = item.get("path") @@ -154,10 +154,8 @@ def _is_allowed(path: Path) -> bool: rp = path.resolve() # Blacklist check by resolved path - if rp == get_config_path().resolve(): - return False - if rp == get_credentials_path().resolve(): - return False + if rp == get_config_path().resolve(): return False + if rp == get_credentials_path().resolve(): return False name = path.name.lower() if name == "history.toml" or name.endswith("_history.toml"): @@ -209,10 +207,8 @@ def read_file(path: str) -> str: p, err = _resolve_and_check(path) if err or p is None: return err - if not p.exists(): - return f"ERROR: file not found: {path}" - if not p.is_file(): - return f"ERROR: not a file: {path}" + if not p.exists(): return f"ERROR: file not found: {path}" + if not p.is_file(): return f"ERROR: not a file: {path}" try: return p.read_text(encoding="utf-8") except Exception as e: @@ -223,10 +219,8 @@ def list_directory(path: str) -> str: p, err = _resolve_and_check(path) if err or p is None: return err - if not p.exists(): - return f"ERROR: path not found: {path}" - if not p.is_dir(): - return f"ERROR: not a directory: {path}" + if not p.exists(): return f"ERROR: path not found: {path}" + if not p.is_dir(): return f"ERROR: not a directory: {path}" try: entries = sorted(p.iterdir(), key=lambda e: (e.is_file(), e.name.lower())) lines = [f"Directory: {p}", ""] diff --git a/src/models.py b/src/models.py index 7d84db24..ea4e7e8c 100644 --- a/src/models.py +++ b/src/models.py @@ -21,7 +21,7 @@ Status Machine (Ticket): Serialization: All dataclasses provide to_dict() and from_dict() class methods for TOML/JSON persistence via project_manager.py. - +tomli_w Thread Safety: These dataclasses are NOT thread-safe. Callers must synchronize mutations if sharing instances across threads (e.g., during ConductorEngine execution). @@ -43,6 +43,7 @@ import json import os import sys import tomllib +import tomli_w from dataclasses import dataclass, field from pathlib import Path @@ -179,12 +180,12 @@ def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[ from src import thinking_parser entries = [] for raw in history_strings: - ts = "" + ts = "" rest = raw if rest.startswith("@"): nl = rest.find("\n") if nl != -1: - ts = rest[1:nl] + ts = rest[1:nl] rest = rest[nl + 1:] known = roles or ["User", "AI", "Vendor API", "System"] role_pat = re.compile(r"^(" + "|".join(re.escape(r) for r in known) + r"):", re.IGNORECASE) @@ -206,15 +207,15 @@ def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[ #region: Pydantic Models class GenerateRequest(BaseModel): - prompt: str + prompt: str auto_add_history: bool = True - temperature: float | None = None - top_p: float | None = None - max_tokens: int | None = None + temperature: float | None = None + top_p: float | None = None + max_tokens: int | None = None class ConfirmRequest(BaseModel): approved: bool - script: Optional[str] = None + script: Optional[str] = None #region: MMA Core @@ -238,45 +239,45 @@ class ThinkingSegment: @dataclass class Ticket: - id: str - description: str - target_symbols: List[str] = field(default_factory=list) + id: str + description: str + target_symbols: List[str] = field(default_factory=list) context_requirements: List[str] = field(default_factory=list) - depends_on: List[str] = field(default_factory=list) - status: str = "todo" - assigned_to: str = "unassigned" - priority: str = "medium" - target_file: Optional[str] = None - blocked_reason: Optional[str] = None - step_mode: bool = False - retry_count: int = 0 - manual_block: bool = False - model_override: Optional[str] = None - persona_id: Optional[str] = None + depends_on: List[str] = field(default_factory=list) + status: str = "todo" + assigned_to: str = "unassigned" + priority: str = "medium" + target_file: Optional[str] = None + blocked_reason: Optional[str] = None + step_mode: bool = False + retry_count: int = 0 + manual_block: bool = False + model_override: Optional[str] = None + persona_id: Optional[str] = None def mark_blocked(self, reason: str) -> None: """ [C: src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_mma_models.py:test_ticket_mark_blocked] """ - self.status = "blocked" + self.status = "blocked" self.blocked_reason = reason def mark_manual_block(self, reason: str) -> None: """ [C: tests/test_manual_block.py:test_clear_manual_block_method, tests/test_manual_block.py:test_mark_manual_block_method] """ - self.status = "blocked" + self.status = "blocked" self.blocked_reason = f"[MANUAL] {reason}" - self.manual_block = True + self.manual_block = True def clear_manual_block(self) -> None: """ [C: tests/test_manual_block.py:test_clear_manual_block_method] """ if self.manual_block: - self.status = "todo" + self.status = "todo" self.blocked_reason = None - self.manual_block = False + self.manual_block = False def mark_complete(self) -> None: """ @@ -295,21 +296,21 @@ class Ticket: [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return { - "id": self.id, - "description": self.description, - "status": self.status, - "assigned_to": self.assigned_to, - "priority": self.priority, - "target_file": self.target_file, - "target_symbols": self.target_symbols, + "id": self.id, + "description": self.description, + "status": self.status, + "assigned_to": self.assigned_to, + "priority": self.priority, + "target_file": self.target_file, + "target_symbols": self.target_symbols, "context_requirements": self.context_requirements, - "depends_on": self.depends_on, - "blocked_reason": self.blocked_reason, - "step_mode": self.step_mode, - "retry_count": self.retry_count, - "manual_block": self.manual_block, - "model_override": self.model_override, - "persona_id": self.persona_id, + "depends_on": self.depends_on, + "blocked_reason": self.blocked_reason, + "step_mode": self.step_mode, + "retry_count": self.retry_count, + "manual_block": self.manual_block, + "model_override": self.model_override, + "persona_id": self.persona_id, } @classmethod @@ -318,28 +319,28 @@ class Ticket: [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return cls( - id=data["id"], - description=data.get("description", ""), - status=data.get("status", "todo"), - assigned_to=data.get("assigned_to", "unassigned"), - priority=data.get("priority", "medium"), - target_file=data.get("target_file"), - target_symbols=data.get("target_symbols", []), - context_requirements=data.get("context_requirements", []), - depends_on=data.get("depends_on", []), - blocked_reason=data.get("blocked_reason"), - step_mode=data.get("step_mode", False), - retry_count=data.get("retry_count", 0), - manual_block=data.get("manual_block", False), - model_override=data.get("model_override"), - persona_id=data.get("persona_id"), + id = data["id"], + description = data.get("description", ""), + status = data.get("status", "todo"), + assigned_to = data.get("assigned_to", "unassigned"), + priority = data.get("priority", "medium"), + target_file = data.get("target_file"), + target_symbols = data.get("target_symbols", []), + context_requirements = data.get("context_requirements", []), + depends_on = data.get("depends_on", []), + blocked_reason = data.get("blocked_reason"), + step_mode = data.get("step_mode", False), + retry_count = data.get("retry_count", 0), + manual_block = data.get("manual_block", False), + model_override = data.get("model_override"), + persona_id = data.get("persona_id"), ) @dataclass class Track: - id: str + id: str description: str - tickets: List[Ticket] = field(default_factory=list) + tickets: List[Ticket] = field(default_factory=list) def get_executable_tickets(self) -> List[Ticket]: """ @@ -353,9 +354,9 @@ class Track: [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return { - "id": self.id, + "id": self.id, "description": self.description, - "tickets": [t.to_dict() for t in self.tickets], + "tickets": [t.to_dict() for t in self.tickets], } @classmethod @@ -364,24 +365,24 @@ class Track: [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return cls( - id=data["id"], - description=data.get("description", ""), - tickets=[Ticket.from_dict(t) for t in data.get("tickets", [])], + id = data["id"], + description = data.get("description", ""), + tickets = [Ticket.from_dict(t) for t in data.get("tickets", [])], ) @dataclass class WorkerContext: - ticket_id: str - model_name: str - messages: List[Dict[str, Any]] = field(default_factory=list) + ticket_id: str + model_name: str + messages: List[Dict[str, Any]] = field(default_factory=list) tool_preset: Optional[str] = None - persona_id: Optional[str] = None + persona_id: Optional[str] = None @dataclass class Metadata: - id: str - name: str - status: Optional[str] = None + id: str + name: str + status: Optional[str] = None created_at: Optional[datetime.datetime] = None updated_at: Optional[datetime.datetime] = None @@ -390,9 +391,9 @@ class Metadata: [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return { - "id": self.id, - "name": self.name, - "status": self.status, + "id": self.id, + "name": self.name, + "status": self.status, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, } @@ -415,20 +416,20 @@ class Metadata: except ValueError: updated = None return cls( - id=data["id"], - name=data.get("name", ""), - status=data.get("status"), - created_at=created, - updated_at=updated, + id = data["id"], + name = data.get("name", ""), + status = data.get("status"), + created_at = created, + updated_at = updated, ) #region: State & Config @dataclass class TrackState: - metadata: Metadata - discussion: List[str] = field(default_factory=list) - tasks: List[Ticket] = field(default_factory=list) + metadata: Metadata + discussion: List[str] = field(default_factory=list) + tasks: List[Ticket] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: """ @@ -444,9 +445,9 @@ class TrackState: else: serialized_discussion.append(item) return { - "metadata": self.metadata.to_dict(), + "metadata": self.metadata.to_dict(), "discussion": serialized_discussion, - "tasks": [t.to_dict() for t in self.tasks], + "tasks": [t.to_dict() for t in self.tasks], } @classmethod @@ -454,12 +455,12 @@ class TrackState: """ [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ - discussion = data.get("discussion", []) + discussion = data.get("discussion", []) parsed_discussion = [] for item in discussion: if isinstance(item, dict): new_item = dict(item) - ts = new_item.get("ts") + ts = new_item.get("ts") if isinstance(ts, str): try: new_item["ts"] = datetime.datetime.fromisoformat(ts) @@ -469,23 +470,23 @@ class TrackState: else: parsed_discussion.append(item) return cls( - metadata=Metadata.from_dict(data["metadata"]), - discussion=parsed_discussion, - tasks=[Ticket.from_dict(t) for t in data.get("tasks", [])], + metadata = Metadata.from_dict(data["metadata"]), + discussion = parsed_discussion, + tasks = [Ticket.from_dict(t) for t in data.get("tasks", [])], ) @dataclass class FileItem: - path: str - auto_aggregate: bool = True - force_full: bool = False - view_mode: str = 'full' - selected: bool = False - ast_signatures: bool = False + path: str + auto_aggregate: bool = True + force_full: bool = False + view_mode: str = 'full' + selected: bool = False + ast_signatures: bool = False ast_definitions: bool = False - ast_mask: dict[str, str] = field(default_factory=dict) - custom_slices: list[dict] = field(default_factory=list) - injected_at: Optional[float] = None + ast_mask: dict[str, str] = field(default_factory=dict) + custom_slices: list[dict] = field(default_factory=list) + injected_at: Optional[float] = None def __post_init__(self): if self.custom_slices: @@ -493,7 +494,7 @@ class FileItem: for slc in self.custom_slices: if isinstance(slc, dict): new_slc = slc.copy() - if "tag" not in new_slc: new_slc["tag"] = None + if "tag" not in new_slc: new_slc["tag"] = None if "comment" not in new_slc: new_slc["comment"] = None normalized.append(new_slc) else: @@ -505,15 +506,15 @@ class FileItem: [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return { - "path": self.path, - "auto_aggregate": self.auto_aggregate, - "force_full": self.force_full, - "view_mode": self.view_mode, - "ast_signatures": self.ast_signatures, + "path": self.path, + "auto_aggregate": self.auto_aggregate, + "force_full": self.force_full, + "view_mode": self.view_mode, + "ast_signatures": self.ast_signatures, "ast_definitions": self.ast_definitions, - "ast_mask": self.ast_mask, - "custom_slices": self.custom_slices, - "injected_at": self.injected_at, + "ast_mask": self.ast_mask, + "custom_slices": self.custom_slices, + "injected_at": self.injected_at, } @classmethod @@ -522,20 +523,20 @@ class FileItem: [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return cls( - path=data["path"], - auto_aggregate=data.get("auto_aggregate", True), - force_full=data.get("force_full", False), - view_mode=data.get("view_mode", 'full'), - ast_signatures=data.get("ast_signatures", False), - ast_definitions=data.get("ast_definitions", False), - ast_mask=data.get("ast_mask", {}), - custom_slices=data.get("custom_slices", []), - injected_at=data.get("injected_at"), + path = data["path"], + auto_aggregate = data.get("auto_aggregate", True), + force_full = data.get("force_full", False), + view_mode = data.get("view_mode", 'full'), + ast_signatures = data.get("ast_signatures", False), + ast_definitions = data.get("ast_definitions", False), + ast_mask = data.get("ast_mask", {}), + custom_slices = data.get("custom_slices", []), + injected_at = data.get("injected_at"), ) @dataclass class Preset: - name: str + name: str system_prompt: str def to_dict(self) -> Dict[str, Any]: @@ -555,9 +556,9 @@ class Preset: @dataclass class Tool: - name: str - approval: str = 'auto' - weight: int = 3 + name: str + approval: str = 'auto' + weight: int = 3 parameter_bias: Dict[str, str] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: @@ -565,9 +566,9 @@ class Tool: [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return { - "name": self.name, - "approval": self.approval, - "weight": self.weight, + "name": self.name, + "approval": self.approval, + "weight": self.weight, "parameter_bias": self.parameter_bias, } @@ -585,7 +586,7 @@ class Tool: @dataclass class ToolPreset: - name: str + name: str categories: Dict[str, List[Union[Tool, Any]]] def to_dict(self) -> Dict[str, Any]: @@ -602,7 +603,7 @@ class ToolPreset: """ [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ - raw_categories = data.get("categories", {}) + raw_categories = data.get("categories", {}) parsed_categories = {} for cat, tools in raw_categories.items(): parsed_categories[cat] = [Tool.from_dict(t) if isinstance(t, dict) else t for t in tools] @@ -610,8 +611,8 @@ class ToolPreset: @dataclass class BiasProfile: - name: str - tool_weights: Dict[str, int] = field(default_factory=dict) + name: str + tool_weights: Dict[str, int] = field(default_factory=dict) category_multipliers: Dict[str, float] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: @@ -619,8 +620,8 @@ class BiasProfile: [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return { - "name": self.name, - "tool_weights": self.tool_weights, + "name": self.name, + "tool_weights": self.tool_weights, "category_multipliers": self.category_multipliers, } @@ -630,17 +631,17 @@ class BiasProfile: [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return cls( - name=data["name"], - tool_weights=data.get("tool_weights", {}), - category_multipliers=data.get("category_multipliers", {}), - ) + name = data["name"], + tool_weights = data.get("tool_weights", {}), + category_multipliers = data.get("category_multipliers", {}), + ) #region: UI/Editor @dataclass class TextEditorConfig: - name: str - path: str + name: str + path: str diff_args: List[str] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: @@ -648,8 +649,8 @@ class TextEditorConfig: [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return { - "name": self.name, - "path": self.path, + "name": self.name, + "path": self.path, "diff_args": self.diff_args, } @@ -659,14 +660,14 @@ class TextEditorConfig: [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return cls( - name=data["name"], - path=data["path"], - diff_args=data.get("diff_args", []), + name = data["name"], + path = data["path"], + diff_args = data.get("diff_args", []), ) @dataclass class ExternalEditorConfig: - editors: Dict[str, TextEditorConfig] = field(default_factory=dict) + editors: Dict[str, TextEditorConfig] = field(default_factory=dict) default_editor: Optional[str] = None def get_default(self) -> Optional[TextEditorConfig]: @@ -684,7 +685,7 @@ class ExternalEditorConfig: [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return { - "editors": {k: v.to_dict() for k, v in self.editors.items()}, + "editors": {k: v.to_dict() for k, v in self.editors.items()}, "default_editor": self.default_editor, } @@ -695,10 +696,8 @@ class ExternalEditorConfig: """ editors = {} for name, ed_data in data.get("editors", {}).items(): - if isinstance(ed_data, dict): - editors[name] = TextEditorConfig.from_dict(ed_data) - elif isinstance(ed_data, str): - editors[name] = TextEditorConfig(name=name, path=ed_data) + if isinstance(ed_data, dict): editors[name] = TextEditorConfig.from_dict(ed_data) + elif isinstance(ed_data, str): editors[name] = TextEditorConfig(name=name, path=ed_data) return cls(editors=editors, default_editor=data.get("default_editor")) #region: Persona @@ -751,14 +750,10 @@ class Persona: else: processed.append(m) res["preferred_models"] = processed - if self.tool_preset is not None: - res["tool_preset"] = self.tool_preset - if self.bias_profile is not None: - res["bias_profile"] = self.bias_profile - if self.context_preset is not None: - res["context_preset"] = self.context_preset - if self.aggregation_strategy is not None: - res["aggregation_strategy"] = self.aggregation_strategy + if self.tool_preset is not None: res["tool_preset"] = self.tool_preset + if self.bias_profile is not None: res["bias_profile"] = self.bias_profile + if self.context_preset is not None: res["context_preset"] = self.context_preset + if self.aggregation_strategy is not None: res["aggregation_strategy"] = self.aggregation_strategy return res @classmethod @@ -785,21 +780,21 @@ class Persona: if k not in parsed_models[0] or parsed_models[0][k] is None: parsed_models[0][k] = v return cls( - name=name, - preferred_models=parsed_models, - system_prompt=data.get("system_prompt", ""), - tool_preset=data.get("tool_preset"), - bias_profile=data.get("bias_profile"), - context_preset=data.get("context_preset"), - aggregation_strategy=data.get("aggregation_strategy"), + name = name, + preferred_models = parsed_models, + system_prompt = data.get("system_prompt", ""), + tool_preset = data.get("tool_preset"), + bias_profile = data.get("bias_profile"), + context_preset = data.get("context_preset"), + aggregation_strategy = data.get("aggregation_strategy"), ) #region: Workspace @dataclass class WorkspaceProfile: - name: str - ini_content: str + name: str + ini_content: str show_windows: Dict[str, bool] panel_states: Dict[str, Any] @@ -808,7 +803,7 @@ class WorkspaceProfile: [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return { - "ini_content": self.ini_content, + "ini_content": self.ini_content, "show_windows": self.show_windows, "panel_states": self.panel_states, } @@ -819,19 +814,19 @@ class WorkspaceProfile: [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return cls( - name=name, - ini_content=data.get("ini_content", ""), - show_windows=data.get("show_windows", {}), - panel_states=data.get("panel_states", {}), + name = name, + ini_content = data.get("ini_content", ""), + show_windows = data.get("show_windows", {}), + panel_states = data.get("panel_states", {}), ) @dataclass class ContextFileEntry: - path: str - view_mode: str = "summary" - custom_slices: list = field(default_factory=list) - ast_mask: dict = field(default_factory=dict) - ast_signatures: bool = False + path: str + view_mode: str = "summary" + custom_slices: list = field(default_factory=list) + ast_mask: dict = field(default_factory=dict) + ast_signatures: bool = False ast_definitions: bool = False def to_dict(self) -> dict[str, Any]: @@ -846,19 +841,19 @@ class ContextFileEntry: [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return cls( - path=data.get("path", ""), - view_mode=data.get("view_mode", "summary"), - custom_slices=data.get("custom_slices", []), - ast_mask=data.get("ast_mask", {}), - ast_signatures=data.get("ast_signatures", False), - ast_definitions=data.get("ast_definitions", False), + path = data.get("path", ""), + view_mode = data.get("view_mode", "summary"), + custom_slices = data.get("custom_slices", []), + ast_mask = data.get("ast_mask", {}), + ast_signatures = data.get("ast_signatures", False), + ast_definitions = data.get("ast_definitions", False), ) @dataclass class NamedViewPreset: - name: str - view_mode: str - ast_mask: dict = field(default_factory=dict) + name: str + view_mode: str + ast_mask: dict = field(default_factory=dict) custom_slices: list = field(default_factory=list) def to_dict(self) -> dict[str, Any]: @@ -873,16 +868,16 @@ class NamedViewPreset: [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return cls( - name=data.get("name", ""), - view_mode=data.get("view_mode", "summary"), - ast_mask=data.get("ast_mask", {}), - custom_slices=data.get("custom_slices", []), + name = data.get("name", ""), + view_mode = data.get("view_mode", "summary"), + ast_mask = data.get("ast_mask", {}), + custom_slices = data.get("custom_slices", []), ) @dataclass class ContextPreset: - name: str - files: list[ContextFileEntry] = field(default_factory=list) + name: str + files: list[ContextFileEntry] = field(default_factory=list) screenshots: list[str] = field(default_factory=list) description: str = "" @@ -891,7 +886,7 @@ class ContextPreset: [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ return { - "files": [f.to_dict() for f in self.files], + "files": [f.to_dict() for f in self.files], "screenshots": self.screenshots, "description": self.description, } @@ -903,21 +898,21 @@ class ContextPreset: """ files_data = data.get("files", []) return cls( - name=name, - files=[ContextFileEntry.from_dict(f) if isinstance(f, dict) else ContextFileEntry(path=str(f)) for f in files_data], - screenshots=data.get("screenshots", []), - description=data.get("description", ""), + name = name, + files = [ContextFileEntry.from_dict(f) if isinstance(f, dict) else ContextFileEntry(path=str(f)) for f in files_data], + screenshots = data.get("screenshots", []), + description = data.get("description", ""), ) #region: MCP Config @dataclass class MCPServerConfig: - name: str - command: Optional[str] = None - args: List[str] = field(default_factory=list) - url: Optional[str] = None - auto_start: bool = False + name: str + command: Optional[str] = None + args: List[str] = field(default_factory=list) + url: Optional[str] = None + auto_start: bool = False def to_dict(self) -> Dict[str, Any]: """ diff --git a/src/multi_agent_conductor.py b/src/multi_agent_conductor.py index 51a75e01..cb4f8d72 100644 --- a/src/multi_agent_conductor.py +++ b/src/multi_agent_conductor.py @@ -62,11 +62,9 @@ class WorkerPool: def spawn(self, ticket_id: str, target: Callable, args: tuple) -> Optional[threading.Thread]: """ - - - Spawns a new worker thread if the pool is not full. - Returns the thread object or None if full. - [C: tests/test_parallel_execution.py:test_worker_pool_completion_cleanup, tests/test_parallel_execution.py:test_worker_pool_limit, tests/test_parallel_execution.py:test_worker_pool_tracking] + Spawns a new worker thread if the pool is not full. + Returns the thread object or None if full. + [C: tests/test_parallel_execution.py:test_worker_pool_completion_cleanup, tests/test_parallel_execution.py:test_worker_pool_limit, tests/test_parallel_execution.py:test_worker_pool_tracking] """ with self._lock: if len(self._active) >= self.max_workers: @@ -88,7 +86,7 @@ class WorkerPool: def join_all(self, timeout: float = None) -> None: """ - [C: tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_parallel_execution.py:test_worker_pool_limit, tests/test_parallel_execution.py:test_worker_pool_tracking] + [C: tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_parallel_execution.py:test_worker_pool_limit, tests/test_parallel_execution.py:test_worker_pool_tracking] """ with self._lock: threads = list(self._active.values()) @@ -99,22 +97,20 @@ class WorkerPool: def get_active_count(self) -> int: """ - [C: tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_parallel_execution.py:test_worker_pool_completion_cleanup, tests/test_parallel_execution.py:test_worker_pool_limit] + [C: tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_parallel_execution.py:test_worker_pool_completion_cleanup, tests/test_parallel_execution.py:test_worker_pool_limit] """ with self._lock: return len(self._active) def is_full(self) -> bool: """ - [C: tests/test_parallel_execution.py:test_worker_pool_limit] + [C: tests/test_parallel_execution.py:test_worker_pool_limit] """ return self.get_active_count() >= self.max_workers 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, auto_queue: bool = False) -> None: @@ -123,74 +119,69 @@ class ConductorEngine: self.tier_usage = { "Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview", "tool_preset": None, "persona": None}, "Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview", "tool_preset": None, "persona": None}, - "Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite", "tool_preset": None, "persona": None}, - "Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite", "tool_preset": None, "persona": None}, + "Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite", "tool_preset": None, "persona": None}, + "Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite", "tool_preset": None, "persona": None}, } - self.dag = TrackDAG(self.track.tickets) + self.dag = TrackDAG(self.track.tickets) self.engine = ExecutionEngine(self.dag, auto_queue=auto_queue) # Load MMA config try: - config = models.load_config() - mma_cfg = config.get("mma", {}) + config = models.load_config() + mma_cfg = config.get("mma", {}) max_workers = mma_cfg.get("max_workers", 4) except Exception: max_workers = 4 - self.pool = WorkerPool(max_workers=max_workers) - self._workers_lock = threading.Lock() + self.pool = WorkerPool(max_workers=max_workers) + self._workers_lock = threading.Lock() self._active_workers: dict[str, threading.Thread] = {} - self._abort_events: dict[str, threading.Event] = {} - self._pause_event: threading.Event = threading.Event() + self._abort_events: dict[str, threading.Event] = {} + self._pause_event: threading.Event = threading.Event() self._tier_usage_lock = threading.Lock() - self._dirty: bool = True + self._dirty: bool = True def update_usage(self, tier: str, input_tokens: int, output_tokens: int) -> None: """Updates token usage for a specific tier.""" with self._tier_usage_lock: if tier in self.tier_usage: - self.tier_usage[tier]["input"] += input_tokens + self.tier_usage[tier]["input"] += input_tokens self.tier_usage[tier]["output"] += output_tokens def pause(self) -> None: """ - - Pauses the pipeline execution. - [C: tests/test_pipeline_pause.py:test_pause_method, tests/test_pipeline_pause.py:test_resume_method] + Pauses the pipeline execution. + [C: tests/test_pipeline_pause.py:test_pause_method, tests/test_pipeline_pause.py:test_resume_method] """ self._pause_event.set() def resume(self) -> None: """ - - Resumes the pipeline execution. - [C: tests/test_pipeline_pause.py:test_resume_method] + Resumes the pipeline execution. + [C: tests/test_pipeline_pause.py:test_resume_method] """ self._pause_event.clear() def approve_task(self, task_id: str) -> None: """ - - Manually transition todo to in_progress and mark engine dirty. - [C: tests/test_execution_engine.py:test_execution_engine_approve_task, tests/test_execution_engine.py:test_execution_engine_step_mode] + Manually transition todo to in_progress and mark engine dirty. + [C: tests/test_execution_engine.py:test_execution_engine_approve_task, tests/test_execution_engine.py:test_execution_engine_step_mode] """ self.engine.approve_task(task_id) self._dirty = True def update_task_status(self, task_id: str, status: str) -> None: """ - - Force-update ticket status and mark engine dirty. - [C: tests/test_arch_boundary_phase3.py:TestArchBoundaryPhase3.test_manual_unblock_restores_todo, tests/test_execution_engine.py:test_execution_engine_auto_queue, tests/test_execution_engine.py:test_execution_engine_basic_flow, tests/test_execution_engine.py:test_execution_engine_status_persistence, tests/test_execution_engine.py:test_execution_engine_update_nonexistent_task] + Force-update ticket status and mark engine dirty. + [C: tests/test_arch_boundary_phase3.py:TestArchBoundaryPhase3.test_manual_unblock_restores_todo, tests/test_execution_engine.py:test_execution_engine_auto_queue, tests/test_execution_engine.py:test_execution_engine_basic_flow, tests/test_execution_engine.py:test_execution_engine_status_persistence, tests/test_execution_engine.py:test_execution_engine_update_nonexistent_task] """ self.engine.update_task_status(task_id, status) self._dirty = True def kill_worker(self, ticket_id: str) -> None: """ - - Sets the abort event for a worker and attempts to join its thread. - [C: tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread] + Sets the abort event for a worker and attempts to join its thread. + [C: tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread] """ if ticket_id in self._abort_events: print(f"[MMA] Setting abort event for {ticket_id}") diff --git a/src/orchestrator_pm.py b/src/orchestrator_pm.py index 9f378c86..8d9a1253 100644 --- a/src/orchestrator_pm.py +++ b/src/orchestrator_pm.py @@ -12,32 +12,27 @@ from src import summarize def get_track_history_summary() -> str: """ - - - Scans conductor/archive/ and conductor/tracks/ to build a summary of past work. - [C: tests/test_orchestrator_pm_history.py:TestOrchestratorPMHistory.test_get_track_history_summary, tests/test_orchestrator_pm_history.py:TestOrchestratorPMHistory.test_get_track_history_summary_missing_files] + Scans conductor/archive/ and conductor/tracks/ to build a summary of past work. + [C: tests/test_orchestrator_pm_history.py:TestOrchestratorPMHistory.test_get_track_history_summary, tests/test_orchestrator_pm_history.py:TestOrchestratorPMHistory.test_get_track_history_summary_missing_files] """ summary_parts = [] - archive_path = paths.get_archive_dir() - tracks_path = paths.get_tracks_dir() + archive_path = paths.get_archive_dir() + tracks_path = paths.get_tracks_dir() 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())) + 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 + 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." + 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) + meta = json.load(f) + title = meta.get("title", title) status = meta.get("status", status) except Exception: pass @@ -60,11 +55,9 @@ def get_track_history_summary() -> str: def generate_tracks(user_request: str, project_config: dict[str, Any], file_items: list[dict[str, Any]], history_summary: Optional[str] = None) -> list[dict[str, Any]]: """ - - - Tier 1 (Strategic PM) call. - Analyzes the project state and user request to generate a list of Tracks. - [C: tests/test_orchestration_logic.py:test_generate_tracks, tests/test_orchestrator_pm.py:TestOrchestratorPM.test_generate_tracks_malformed_json, tests/test_orchestrator_pm.py:TestOrchestratorPM.test_generate_tracks_markdown_wrapped, tests/test_orchestrator_pm.py:TestOrchestratorPM.test_generate_tracks_success, tests/test_orchestrator_pm_history.py:TestOrchestratorPMHistory.test_generate_tracks_with_history] + Tier 1 (Strategic PM) call. + Analyzes the project state and user request to generate a list of Tracks. + [C: tests/test_orchestration_logic.py:test_generate_tracks, tests/test_orchestrator_pm.py:TestOrchestratorPM.test_generate_tracks_malformed_json, tests/test_orchestrator_pm.py:TestOrchestratorPM.test_generate_tracks_markdown_wrapped, tests/test_orchestrator_pm.py:TestOrchestratorPM.test_generate_tracks_success, tests/test_orchestrator_pm_history.py:TestOrchestratorPMHistory.test_generate_tracks_with_history] """ # 1. Build Repository Map (Summary View) repo_map = summarize.build_summary_markdown(file_items) @@ -131,4 +124,4 @@ if __name__ == "__main__": print("Testing Tier 1 Track Generation...") 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)) \ No newline at end of file + print(json.dumps(tracks, indent=2)) diff --git a/src/paths.py b/src/paths.py index 3e6190ca..ad9c8a5d 100644 --- a/src/paths.py +++ b/src/paths.py @@ -51,79 +51,79 @@ _RESOLVED: dict[str, Path] = {} def get_config_path() -> Path: """ - [C: tests/test_paths.py:test_default_paths] + [C: tests/test_paths.py:test_default_paths] """ root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_CONFIG", root_dir / "config.toml")) def get_global_presets_path() -> Path: """ - [C: src/presets.py:PresetManager.__init__, src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.get_preset_scope] + [C: src/presets.py:PresetManager.__init__, src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.get_preset_scope] """ root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_GLOBAL_PRESETS", root_dir / "presets.toml")) def get_project_presets_path(project_root: Path) -> Path: """ - [C: src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.get_preset_scope, src/presets.py:PresetManager.project_path] + [C: src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.get_preset_scope, src/presets.py:PresetManager.project_path] """ return project_root / "project_presets.toml" def get_global_tool_presets_path() -> Path: """ - [C: src/tool_presets.py:ToolPresetManager._get_path, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets] + [C: src/tool_presets.py:ToolPresetManager._get_path, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets] """ root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_GLOBAL_TOOL_PRESETS", root_dir / "tool_presets.toml")) def get_project_tool_presets_path(project_root: Path) -> Path: """ - [C: src/tool_presets.py:ToolPresetManager._get_path, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets] + [C: src/tool_presets.py:ToolPresetManager._get_path, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets] """ return project_root / "project_tool_presets.toml" def get_global_personas_path() -> Path: """ - [C: src/personas.py:PersonaManager._get_path, src/personas.py:PersonaManager.get_persona_scope, src/personas.py:PersonaManager.load_all] + [C: src/personas.py:PersonaManager._get_path, src/personas.py:PersonaManager.get_persona_scope, src/personas.py:PersonaManager.load_all] """ root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_GLOBAL_PERSONAS", root_dir / "personas.toml")) def get_project_personas_path(project_root: Path) -> Path: """ - [C: src/personas.py:PersonaManager._get_path, src/personas.py:PersonaManager.get_persona_scope, src/personas.py:PersonaManager.load_all] + [C: src/personas.py:PersonaManager._get_path, src/personas.py:PersonaManager.get_persona_scope, src/personas.py:PersonaManager.load_all] """ return project_root / "project_personas.toml" def get_global_themes_path() -> Path: """ - [C: src/theme_2.py:load_themes_from_disk] + [C: src/theme_2.py:load_themes_from_disk] """ root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_GLOBAL_THEMES", root_dir / "themes")) def get_project_themes_path(project_root: Path) -> Path: """ - [C: src/theme_2.py:load_themes_from_disk] + [C: src/theme_2.py:load_themes_from_disk] """ return project_root / "project_themes.toml" def get_global_workspace_profiles_path() -> Path: """ - [C: src/workspace_manager.py:WorkspaceManager._get_path, src/workspace_manager.py:WorkspaceManager.load_all_profiles] + [C: src/workspace_manager.py:WorkspaceManager._get_path, src/workspace_manager.py:WorkspaceManager.load_all_profiles] """ root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_GLOBAL_WORKSPACE_PROFILES", root_dir / "workspace_profiles.toml")) def get_project_workspace_profiles_path(project_root: Path) -> Path: """ - [C: src/workspace_manager.py:WorkspaceManager._get_path, src/workspace_manager.py:WorkspaceManager.load_all_profiles] + [C: src/workspace_manager.py:WorkspaceManager._get_path, src/workspace_manager.py:WorkspaceManager.load_all_profiles] """ return project_root / ".ai" / "workspace_profiles.toml" def get_credentials_path() -> Path: """ - [C: src/mcp_client.py:_is_allowed] + [C: src/mcp_client.py:_is_allowed] """ root_dir = Path(__file__).resolve().parent.parent return Path(os.environ.get("SLOP_CREDENTIALS", str(root_dir / "credentials.toml"))) @@ -165,21 +165,20 @@ def _get_project_conductor_dir_from_toml(project_root: Path) -> Optional[Path]: def get_conductor_dir(project_path: Optional[str] = None) -> Path: """ - [C: tests/test_paths.py:test_conductor_dir_project_relative, tests/test_project_paths.py:test_get_conductor_dir_default, tests/test_project_paths.py:test_get_conductor_dir_project_specific_with_toml] + [C: tests/test_paths.py:test_conductor_dir_project_relative, tests/test_project_paths.py:test_get_conductor_dir_default, tests/test_project_paths.py:test_get_conductor_dir_project_specific_with_toml] """ if not project_path: # Fallback for legacy/tests, but we should avoid this return Path('conductor').resolve() project_root = Path(project_path).resolve() - p = _get_project_conductor_dir_from_toml(project_root) + p = _get_project_conductor_dir_from_toml(project_root) if p: return p - return (project_root / "conductor").resolve() def get_logs_dir() -> Path: """ - [C: src/session_logger.py:close_session, src/session_logger.py:open_session, tests/test_paths.py:test_config_overrides, tests/test_paths.py:test_default_paths, tests/test_paths.py:test_env_var_overrides, tests/test_paths.py:test_precedence] + [C: src/session_logger.py:close_session, src/session_logger.py:open_session, tests/test_paths.py:test_config_overrides, tests/test_paths.py:test_default_paths, tests/test_paths.py:test_env_var_overrides, tests/test_paths.py:test_precedence] """ if "logs_dir" not in _RESOLVED: _RESOLVED["logs_dir"] = _resolve_path("SLOP_LOGS_DIR", "logs_dir", "logs/sessions") @@ -187,7 +186,7 @@ def get_logs_dir() -> Path: def get_scripts_dir() -> Path: """ - [C: src/session_logger.py:log_tool_call, src/session_logger.py:open_session, tests/test_paths.py:test_config_overrides, tests/test_paths.py:test_default_paths] + [C: src/session_logger.py:log_tool_call, src/session_logger.py:open_session, tests/test_paths.py:test_config_overrides, tests/test_paths.py:test_default_paths] """ if "scripts_dir" not in _RESOLVED: _RESOLVED["scripts_dir"] = _resolve_path("SLOP_SCRIPTS_DIR", "scripts_dir", "scripts/generated") @@ -195,13 +194,13 @@ def get_scripts_dir() -> Path: def get_tracks_dir(project_path: Optional[str] = None) -> Path: """ - [C: src/project_manager.py:get_all_tracks, tests/test_paths.py:test_conductor_dir_project_relative] + [C: src/project_manager.py:get_all_tracks, tests/test_paths.py:test_conductor_dir_project_relative] """ return get_conductor_dir(project_path) / "tracks" def get_track_state_dir(track_id: str, project_path: Optional[str] = None) -> Path: """ - [C: src/project_manager.py:load_track_state, src/project_manager.py:save_track_state, tests/test_paths.py:test_conductor_dir_project_relative] + [C: src/project_manager.py:load_track_state, src/project_manager.py:save_track_state, tests/test_paths.py:test_conductor_dir_project_relative] """ return get_tracks_dir(project_path) / track_id @@ -235,9 +234,8 @@ def get_full_path_info() -> dict[str, dict[str, Any]]: def reset_resolved() -> None: """ - - For testing only - clear cached resolutions. - [C: tests/conftest.py:reset_paths, tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_gui_phase3.py:test_conductor_setup_scan, tests/test_paths.py:reset_paths, tests/test_project_paths.py:test_get_all_tracks_project_specific, tests/test_project_paths.py:test_get_conductor_dir_default, tests/test_project_paths.py:test_get_conductor_dir_project_specific_with_toml] + For testing only - clear cached resolutions. + [C: tests/conftest.py:reset_paths, tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_gui_phase3.py:test_conductor_setup_scan, tests/test_paths.py:reset_paths, tests/test_project_paths.py:test_get_all_tracks_project_specific, tests/test_project_paths.py:test_get_conductor_dir_default, tests/test_project_paths.py:test_get_conductor_dir_project_specific_with_toml] """ _RESOLVED.clear() \ No newline at end of file diff --git a/src/performance_monitor.py b/src/performance_monitor.py index 40b0bdd3..0520b4e9 100644 --- a/src/performance_monitor.py +++ b/src/performance_monitor.py @@ -70,7 +70,7 @@ class PerformanceScope: """Helper class for PerformanceMonitor.scope() context manager.""" def __init__(self, monitor: PerformanceMonitor, name: str) -> None: self.monitor = monitor - self.name = name + self.name = name def __enter__(self) -> PerformanceScope: self.monitor.start_component(self.name) return self @@ -89,35 +89,33 @@ def get_monitor() -> PerformanceMonitor: class PerformanceMonitor: """ - - - Tracks application performance metrics like FPS, frame time, and CPU usage. - Supports thread-safe tracking for individual components with efficient moving averages. + Tracks application performance metrics like FPS, frame time, and CPU usage. + Supports thread-safe tracking for individual components with efficient moving averages. """ def __init__(self, history_size: int = 300) -> None: self.enabled: bool = False - self.history_size = history_size - self._lock = threading.Lock() + self.history_size = history_size + self._lock = threading.Lock() - self._start_time: Optional[float] = None + self._start_time: Optional[float] = None self._last_frame_start_time: float = 0.0 - self._last_frame_time: float = 0.0 - self._fps: float = 0.0 - self._last_calculated_fps: float = 0.0 - self._frame_count: int = 0 - self._fps_timer: float = 0.0 - self._cpu_percent: float = 0.0 - self._input_lag_ms: float = 0.0 + self._last_frame_time: float = 0.0 + self._fps: float = 0.0 + self._last_calculated_fps: float = 0.0 + self._frame_count: int = 0 + self._fps_timer: float = 0.0 + self._cpu_percent: float = 0.0 + self._input_lag_ms: float = 0.0 - self._component_starts: dict[str, float] = {} + self._component_starts: dict[str, float] = {} self._component_timings: dict[str, float] = {} - self._component_counts: dict[str, int] = {} - self._component_max: dict[str, float] = {} - self._component_min: dict[str, float] = {} + self._component_counts: dict[str, int] = {} + self._component_max: dict[str, float] = {} + self._component_min: dict[str, float] = {} # Rolling history and running sums for O(1) average calculation # deques are thread-safe for appends and pops. - self._history: Dict[str, deque[float]] = {} + self._history: Dict[str, deque[float]] = {} self._history_sums: Dict[str, float] = {} # For slowing down graph updates @@ -143,7 +141,7 @@ class PerformanceMonitor: """Thread-safe O(1) history update.""" with self._lock: if key not in self._history: - self._history[key] = deque(maxlen=self.history_size) + self._history[key] = deque(maxlen=self.history_size) self._history_sums[key] = 0.0 h = self._history[key] @@ -158,13 +156,12 @@ class PerformanceMonitor: """Thread-safe O(1) average retrieval.""" with self._lock: h = self._history.get(key) - if not h or len(h) == 0: - return 0.0 + if not h or len(h) == 0: return 0.0 return self._history_sums[key] / len(h) def start_frame(self) -> None: """ - [C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing] + [C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing] """ now = time.perf_counter() with self._lock: @@ -173,24 +170,24 @@ class PerformanceMonitor: if dt > 0: self._fps = 1.0 / dt self._last_frame_start_time = now - self._start_time = now - self._frame_count += 1 + self._start_time = now + self._frame_count += 1 def end_frame(self) -> None: """ - [C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing] + [C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing] """ if self._start_time is None: return - now = time.perf_counter() - elapsed = now - self._start_time + now = time.perf_counter() + elapsed = now - self._start_time frame_time_ms = elapsed * 1000 with self._lock: self._last_frame_time = frame_time_ms - cpu = self._cpu_percent + cpu = self._cpu_percent ilag = self._input_lag_ms - fps = self._fps + fps = self._fps # Slow down history sampling for core metrics if now - self._last_sample_time >= self._sample_interval: @@ -205,11 +202,11 @@ class PerformanceMonitor: with self._lock: self._last_calculated_fps = self._frame_count / self._fps_timer self._frame_count = 0 - self._fps_timer = 0.0 + self._fps_timer = 0.0 def start_component(self, name: str) -> None: """ - [C: tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics] + [C: tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics] """ if not self.enabled: return now = time.perf_counter() @@ -218,7 +215,7 @@ class PerformanceMonitor: def end_component(self, name: str) -> None: """ - [C: tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics] + [C: tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics] """ if not self.enabled: return now = time.perf_counter() @@ -228,55 +225,51 @@ class PerformanceMonitor: elapsed = (now - start) * 1000 with self._lock: self._component_timings[name] = elapsed - self._component_counts[name] = self._component_counts.get(name, 0) + 1 - if name not in self._component_max or elapsed > self._component_max[name]: - self._component_max[name] = elapsed - if name not in self._component_min or elapsed < self._component_min[name]: - self._component_min[name] = elapsed + self._component_counts[name] = self._component_counts.get(name, 0) + 1 + if name not in self._component_max or elapsed > self._component_max[name]: self._component_max[name] = elapsed + if name not in self._component_min or elapsed < self._component_min[name]: self._component_min[name] = elapsed self._add_to_history(f'comp_{name}', elapsed) def get_metrics(self) -> dict[str, float]: """ - - Returns current metrics and their moving averages. Thread-safe. - [C: tests/test_perf_aggregate.py:test_build_tier3_context_scaling, tests/test_perf_dag.py:test_dag_performance, tests/test_performance_monitor.py:test_perf_monitor_basic_timing, tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager] + Returns current metrics and their moving averages. Thread-safe. + [C: tests/test_perf_aggregate.py:test_build_tier3_context_scaling, tests/test_perf_dag.py:test_dag_performance, tests/test_performance_monitor.py:test_perf_monitor_basic_timing, tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager] """ with self._lock: - fps = self._fps - last_ft = self._last_frame_time - cpu = self._cpu_percent - ilag = self._input_lag_ms - last_calc_fps = self._last_calculated_fps - total_frames = float(self._frame_count) + fps = self._fps + last_ft = self._last_frame_time + cpu = self._cpu_percent + ilag = self._input_lag_ms + last_calc_fps = self._last_calculated_fps + total_frames = float(self._frame_count) timings_snapshot = dict(self._component_timings) - counts_snapshot = dict(self._component_counts) - max_snapshot = dict(self._component_max) - min_snapshot = dict(self._component_min) + counts_snapshot = dict(self._component_counts) + max_snapshot = dict(self._component_max) + min_snapshot = dict(self._component_min) metrics = { - 'fps': fps, - 'fps_avg': self._get_avg('fps'), + 'fps': fps, + 'fps_avg': self._get_avg('fps'), 'last_frame_time_ms': last_ft, - 'frame_time_ms_avg': self._get_avg('frame_time_ms'), - 'cpu_percent': cpu, - 'cpu_percent_avg': self._get_avg('cpu_percent'), - 'input_lag_ms': ilag, - 'input_lag_ms_avg': self._get_avg('input_lag_ms'), - 'total_frames': total_frames + 'frame_time_ms_avg': self._get_avg('frame_time_ms'), + 'cpu_percent': cpu, + 'cpu_percent_avg': self._get_avg('cpu_percent'), + 'input_lag_ms': ilag, + 'input_lag_ms_avg': self._get_avg('input_lag_ms'), + 'total_frames': total_frames } for name, elapsed in timings_snapshot.items(): - metrics[f'time_{name}_ms'] = elapsed + metrics[f'time_{name}_ms'] = elapsed metrics[f'time_{name}_ms_avg'] = self._get_avg(f'comp_{name}') - metrics[f'count_{name}'] = float(counts_snapshot.get(name, 0)) - metrics[f'max_{name}_ms'] = max_snapshot.get(name, 0.0) - metrics[f'min_{name}_ms'] = min_snapshot.get(name, 0.0) + metrics[f'count_{name}'] = float(counts_snapshot.get(name, 0)) + metrics[f'max_{name}_ms'] = max_snapshot.get(name, 0.0) + metrics[f'min_{name}_ms'] = min_snapshot.get(name, 0.0) return metrics def get_history(self, key: str) -> List[float]: """ - - Returns a snapshot of the full history buffer for a specific metric key. - [C: tests/test_history.py:test_initial_state, tests/test_history.py:test_push_state, tests/test_history_manager.py:TestHistoryManager.test_get_history_returns_descriptions] + Returns a snapshot of the full history buffer for a specific metric key. + [C: tests/test_history.py:test_initial_state, tests/test_history.py:test_push_state, tests/test_history_manager.py:TestHistoryManager.test_get_history_returns_descriptions] """ with self._lock: if key in self._history: @@ -287,15 +280,14 @@ class PerformanceMonitor: def scope(self, name: str) -> PerformanceScope: """ - - Returns a context manager for timing a component. - [C: tests/test_perf_aggregate.py:test_build_tier3_context_scaling, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager] + Returns a context manager for timing a component. + [C: tests/test_perf_aggregate.py:test_build_tier3_context_scaling, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager] """ return PerformanceScope(self, name) def stop(self) -> None: """ - [C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing, tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast] + [C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing, tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast] """ self._stop_event.set() if self._cpu_thread.is_alive(): diff --git a/src/personas.py b/src/personas.py index 0c1ff936..574c8d40 100644 --- a/src/personas.py +++ b/src/personas.py @@ -15,7 +15,7 @@ class PersonaManager: def _get_path(self, scope: str) -> Path: """ - [C: src/tool_presets.py:ToolPresetManager.delete_bias_profile, src/tool_presets.py:ToolPresetManager.delete_preset, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile] + [C: src/tool_presets.py:ToolPresetManager.delete_bias_profile, src/tool_presets.py:ToolPresetManager.delete_preset, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile] """ if scope == "global": return paths.get_global_personas_path() @@ -28,9 +28,8 @@ class PersonaManager: def load_all(self) -> Dict[str, Persona]: """ - - Merges global and project personas into a single dictionary. - [C: tests/test_persona_manager.py:test_delete_persona, tests/test_persona_manager.py:test_load_all_merged, tests/test_persona_manager.py:test_save_persona, tests/test_preset_manager.py:test_delete_preset, tests/test_preset_manager.py:test_load_all_merged, tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project] + Merges global and project personas into a single dictionary. + [C: tests/test_persona_manager.py:test_delete_persona, tests/test_persona_manager.py:test_load_all_merged, tests/test_persona_manager.py:test_save_persona, tests/test_preset_manager.py:test_delete_preset, tests/test_preset_manager.py:test_load_all_merged, tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project] """ personas = {} @@ -49,7 +48,7 @@ class PersonaManager: def save_persona(self, persona: Persona, scope: str = "project") -> None: """ - [C: tests/test_persona_manager.py:test_save_persona] + [C: tests/test_persona_manager.py:test_save_persona] """ path = self._get_path(scope) data = self._load_file(path) @@ -76,7 +75,7 @@ class PersonaManager: def delete_persona(self, name: str, scope: str = "project") -> None: """ - [C: tests/test_persona_manager.py:test_delete_persona] + [C: tests/test_persona_manager.py:test_delete_persona] """ path = self._get_path(scope) data = self._load_file(path) @@ -86,7 +85,7 @@ class PersonaManager: def _load_file(self, path: Path) -> Dict[str, Any]: """ - [C: src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.get_preset_scope, src/presets.py:PresetManager.load_all, src/presets.py:PresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.load_all_profiles, src/workspace_manager.py:WorkspaceManager.save_profile] + [C: src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.get_preset_scope, src/presets.py:PresetManager.load_all, src/presets.py:PresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.load_all_profiles, src/workspace_manager.py:WorkspaceManager.save_profile] """ if not path.exists(): return {} @@ -98,7 +97,7 @@ class PersonaManager: def _save_file(self, path: Path, data: Dict[str, Any]) -> None: """ - [C: src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile] + [C: src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile] """ path.parent.mkdir(parents=True, exist_ok=True) with open(path, "wb") as f: diff --git a/src/presets.py b/src/presets.py index c465fcbd..90a11af0 100644 --- a/src/presets.py +++ b/src/presets.py @@ -22,9 +22,8 @@ class PresetManager: def load_all(self) -> Dict[str, Preset]: """ - - Merges global and project presets into a single dictionary. - [C: tests/test_persona_manager.py:test_delete_persona, tests/test_persona_manager.py:test_load_all_merged, tests/test_persona_manager.py:test_save_persona, tests/test_preset_manager.py:test_delete_preset, tests/test_preset_manager.py:test_load_all_merged, tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project] + Merges global and project presets into a single dictionary. + [C: tests/test_persona_manager.py:test_delete_persona, tests/test_persona_manager.py:test_load_all_merged, tests/test_persona_manager.py:test_save_persona, tests/test_preset_manager.py:test_delete_preset, tests/test_preset_manager.py:test_load_all_merged, tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project] """ presets: Dict[str, Preset] = {} @@ -49,9 +48,8 @@ class PresetManager: def save_preset(self, preset: Preset, scope: str = "project") -> None: """ - - Saves a preset to either the global or project-specific TOML file. - [C: tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_preset_manager.py:test_save_preset_project_no_root, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project] + Saves a preset to either the global or project-specific TOML file. + [C: tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_preset_manager.py:test_save_preset_project_no_root, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project] """ path = self.global_path if scope == "global" else self.project_path if not path: @@ -68,7 +66,7 @@ class PresetManager: def delete_preset(self, name: str, scope: str) -> None: """ - [C: tests/test_preset_manager.py:test_delete_preset, tests/test_presets.py:TestPresetManager.test_delete_preset] + [C: tests/test_preset_manager.py:test_delete_preset, tests/test_presets.py:TestPresetManager.test_delete_preset] """ if scope == "project" and self.project_root: path = get_project_presets_path(self.project_root) @@ -97,7 +95,7 @@ class PresetManager: def _load_file(self, path: Path) -> Dict[str, Any]: """ - [C: src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.load_all_profiles, src/workspace_manager.py:WorkspaceManager.save_profile] + [C: src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.load_all_profiles, src/workspace_manager.py:WorkspaceManager.save_profile] """ if not path.exists(): return {"presets": {}} @@ -115,7 +113,7 @@ class PresetManager: def _save_file(self, path: Path, data: Dict[str, Any]) -> None: """ - [C: src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile] + [C: src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile] """ if path.parent.exists() and path.parent.is_file(): raise ValueError(f"Cannot save to {path}: Parent directory {path.parent} is a file.") diff --git a/src/project_manager.py b/src/project_manager.py index 95e91ebc..5df65f45 100644 --- a/src/project_manager.py +++ b/src/project_manager.py @@ -38,16 +38,16 @@ def entry_to_str(entry: dict[str, Any]) -> str: Serialise a disc entry dict -> stored string. [C: tests/test_thinking_persistence.py:test_entry_to_str_with_thinking] """ - ts = entry.get("ts", "") - role = entry.get("role", "User") + ts = entry.get("ts", "") + role = entry.get("role", "User") content = entry.get("content", "") segments = entry.get("thinking_segments") if segments: for s in segments: - marker = s.get("marker", "thinking") + marker = s.get("marker", "thinking") s_content = s.get("content", "") - content = f"<{marker}>\n{s_content}\n\n{content}" + content = f"<{marker}>\n{s_content}\n\n{content}" if ts: return f"@{ts}\n{role}:\n{content}" @@ -64,27 +64,27 @@ def str_to_entry(raw: str, roles: list[str]) -> dict[str, Any]: Parse a stored string back to a disc entry dict. [C: tests/test_thinking_persistence.py:test_str_to_entry_with_thinking] """ - ts = "" + ts = "" rest = raw if rest.startswith("@"): nl = rest.find("\n") if nl != -1: - ts = rest[1:nl] + ts = rest[1:nl] rest = rest[nl + 1:] - known = roles or ["User", "AI", "Vendor API", "System"] + known = roles or ["User", "AI", "Vendor API", "System"] role_pat = re.compile( r"^(?:\[)?(" + "|".join(re.escape(r) for r in known) + r")(?:\])?:?\s*$", re.IGNORECASE, ) - parts = rest.split("\n", 1) + parts = rest.split("\n", 1) matched_role = "User" - content = rest.strip() + content = rest.strip() if parts: m = role_pat.match(parts[0].strip()) if m: - raw_role = m.group(1) + raw_role = m.group(1) matched_role = next((r for r in known if r.lower() == raw_role.lower()), raw_role) - content = parts[1].strip() if len(parts) > 1 else "" + content = parts[1].strip() if len(parts) > 1 else "" return {"role": matched_role, "content": content, "collapsed": False, "ts": ts} # ── git helpers ────────────────────────────────────────────────────────────── @@ -102,13 +102,13 @@ def get_git_commit(git_dir: str) -> str: def default_discussion() -> dict[str, Any]: """ - [C: tests/test_discussion_takes.py:TestDiscussionTakes.test_promote_take_renames_discussion] + [C: tests/test_discussion_takes.py:TestDiscussionTakes.test_promote_take_renames_discussion] """ return {"git_commit": "", "last_updated": now_ts(), "history": []} def default_project(name: str = "unnamed") -> dict[str, Any]: """ - [C: tests/test_deepseek_infra.py:test_default_project_includes_reasoning_role, tests/test_discussion_takes.py:TestDiscussionTakes.setUp, tests/test_history_management.py:test_history_persistence_across_turns, tests/test_history_management.py:test_save_separation, tests/test_project_manager_modes.py:test_default_project_execution_mode, tests/test_project_manager_modes.py:test_load_save_execution_mode, tests/test_project_serialization.py:TestProjectSerialization.test_default_roles_include_context, tests/test_project_serialization.py:TestProjectSerialization.test_fileitem_roundtrip] + [C: tests/test_deepseek_infra.py:test_default_project_includes_reasoning_role, tests/test_discussion_takes.py:TestDiscussionTakes.setUp, tests/test_history_management.py:test_history_persistence_across_turns, tests/test_history_management.py:test_save_separation, tests/test_project_manager_modes.py:test_default_project_execution_mode, tests/test_project_manager_modes.py:test_load_save_execution_mode, tests/test_project_serialization.py:TestProjectSerialization.test_default_roles_include_context, tests/test_project_serialization.py:TestProjectSerialization.test_fileitem_roundtrip] """ return { "project": {"name": name, "git_dir": "", "system_prompt": "", "execution_mode": "native"}, @@ -120,31 +120,31 @@ def default_project(name: str = "unnamed") -> dict[str, Any]: "deepseek": {"reasoning_effort": "medium"}, "agent": { "tools": { - "run_powershell": True, - "read_file": True, - "list_directory": True, - "search_files": True, - "get_file_summary": True, - "web_search": True, - "fetch_url": True, - "py_get_skeleton": True, - "py_get_code_outline": True, - "get_file_slice": True, - "py_get_definition": True, - "py_get_signature": True, - "py_get_class_summary": True, + "run_powershell": True, + "read_file": True, + "list_directory": True, + "search_files": True, + "get_file_summary": True, + "web_search": True, + "fetch_url": True, + "py_get_skeleton": True, + "py_get_code_outline": True, + "get_file_slice": True, + "py_get_definition": True, + "py_get_signature": True, + "py_get_class_summary": True, "py_get_var_declaration": True, - "get_git_diff": True, - "py_find_usages": True, - "py_get_imports": True, - "py_check_syntax": True, - "py_get_hierarchy": True, - "py_get_docstring": True, - "get_tree": True, - "get_ui_performance": True, - "set_file_slice": False, - "py_update_definition": False, - "py_set_signature": False, + "get_git_diff": True, + "py_find_usages": True, + "py_get_imports": True, + "py_check_syntax": True, + "py_get_hierarchy": True, + "py_get_docstring": True, + "get_tree": True, + "get_ui_performance": True, + "set_file_slice": False, + "py_update_definition": False, + "py_set_signature": False, "py_set_var_declaration": False, } }, @@ -163,23 +163,19 @@ def default_project(name: str = "unnamed") -> dict[str, Any]: def get_history_path(project_path: Union[str, Path]) -> Path: """ - - Return the Path to the sibling history TOML file for a given project. - [C: tests/test_history_management.py:test_save_separation] + Return the Path to the sibling history TOML file for a given project. + [C: tests/test_history_management.py:test_save_separation] """ p = Path(project_path) return p.parent / f"{p.stem}_history.toml" def load_project(path: Union[str, Path]) -> dict[str, Any]: """ - - - Load a project TOML file. - Automatically migrates legacy 'discussion' keys to a sibling history file. - [C: tests/test_history_management.py:test_history_persistence_across_turns, tests/test_history_management.py:test_migration_on_load, tests/test_project_manager_modes.py:test_load_save_execution_mode, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_project_serialization.py:TestProjectSerialization.test_fileitem_roundtrip] + Load a project TOML file. + Automatically migrates legacy 'discussion' keys to a sibling history file. + [C: tests/test_history_management.py:test_history_persistence_across_turns, tests/test_history_management.py:test_migration_on_load, tests/test_project_manager_modes.py:test_load_save_execution_mode, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_project_serialization.py:TestProjectSerialization.test_fileitem_roundtrip] """ - with open(path, "rb") as f: - proj = tomllib.load(f) + with open(path, "rb") as f: proj = tomllib.load(f) # Deserialise FileItems in files.paths if "files" in proj and "paths" in proj["files"]: from src import models @@ -198,9 +194,8 @@ def load_project(path: Union[str, Path]) -> dict[str, Any]: def load_history(project_path: Union[str, Path]) -> dict[str, Any]: """ - - Load the segregated discussion history from its dedicated TOML file. - [C: tests/test_thinking_persistence.py:test_save_and_load_history_with_thinking_segments] + Load the segregated discussion history from its dedicated TOML file. + [C: tests/test_thinking_persistence.py:test_save_and_load_history_with_thinking_segments] """ hist_path = get_history_path(project_path) if hist_path.exists(): @@ -210,31 +205,25 @@ def load_history(project_path: Union[str, Path]) -> dict[str, Any]: def clean_nones(data: Any) -> Any: """ - - Recursively remove None values from a dictionary/list. - [C: tests/test_thinking_persistence.py:test_clean_nones_removes_thinking] + Recursively remove None values from a dictionary/list. + [C: tests/test_thinking_persistence.py:test_clean_nones_removes_thinking] """ - if isinstance(data, dict): - return {k: clean_nones(v) for k, v in data.items() if v is not None} - elif isinstance(data, list): - return [clean_nones(v) for v in data if v is not None] + if isinstance(data, dict): return {k: clean_nones(v) for k, v in data.items() if v is not None} + elif isinstance(data, list): return [clean_nones(v) for v in data if v is not None] return data def save_project(proj: dict[str, Any], path: Union[str, Path], disc_data: Optional[dict[str, Any]] = None) -> None: """ - - - Save the project TOML. - If 'discussion' is present in proj, it is moved to the sibling history file. - [C: tests/test_history_management.py:test_history_persistence_across_turns, tests/test_history_management.py:test_save_separation, tests/test_project_manager_modes.py:test_load_save_execution_mode, tests/test_project_serialization.py:TestProjectSerialization.test_fileitem_roundtrip, tests/test_thinking_persistence.py:test_save_and_load_history_with_thinking_segments] + Save the project TOML. + If 'discussion' is present in proj, it is moved to the sibling history file. + [C: tests/test_history_management.py:test_history_persistence_across_turns, tests/test_history_management.py:test_save_separation, tests/test_project_manager_modes.py:test_load_save_execution_mode, tests/test_project_serialization.py:TestProjectSerialization.test_fileitem_roundtrip, tests/test_thinking_persistence.py:test_save_and_load_history_with_thinking_segments] """ proj = clean_nones(proj) # Serialise FileItems if "files" in proj and "paths" in proj["files"]: proj["files"]["paths"] = [p.to_dict() if hasattr(p, "to_dict") else p for p in proj["files"]["paths"]] if "discussion" in proj: - if disc_data is None: - disc_data = proj["discussion"] + if disc_data is None: disc_data = proj["discussion"] proj = dict(proj) del proj["discussion"] proj = clean_nones(proj) @@ -252,13 +241,12 @@ def migrate_from_legacy_config(cfg: dict[str, Any]) -> dict[str, Any]: name = cfg.get("output", {}).get("namespace", "project") proj = default_project(name) for key in ("output", "files", "screenshots"): - if key in cfg: - proj[key] = dict(cfg[key]) - disc = cfg.get("discussion", {}) + if key in cfg: proj[key] = dict(cfg[key]) + disc = cfg.get("discussion", {}) proj["discussion"]["roles"] = disc.get("roles", ["User", "AI", "Vendor API", "System", "Context"]) - main_disc = proj["discussion"]["discussions"]["main"] - main_disc["history"] = disc.get("history", []) - main_disc["last_updated"] = now_ts() + main_disc = proj["discussion"]["discussions"]["main"] + main_disc["history"] = disc.get("history", []) + main_disc["last_updated"] = now_ts() return proj # ── flat config for aggregate.run() ───────────────────────────────────────── @@ -268,9 +256,9 @@ def flat_config(proj: dict[str, Any], disc_name: Optional[str] = None, track_id: 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") + name = disc_name or disc_sec.get("active", "main") disc_data = disc_sec.get("discussions", {}).get(name, {}) - history = disc_data.get("history", []) + history = disc_data.get("history", []) return { "project": proj.get("project", {}), "output": proj.get("output", {}), @@ -286,187 +274,169 @@ def flat_config(proj: dict[str, Any], disc_name: Optional[str] = None, track_id: def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None: """ - - - Saves a TrackState object to conductor/tracks//state.toml. - [C: tests/test_project_manager_tracks.py:test_get_all_tracks_with_state, tests/test_track_state_persistence.py:test_track_state_persistence] + Saves a TrackState object to conductor/tracks//state.toml. + [C: tests/test_project_manager_tracks.py:test_get_all_tracks_with_state, tests/test_track_state_persistence.py:test_track_state_persistence] """ track_dir = paths.get_track_state_dir(track_id, project_path=str(base_dir)) 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) + with open(state_file, "wb") as f: tomli_w.dump(data, f) def load_track_state(track_id: str, base_dir: Union[str, Path] = ".") -> Optional['TrackState']: """ - - - Loads a TrackState object from conductor/tracks//state.toml. - [C: tests/test_track_state_persistence.py:test_track_state_persistence] + Loads a TrackState object from conductor/tracks//state.toml. + [C: tests/test_track_state_persistence.py:test_track_state_persistence] """ from src.models import TrackState state_file = paths.get_track_state_dir(track_id, project_path=str(base_dir)) / 'state.toml' - if not state_file.exists(): - return None - with open(state_file, "rb") as f: - data = tomllib.load(f) + 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: Union[str, Path] = ".") -> list[str]: """ - - - Loads the discussion history for a specific track from its state.toml. - Returns a list of entry strings formatted with @timestamp. + Loads the discussion history for a specific track from its state.toml. + Returns a list of entry strings formatted with @timestamp. """ state = load_track_state(track_id, base_dir) - if not state: - return [] + if not state: return [] history: list[str] = [] for entry in state.discussion: - e = dict(entry) + e = dict(entry) ts = e.get("ts") - if isinstance(ts, datetime.datetime): - e["ts"] = ts.strftime(TS_FMT) + 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[str], base_dir: Union[str, Path] = ".") -> None: """ - - - Saves the discussion history for a specific track to its state.toml. - 'history' is expected to be a list of formatted strings. + Saves the discussion history for a specific track to its state.toml. + 'history' is expected to be a list of formatted strings. """ state = load_track_state(track_id, base_dir) if not state: return - roles = ["User", "AI", "Vendor API", "System", "Reasoning"] + 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) def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]: """ - - - Scans the conductor/tracks/ directory and returns a list of dictionaries - containing track metadata: 'id', 'title', 'status', 'complete', 'total', - and 'progress' (0.0 to 1.0). - Handles missing or malformed metadata.json or state.toml by falling back - to available info or defaults. - [C: tests/test_project_manager_tracks.py:test_get_all_tracks_empty, tests/test_project_manager_tracks.py:test_get_all_tracks_malformed, tests/test_project_manager_tracks.py:test_get_all_tracks_with_metadata_json, tests/test_project_manager_tracks.py:test_get_all_tracks_with_state, tests/test_project_paths.py:test_get_all_tracks_project_specific] + Scans the conductor/tracks/ directory and returns a list of dictionaries + containing track metadata: 'id', 'title', 'status', 'complete', 'total', + and 'progress' (0.0 to 1.0). + Handles missing or malformed metadata.json or state.toml by falling back + to available info or defaults. + [C: tests/test_project_manager_tracks.py:test_get_all_tracks_empty, tests/test_project_manager_tracks.py:test_get_all_tracks_malformed, tests/test_project_manager_tracks.py:test_get_all_tracks_with_metadata_json, tests/test_project_manager_tracks.py:test_get_all_tracks_with_state, tests/test_project_paths.py:test_get_all_tracks_project_specific] """ tracks_dir = paths.get_tracks_dir(project_path=str(base_dir)) - if not tracks_dir.exists(): - return [] + if not tracks_dir.exists(): return [] + results: list[dict[str, Any]] = [] for entry in tracks_dir.iterdir(): - if not entry.is_dir(): - continue + if not entry.is_dir(): continue + track_id = entry.name track_info: dict[str, Any] = { - "id": track_id, - "title": track_id, - "status": "unknown", + "id": track_id, + "title": track_id, + "status": "unknown", "complete": 0, - "total": 0, + "total": 0, "progress": 0.0 } state_found = False + try: state = load_track_state(track_id, base_dir) if state: - track_info["id"] = state.metadata.id or track_id - track_info["title"] = state.metadata.name or track_id + track_info["id"] = state.metadata.id or track_id + track_info["title"] = state.metadata.name or track_id track_info["status"] = state.metadata.status or "unknown" progress = calculate_track_progress(state.tasks) track_info["complete"] = progress["completed"] - track_info["total"] = progress["total"] + track_info["total"] = progress["total"] track_info["progress"] = progress["percentage"] / 100.0 - state_found = True + state_found = True except Exception: pass + if not state_found: metadata_file = entry / "metadata.json" if metadata_file.exists(): try: with open(metadata_file, "r") as f: - data = json.load(f) - track_info["id"] = data.get("id", data.get("track_id", track_id)) - track_info["title"] = data.get("title", data.get("name", data.get("description", track_id))) + data = json.load(f) + track_info["id"] = data.get("id", data.get("track_id", track_id)) + track_info["title"] = data.get("title", data.get("name", data.get("description", track_id))) track_info["status"] = data.get("status", "unknown") except Exception: pass + if track_info["total"] == 0: plan_file = entry / "plan.md" if plan_file.exists(): try: with open(plan_file, "r", encoding="utf-8") as f: - content = f.read() - tasks = re.findall(r"^[ \t]*- \[[ x~]\] .*", content, re.MULTILINE) + content = f.read() + tasks = re.findall(r"^[ \t]*- \[[ x~]\] .*", content, re.MULTILINE) completed_tasks = re.findall(r"^[ \t]*- \[x\] .*", content, re.MULTILINE) - track_info["total"] = len(tasks) + track_info["total"] = len(tasks) track_info["complete"] = len(completed_tasks) if track_info["total"] > 0: track_info["progress"] = float(track_info["complete"]) / track_info["total"] except Exception: pass + results.append(track_info) return results def calculate_track_progress(tickets: list) -> dict: """ - - - Calculates track progress based on ticket statuses. - percentage (float), completed (int), total (int), in_progress (int), blocked (int), todo (int) - [C: tests/test_progress_viz.py:test_calculate_track_progress_all_completed, tests/test_progress_viz.py:test_calculate_track_progress_all_todo, tests/test_progress_viz.py:test_calculate_track_progress_empty, tests/test_progress_viz.py:test_calculate_track_progress_mixed] + Calculates track progress based on ticket statuses. + percentage (float), completed (int), total (int), in_progress (int), blocked (int), todo (int) + [C: tests/test_progress_viz.py:test_calculate_track_progress_all_completed, tests/test_progress_viz.py:test_calculate_track_progress_all_todo, tests/test_progress_viz.py:test_calculate_track_progress_empty, tests/test_progress_viz.py:test_calculate_track_progress_mixed] """ total = len(tickets) if total == 0: return { - "percentage": 0.0, - "completed": 0, - "total": 0, + "percentage": 0.0, + "completed": 0, + "total": 0, "in_progress": 0, - "blocked": 0, - "todo": 0 + "blocked": 0, + "todo": 0 } - completed = sum(1 for t in tickets if t.status == "completed") + completed = sum(1 for t in tickets if t.status == "completed") in_progress = sum(1 for t in tickets if t.status == "in_progress") - blocked = sum(1 for t in tickets if t.status == "blocked") - todo = sum(1 for t in tickets if t.status == "todo") - - percentage = (completed / total) * 100.0 + blocked = sum(1 for t in tickets if t.status == "blocked") + todo = sum(1 for t in tickets if t.status == "todo") + percentage = (completed / total) * 100.0 return { - "percentage": float(percentage), - "completed": completed, - "total": total, + "percentage": float(percentage), + "completed": completed, + "total": total, "in_progress": in_progress, - "blocked": blocked, - "todo": todo + "blocked": blocked, + "todo": todo } def branch_discussion(project_dict: dict, source_id: str, new_id: str, message_index: int) -> None: """ - - - Creates a new discussion in project_dict['discussion']['discussions'] by copying - the history from source_id up to (and including) message_index, and sets active to new_id. - [C: tests/test_discussion_takes.py:TestDiscussionTakes.test_branch_discussion_creates_new_take] + Creates a new discussion in project_dict['discussion']['discussions'] by copying + the history from source_id up to (and including) message_index, and sets active to new_id. + [C: tests/test_discussion_takes.py:TestDiscussionTakes.test_branch_discussion_creates_new_take] """ - if "discussion" not in project_dict or "discussions" not in project_dict["discussion"]: - return - if source_id not in project_dict["discussion"]["discussions"]: - return + if "discussion" not in project_dict or "discussions" not in project_dict["discussion"]: return + if source_id not in project_dict["discussion"]["discussions"]: return source_disc = project_dict["discussion"]["discussions"][source_id] - new_disc = default_discussion() + new_disc = default_discussion() new_disc["git_commit"] = source_disc.get("git_commit", "") # Copy history up to and including message_index new_disc["history"] = source_disc["history"][:message_index + 1] @@ -476,14 +446,11 @@ def branch_discussion(project_dict: dict, source_id: str, new_id: str, message_i def promote_take(project_dict: dict, take_id: str, new_id: str) -> None: """ - - Renames a take_id to new_id in the discussions dict. - [C: tests/test_discussion_takes.py:TestDiscussionTakes.test_promote_take_renames_discussion] + Renames a take_id to new_id in the discussions dict. + [C: tests/test_discussion_takes.py:TestDiscussionTakes.test_promote_take_renames_discussion] """ - if "discussion" not in project_dict or "discussions" not in project_dict["discussion"]: - return - if take_id not in project_dict["discussion"]["discussions"]: - return + if "discussion" not in project_dict or "discussions" not in project_dict["discussion"]: return + if take_id not in project_dict["discussion"]["discussions"]: return disc = project_dict["discussion"]["discussions"].pop(take_id) project_dict["discussion"]["discussions"][new_id] = disc