more organization

This commit is contained in:
ed
2026-06-06 11:08:07 -04:00
parent 7d555361f9
commit 339b062913
9 changed files with 471 additions and 544 deletions
+6 -12
View File
@@ -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}", ""]
+74 -79
View File
@@ -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
@@ -318,21 +319,21 @@ 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
@@ -364,9 +365,9 @@ 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
@@ -415,11 +416,11 @@ 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
@@ -469,9 +470,9 @@ 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
@@ -522,15 +523,15 @@ 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
@@ -630,9 +631,9 @@ 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
@@ -659,9 +660,9 @@ 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
@@ -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,13 +780,13 @@ 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
@@ -819,10 +814,10 @@ 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
@@ -846,12 +841,12 @@ 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
@@ -873,10 +868,10 @@ 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
@@ -903,10 +898,10 @@ 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
-9
View File
@@ -62,8 +62,6 @@ 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]
@@ -112,8 +110,6 @@ class WorkerPool:
class ConductorEngine:
"""
Orchestrates the execution of tickets within a track.
"""
@@ -154,7 +150,6 @@ class ConductorEngine:
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]
"""
@@ -162,7 +157,6 @@ class ConductorEngine:
def resume(self) -> None:
"""
Resumes the pipeline execution.
[C: tests/test_pipeline_pause.py:test_resume_method]
"""
@@ -170,7 +164,6 @@ class ConductorEngine:
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]
"""
@@ -179,7 +172,6 @@ class ConductorEngine:
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]
"""
@@ -188,7 +180,6 @@ class ConductorEngine:
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]
"""
+3 -10
View File
@@ -12,8 +12,6 @@ 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]
"""
@@ -21,13 +19,10 @@ def get_track_history_summary() -> str:
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
@@ -60,8 +55,6 @@ 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]
-2
View File
@@ -174,7 +174,6 @@ def get_conductor_dir(project_path: Optional[str] = None) -> Path:
project_root = Path(project_path).resolve()
p = _get_project_conductor_dir_from_toml(project_root)
if p: return p
return (project_root / "conductor").resolve()
def get_logs_dir() -> Path:
@@ -235,7 +234,6 @@ 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]
"""
+3 -11
View File
@@ -89,8 +89,6 @@ 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.
"""
@@ -158,8 +156,7 @@ 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:
@@ -229,15 +226,12 @@ class PerformanceMonitor:
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
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]
"""
@@ -274,7 +268,6 @@ class PerformanceMonitor:
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]
"""
@@ -287,7 +280,6 @@ 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]
"""
-1
View File
@@ -28,7 +28,6 @@ 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]
"""
-2
View File
@@ -22,7 +22,6 @@ 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]
"""
@@ -49,7 +48,6 @@ 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]
"""
+22 -55
View File
@@ -163,7 +163,6 @@ 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]
"""
@@ -172,14 +171,11 @@ def get_history_path(project_path: Union[str, Path]) -> Path:
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]
"""
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,7 +194,6 @@ 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]
"""
@@ -210,20 +205,15 @@ 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]
"""
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]
@@ -233,8 +223,7 @@ def save_project(proj: dict[str, Any], path: Union[str, Path], disc_data: Option
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,8 +241,7 @@ 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])
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"]
@@ -286,8 +274,6 @@ 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]
"""
@@ -295,47 +281,36 @@ def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Pa
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]
"""
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.
"""
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)
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.
"""
@@ -349,8 +324,6 @@ def save_track_history(track_id: str, history: list[str], base_dir: Union[str, P
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).
@@ -359,12 +332,12 @@ def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
[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,
@@ -375,6 +348,7 @@ def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
"progress": 0.0
}
state_found = False
try:
state = load_track_state(track_id, base_dir)
if state:
@@ -388,6 +362,7 @@ def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
state_found = True
except Exception:
pass
if not state_found:
metadata_file = entry / "metadata.json"
if metadata_file.exists():
@@ -399,6 +374,7 @@ def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
track_info["status"] = data.get("status", "unknown")
except Exception:
pass
if track_info["total"] == 0:
plan_file = entry / "plan.md"
if plan_file.exists():
@@ -413,13 +389,12 @@ def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
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]
@@ -439,7 +414,6 @@ def calculate_track_progress(tickets: list) -> dict:
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
return {
@@ -454,16 +428,12 @@ def calculate_track_progress(tickets: list) -> dict:
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]
"""
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()
@@ -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]
"""
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