more organization
This commit is contained in:
+8
-14
@@ -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}", ""]
|
||||
|
||||
+192
-197
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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}")
|
||||
|
||||
+17
-24
@@ -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))
|
||||
print(json.dumps(tracks, indent=2))
|
||||
|
||||
+20
-22
@@ -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()
|
||||
|
||||
+62
-70
@@ -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():
|
||||
|
||||
+7
-8
@@ -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:
|
||||
|
||||
+7
-9
@@ -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.")
|
||||
|
||||
+130
-163
@@ -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</{marker}>\n{content}"
|
||||
content = f"<{marker}>\n{s_content}\n</{marker}>\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/<track_id>/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/<track_id>/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/<track_id>/state.toml.
|
||||
[C: tests/test_track_state_persistence.py:test_track_state_persistence]
|
||||
Loads a TrackState object from conductor/tracks/<track_id>/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
|
||||
|
||||
Reference in New Issue
Block a user