more adjustments

This commit is contained in:
2026-03-12 19:08:51 -04:00
parent 19e7c94c2e
commit 1f8bb58219
14 changed files with 272 additions and 213 deletions

View File

@@ -33,7 +33,7 @@ For deep implementation details when planning or implementing tracks, consult `d
- **Track Browser:** Real-time visualization of all implementation tracks with status indicators and progress bars. Includes a dedicated **Active Track Summary** featuring a color-coded progress bar, precise ticket status breakdown (Completed, In Progress, Blocked, Todo), and dynamic **ETA estimation** based on historical completion times. - **Track Browser:** Real-time visualization of all implementation tracks with status indicators and progress bars. Includes a dedicated **Active Track Summary** featuring a color-coded progress bar, precise ticket status breakdown (Completed, In Progress, Blocked, Todo), and dynamic **ETA estimation** based on historical completion times.
- **Visual Task DAG:** An interactive, node-based visualizer for the active track's task dependencies using `imgui-node-editor`. Features color-coded state tracking (Ready, Running, Blocked, Done), drag-and-drop dependency creation, and right-click deletion. - **Visual Task DAG:** An interactive, node-based visualizer for the active track's task dependencies using `imgui-node-editor`. Features color-coded state tracking (Ready, Running, Blocked, Done), drag-and-drop dependency creation, and right-click deletion.
- **Strategy Visualization:** Dedicated real-time output streams for Tier 1 (Strategic Planning) and Tier 2/3 (Execution) agents, allowing the user to follow the agent's reasoning chains alongside the task DAG. - **Strategy Visualization:** Dedicated real-time output streams for Tier 1 (Strategic Planning) and Tier 2/3 (Execution) agents, allowing the user to follow the agent's reasoning chains alongside the task DAG.
- **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files. Supports **Project-Specific Conductor Directories**, allowing projects to define their own conductor path in `manual_slop.toml` (`[conductor].dir`) for isolated track management. This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective. - **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files. Supports **Project-Specific Conductor Directories**, defaulting to `./conductor` relative to each project's TOML file. Projects can define their own conductor path override in `manual_slop.toml` (`[conductor].dir`) via the Projects tab for isolated track management. This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective.
**Native DAG Execution Engine:** Employs a Python-based Directed Acyclic Graph (DAG) engine to manage complex task dependencies. Supports automated topological sorting, robust cycle detection, and **transitive blocking propagation** (cascading `blocked` status to downstream dependents to prevent execution stalls). **Native DAG Execution Engine:** Employs a Python-based Directed Acyclic Graph (DAG) engine to manage complex task dependencies. Supports automated topological sorting, robust cycle detection, and **transitive blocking propagation** (cascading `blocked` status to downstream dependents to prevent execution stalls).
- **Programmable Execution State machine:** Governing the transition between "Auto-Queue" (autonomous worker spawning) and "Step Mode" (explicit manual approval for each task transition). - **Programmable Execution State machine:** Governing the transition between "Auto-Queue" (autonomous worker spawning) and "Step Mode" (explicit manual approval for each task transition).
@@ -60,7 +60,7 @@ For deep implementation details when planning or implementing tracks, consult `d
- **Dedicated Diagnostics Hub:** Consolidates real-time telemetry (FPS, CPU, Frame Time) and transient system warnings into a standalone **Diagnostics panel**, providing deep visibility into application health without polluting the discussion history. - **Dedicated Diagnostics Hub:** Consolidates real-time telemetry (FPS, CPU, Frame Time) and transient system warnings into a standalone **Diagnostics panel**, providing deep visibility into application health without polluting the discussion history.
- **Improved MMA Observability:** Enhances sub-agent logging by injecting precise ticket IDs and descriptive roles into communication metadata, enabling granular filtering and tracking of parallel worker activities within the Comms History. - **Improved MMA Observability:** Enhances sub-agent logging by injecting precise ticket IDs and descriptive roles into communication metadata, enabling granular filtering and tracking of parallel worker activities within the Comms History.
- **In-Depth Toolset Access:** MCP-like file exploration, URL fetching, search, and dynamic context aggregation embedded within a multi-viewport Dear PyGui/ImGui interface. - **In-Depth Toolset Access:** MCP-like file exploration, URL fetching, search, and dynamic context aggregation embedded within a multi-viewport Dear PyGui/ImGui interface.
- **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows. Features **GUI-Based Path Configuration** within the Context Hub, allowing users to view and edit system paths (conductor, logs, scripts) with real-time resolution source tracking (default, env, or config). - **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows. Features **GUI-Based Path Configuration** within the Context Hub, allowing users to view and edit system paths (conductor, logs, scripts) with real-time resolution source tracking (default, env, or config). Changes are applied immediately at runtime without requiring an application restart.
- **Session Analysis:** Ability to load and visualize historical session logs with a dedicated tinted "Prior Session" viewing mode. - **Session Analysis:** Ability to load and visualize historical session logs with a dedicated tinted "Prior Session" viewing mode.
- **Structured Log Taxonomy:** Automated session-based log organization into configurable directories (defaulting to `logs/sessions/`). Includes a dedicated GUI panel for monitoring and manual whitelisting. Features an intelligent heuristic-based pruner that automatically cleans up insignificant logs older than 24 hours while preserving valuable sessions. - **Structured Log Taxonomy:** Automated session-based log organization into configurable directories (defaulting to `logs/sessions/`). Includes a dedicated GUI panel for monitoring and manual whitelisting. Features an intelligent heuristic-based pruner that automatically cleans up insignificant logs older than 24 hours while preserving valuable sessions.
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`. - **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.

View File

@@ -30,7 +30,7 @@
- **ai_style_formatter.py:** Custom Python formatter specifically designed to enforce 1-space indentation and ultra-compact whitespace to minimize token consumption. - **ai_style_formatter.py:** Custom Python formatter specifically designed to enforce 1-space indentation and ultra-compact whitespace to minimize token consumption.
- **src/paths.py:** Centralized module for path resolution. Supports project-specific conductor directory overrides via project TOML (`[conductor].dir`), enabling isolated track management per project. All paths are resolved to absolute objects. Provides **Path Resolution Metadata**, exposing the source of each resolved path (default, environment variable, or configuration file) for high-fidelity GUI display. Path configuration (logs, conductor, scripts) can also be configured via `config.toml` or environment variables, eliminating hardcoded filesystem dependencies. - **src/paths.py:** Centralized module for path resolution. Supports project-specific conductor directory overrides via project TOML (`[conductor].dir`), enabling isolated track management per project. If not specified, conductor paths default to `./conductor` relative to each project's TOML file. All paths are resolved to absolute objects. Provides **Path Resolution Metadata**, exposing the source of each resolved path (default, environment variable, or configuration file) for high-fidelity GUI display. Supports **Runtime Re-Resolution** via `reset_resolved()`, allowing path changes to be applied immediately without an application restart. Path configuration (logs, scripts) can also be configured via `config.toml` or environment variables, eliminating hardcoded filesystem dependencies.
- **src/presets.py:** Implements `PresetManager` for high-performance CRUD operations on system prompt presets stored in TOML format (`presets.toml`, `project_presets.toml`). Supports dynamic path resolution and scope-based inheritance. - **src/presets.py:** Implements `PresetManager` for high-performance CRUD operations on system prompt presets stored in TOML format (`presets.toml`, `project_presets.toml`). Supports dynamic path resolution and scope-based inheritance.

View File

@@ -1,12 +1,12 @@
[ai] [ai]
provider = "gemini_cli" provider = "minimax"
model = "gemini-2.5-flash-lite" model = "MiniMax-M2.5"
temperature = 0.85 temperature = 0.0
top_p = 1.0 top_p = 1.0
max_tokens = 1024 max_tokens = 32000
history_trunc_limit = 900000 history_trunc_limit = 900000
active_preset = "" active_preset = "Default"
system_prompt = "Overridden Prompt" system_prompt = ""
[projects] [projects]
paths = [ paths = [
@@ -17,7 +17,7 @@ paths = [
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_liveexecutionsim.toml", "C:\\projects\\manual_slop\\tests\\artifacts\\temp_liveexecutionsim.toml",
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_project.toml", "C:\\projects\\manual_slop\\tests\\artifacts\\temp_project.toml",
] ]
active = "C:\\projects\\manual_slop\\tests\\artifacts\\live_gui_workspace\\manual_slop.toml" active = "C:/projects/gencpp/gencpp_sloppy.toml"
[gui] [gui]
separate_message_panel = false separate_message_panel = false
@@ -38,7 +38,7 @@ separate_tier4 = false
"AI Settings" = true "AI Settings" = true
"MMA Dashboard" = true "MMA Dashboard" = true
"Task DAG" = true "Task DAG" = true
"Usage Analytics" = true "Usage Analytics" = false
"Tier 1" = false "Tier 1" = false
"Tier 2" = false "Tier 2" = false
"Tier 3" = false "Tier 3" = false
@@ -54,7 +54,6 @@ Response = false
"Tool Calls" = false "Tool Calls" = false
Theme = true Theme = true
"Log Management" = true "Log Management" = true
Diagnostics = false
[theme] [theme]
palette = "Nord Dark" palette = "Nord Dark"
@@ -69,3 +68,8 @@ max_workers = 4
[headless] [headless]
api_key = "test-secret-key" api_key = "test-secret-key"
[paths]
conductor_dir = "C:\\projects\\gencpp\\.ai\\conductor"
logs_dir = "C:\\projects\\manual_slop\\logs"
scripts_dir = "C:\\projects\\manual_slop\\scripts"

View File

@@ -54,8 +54,8 @@ Size=1111,224
Collapsed=0 Collapsed=0
[Window][Tool Calls] [Window][Tool Calls]
Pos=790,1483 Pos=855,1482
Size=876,654 Size=1014,655
Collapsed=0 Collapsed=0
DockId=0x00000006,0 DockId=0x00000006,0
@@ -74,8 +74,8 @@ Collapsed=0
DockId=0xAFC85805,2 DockId=0xAFC85805,2
[Window][Theme] [Window][Theme]
Pos=0,1786 Pos=0,1212
Size=676,351 Size=853,925
Collapsed=0 Collapsed=0
DockId=0x00000002,2 DockId=0x00000002,2
@@ -91,8 +91,8 @@ Collapsed=0
DockId=0x00000010,2 DockId=0x00000010,2
[Window][Context Hub] [Window][Context Hub]
Pos=0,1786 Pos=0,1212
Size=676,351 Size=853,925
Collapsed=0 Collapsed=0
DockId=0x00000002,1 DockId=0x00000002,1
@@ -103,26 +103,26 @@ Collapsed=0
DockId=0x0000000D,0 DockId=0x0000000D,0
[Window][Discussion Hub] [Window][Discussion Hub]
Pos=1668,22 Pos=1871,22
Size=915,2115 Size=949,2115
Collapsed=0 Collapsed=0
DockId=0x00000013,0 DockId=0x00000013,0
[Window][Operations Hub] [Window][Operations Hub]
Pos=678,22 Pos=855,22
Size=988,2115 Size=1014,2115
Collapsed=0 Collapsed=0
DockId=0x00000005,0 DockId=0x00000005,0
[Window][Files & Media] [Window][Files & Media]
Pos=0,1786 Pos=0,1212
Size=676,351 Size=853,925
Collapsed=0 Collapsed=0
DockId=0x00000002,0 DockId=0x00000002,0
[Window][AI Settings] [Window][AI Settings]
Pos=0,22 Pos=0,22
Size=676,1762 Size=853,1188
Collapsed=0 Collapsed=0
DockId=0x00000001,0 DockId=0x00000001,0
@@ -132,14 +132,14 @@ Size=416,325
Collapsed=0 Collapsed=0
[Window][MMA Dashboard] [Window][MMA Dashboard]
Pos=2585,22 Pos=2822,22
Size=1255,2115 Size=1018,2115
Collapsed=0 Collapsed=0
DockId=0x00000010,0 DockId=0x00000010,0
[Window][Log Management] [Window][Log Management]
Pos=2585,22 Pos=2822,22
Size=1255,2115 Size=1018,2115
Collapsed=0 Collapsed=0
DockId=0x00000010,1 DockId=0x00000010,1
@@ -333,8 +333,8 @@ Size=967,499
Collapsed=0 Collapsed=0
[Window][Usage Analytics] [Window][Usage Analytics]
Pos=2641,1719 Pos=2822,1716
Size=1199,418 Size=1018,421
Collapsed=0 Collapsed=0
DockId=0x0000000F,0 DockId=0x0000000F,0
@@ -384,11 +384,11 @@ Column 3 Width=20
Column 4 Weight=1.0000 Column 4 Weight=1.0000
[Table][0x2A6000B6,4] [Table][0x2A6000B6,4]
RefScale=24 RefScale=14
Column 0 Width=72 Column 0 Width=42
Column 1 Width=106 Column 1 Width=61
Column 2 Weight=1.0000 Column 2 Weight=1.0000
Column 3 Width=180 Column 3 Width=105
[Table][0x8BCC69C7,6] [Table][0x8BCC69C7,6]
RefScale=13 RefScale=13
@@ -407,11 +407,11 @@ Column 2 Weight=1.0000
Column 3 Width=105 Column 3 Width=105
[Table][0x2C515046,4] [Table][0x2C515046,4]
RefScale=24 RefScale=14
Column 0 Width=73 Column 0 Width=42
Column 1 Weight=1.0000 Column 1 Weight=1.0000
Column 2 Width=181 Column 2 Width=105
Column 3 Width=72 Column 3 Width=42
[Table][0xD99F45C5,4] [Table][0xD99F45C5,4]
Column 0 Sort=0v Column 0 Sort=0v
@@ -467,18 +467,18 @@ DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,22 Size=3840,2115 Split=X DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,22 Size=3840,2115 Split=X
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2583,1183 Split=X DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2820,1183 Split=X
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2 DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=676,858 Split=Y Selected=0x8CA2375C DockNode ID=0x00000007 Parent=0x0000000B SizeRef=853,858 Split=Y Selected=0x8CA2375C
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,1759 CentralNode=1 Selected=0x7BD57D6A DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,1188 CentralNode=1 Selected=0x7BD57D6A
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,351 Selected=0x1DCB2623 DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,925 Selected=0xF4139CA2
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1905,858 Split=X Selected=0x418C7449 DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1965,858 Split=X Selected=0x418C7449
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=988,402 Split=Y Selected=0x418C7449 DockNode ID=0x00000012 Parent=0x0000000E SizeRef=1014,402 Split=Y Selected=0x418C7449
DockNode ID=0x00000005 Parent=0x00000012 SizeRef=876,1455 Selected=0x418C7449 DockNode ID=0x00000005 Parent=0x00000012 SizeRef=876,1455 Selected=0x418C7449
DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,654 Selected=0x1D56B311 DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,654 Selected=0x1D56B311
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=915,402 Selected=0x6F2B5B04 DockNode ID=0x00000013 Parent=0x0000000E SizeRef=949,402 Selected=0x6F2B5B04
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1255,1183 Split=Y Selected=0x3AEC3498 DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1018,1183 Split=Y Selected=0x3AEC3498
DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0x2C0206CE DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0x2C0206CE
DockNode ID=0x00000011 Parent=0x00000004 SizeRef=1199,420 Split=X Selected=0xDEB547B6 DockNode ID=0x00000011 Parent=0x00000004 SizeRef=1199,420 Split=X Selected=0xDEB547B6
DockNode ID=0x0000000C Parent=0x00000011 SizeRef=916,380 Selected=0x655BC6E9 DockNode ID=0x0000000C Parent=0x00000011 SizeRef=916,380 Selected=0x655BC6E9

View File

@@ -2416,3 +2416,49 @@ PROMPT:
role: tool role: tool
Here are the results: {"content": "done"} Here are the results: {"content": "done"}
------------------ ------------------
--- MOCK INVOKED ---
ARGS: ['tests/mock_gemini_cli.py']
PROMPT:
PATH: Epic Initialization — please produce tracks
------------------
--- MOCK INVOKED ---
ARGS: ['tests/mock_gemini_cli.py']
PROMPT:
Please generate the implementation tickets for this track.
------------------
--- MOCK INVOKED ---
ARGS: ['tests/mock_gemini_cli.py']
PROMPT:
Please read test.txt
You are assigned to Ticket T1.
Task Description: do something
------------------
--- MOCK INVOKED ---
ARGS: ['tests/mock_gemini_cli.py']
PROMPT:
role: tool
Here are the results: {"content": "done"}
------------------
--- MOCK INVOKED ---
ARGS: ['tests/mock_gemini_cli.py']
PROMPT:
PATH: Epic Initialization — please produce tracks
------------------
--- MOCK INVOKED ---
ARGS: ['tests/mock_gemini_cli.py']
PROMPT:
Please generate the implementation tickets for this track.
------------------
--- MOCK INVOKED ---
ARGS: ['tests/mock_gemini_cli.py']
PROMPT:
Please read test.txt
You are assigned to Ticket T1.
Task Description: do something
------------------
--- MOCK INVOKED ---
ARGS: ['tests/mock_gemini_cli.py']
PROMPT:
role: tool
Here are the results: {"content": "done"}
------------------

View File

@@ -9,5 +9,5 @@ active = "main"
[discussions.main] [discussions.main]
git_commit = "" git_commit = ""
last_updated = "2026-03-11T23:45:09" last_updated = "2026-03-12T18:41:55"
history = [] history = []

View File

@@ -852,7 +852,6 @@ class AppController:
self.ui_separate_tier4 = False self.ui_separate_tier4 = False
self.config = models.load_config() self.config = models.load_config()
path_info = paths.get_full_path_info() path_info = paths.get_full_path_info()
self.ui_conductor_dir = str(path_info['conductor_dir']['path'])
self.ui_logs_dir = str(path_info['logs_dir']['path']) self.ui_logs_dir = str(path_info['logs_dir']['path'])
self.ui_scripts_dir = str(path_info['scripts_dir']['path']) self.ui_scripts_dir = str(path_info['scripts_dir']['path'])
theme.load_from_config(self.config) theme.load_from_config(self.config)
@@ -890,6 +889,7 @@ class AppController:
self.ui_shots_base_dir = self.project.get("screenshots", {}).get("base_dir", ".") self.ui_shots_base_dir = self.project.get("screenshots", {}).get("base_dir", ".")
proj_meta = self.project.get("project", {}) proj_meta = self.project.get("project", {})
self.ui_project_git_dir = proj_meta.get("git_dir", "") self.ui_project_git_dir = proj_meta.get("git_dir", "")
self.ui_project_conductor_dir = self.project.get('conductor', {}).get('dir', 'conductor')
self.ui_project_main_context = proj_meta.get("main_context", "") self.ui_project_main_context = proj_meta.get("main_context", "")
self.ui_project_system_prompt = proj_meta.get("system_prompt", "") self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini") self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
@@ -962,7 +962,7 @@ class AppController:
agent_tools_cfg = self.project.get("agent", {}).get("tools", {}) agent_tools_cfg = self.project.get("agent", {}).get("tools", {})
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES} self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES}
label = self.project.get("project", {}).get("name", "") label = self.project.get("project", {}).get("name", "")
session_logger.open_session(label=label) session_logger.reset_session(label=label)
# Trigger auto-start of MCP servers # Trigger auto-start of MCP servers
self.event_queue.put('refresh_external_mcps', None) self.event_queue.put('refresh_external_mcps', None)
@@ -2295,6 +2295,7 @@ class AppController:
proj["screenshots"]["paths"] = self.screenshots proj["screenshots"]["paths"] = self.screenshots
proj.setdefault("project", {}) proj.setdefault("project", {})
proj["project"]["git_dir"] = self.ui_project_git_dir proj["project"]["git_dir"] = self.ui_project_git_dir
proj.setdefault("conductor", {})["dir"] = self.ui_project_conductor_dir
proj["project"]["system_prompt"] = self.ui_project_system_prompt proj["project"]["system_prompt"] = self.ui_project_system_prompt
proj["project"]["main_context"] = self.ui_project_main_context proj["project"]["main_context"] = self.ui_project_main_context
proj["project"]["active_preset"] = self.ui_project_preset_name proj["project"]["active_preset"] = self.ui_project_preset_name

View File

@@ -1419,6 +1419,15 @@ class App:
r.destroy() r.destroy()
if d: self.ui_output_dir = d if d: self.ui_output_dir = d
imgui.separator() imgui.separator()
imgui.text("Conductor Directory")
ch, self.ui_project_conductor_dir = imgui.input_text("##cond_dir", self.ui_project_conductor_dir)
imgui.same_line()
if imgui.button("Browse##cond"):
r = hide_tk_root()
d = filedialog.askdirectory(title="Select Conductor Directory")
r.destroy()
if d: self.ui_project_conductor_dir = d
imgui.separator()
imgui.text("Project Files") imgui.text("Project Files")
imgui.begin_child("proj_files", imgui.ImVec2(0, 150), True) imgui.begin_child("proj_files", imgui.ImVec2(0, 150), True)
for i, pp in enumerate(self.project_paths): for i, pp in enumerate(self.project_paths):
@@ -1473,7 +1482,6 @@ class App:
def _save_paths(self): def _save_paths(self):
self.config["paths"] = { self.config["paths"] = {
"conductor_dir": self.ui_conductor_dir,
"logs_dir": self.ui_logs_dir, "logs_dir": self.ui_logs_dir,
"scripts_dir": self.ui_scripts_dir "scripts_dir": self.ui_scripts_dir
} }
@@ -1482,7 +1490,8 @@ class App:
shutil.copy(cfg_path, str(cfg_path) + ".bak") shutil.copy(cfg_path, str(cfg_path) + ".bak")
models.save_config(self.config) models.save_config(self.config)
paths.reset_resolved() paths.reset_resolved()
self.ai_status = "paths saved - restart required" self.init_state()
self.ai_status = 'paths applied and session reset'
def _render_paths_panel(self) -> None: def _render_paths_panel(self) -> None:
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_paths_panel") if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_paths_panel")
@@ -1490,10 +1499,6 @@ class App:
imgui.text_colored(C_IN, "System Path Configuration") imgui.text_colored(C_IN, "System Path Configuration")
imgui.separator() imgui.separator()
if self.ai_status == "paths saved - restart required":
imgui.text_colored(vec4(255, 50, 50), "Restart required for path changes to take effect.")
imgui.separator()
def render_path_field(label: str, attr: str, key: str, tooltip: str): def render_path_field(label: str, attr: str, key: str, tooltip: str):
info = path_info.get(key, {'source': 'unknown'}) info = path_info.get(key, {'source': 'unknown'})
@@ -1513,7 +1518,6 @@ class App:
r.destroy() r.destroy()
if d: setattr(self, attr, d) if d: setattr(self, attr, d)
render_path_field("Conductor Directory", "ui_conductor_dir", "conductor_dir", "Base directory for implementation tracks and project state.")
render_path_field("Logs Directory", "ui_logs_dir", "logs_dir", "Directory where session JSON-L logs and artifacts are stored.") render_path_field("Logs Directory", "ui_logs_dir", "logs_dir", "Directory where session JSON-L logs and artifacts are stored.")
render_path_field("Scripts Directory", "ui_scripts_dir", "scripts_dir", "Directory for AI-generated PowerShell scripts.") render_path_field("Scripts Directory", "ui_scripts_dir", "scripts_dir", "Directory for AI-generated PowerShell scripts.")

View File

@@ -6,13 +6,11 @@ This module provides centralized path resolution for all configurable paths in t
Environment Variables: Environment Variables:
SLOP_CONFIG: Path to config.toml SLOP_CONFIG: Path to config.toml
SLOP_CONDUCTOR_DIR: Path to conductor directory
SLOP_LOGS_DIR: Path to logs directory SLOP_LOGS_DIR: Path to logs directory
SLOP_SCRIPTS_DIR: Path to generated scripts directory SLOP_SCRIPTS_DIR: Path to generated scripts directory
Configuration (config.toml): Configuration (config.toml):
[paths] [paths]
conductor_dir = "conductor"
logs_dir = "logs/sessions" logs_dir = "logs/sessions"
scripts_dir = "scripts/generated" scripts_dir = "scripts/generated"
@@ -27,8 +25,8 @@ Path Functions:
Resolution Order: Resolution Order:
1. Check project-specific manual_slop.toml (for conductor paths) 1. Check project-specific manual_slop.toml (for conductor paths)
2. Check environment variable 2. Check environment variable (for logs/scripts)
3. Check config.toml [paths] section 3. Check config.toml [paths] section (for logs/scripts)
4. Fall back to default 4. Fall back to default
Usage: Usage:
@@ -110,37 +108,15 @@ def _get_project_conductor_dir_from_toml(project_root: Path) -> Optional[Path]:
return None return None
def get_conductor_dir(project_path: Optional[str] = None) -> Path: def get_conductor_dir(project_path: Optional[str] = None) -> Path:
if project_path: if not project_path:
project_root = Path(project_path).resolve() # Fallback for legacy/tests, but we should avoid this
p = _get_project_conductor_dir_from_toml(project_root) return Path('conductor').resolve()
if p: return p
if "conductor_dir" not in _RESOLVED: project_root = Path(project_path).resolve()
# Check env and config p = _get_project_conductor_dir_from_toml(project_root)
root_dir = Path(__file__).resolve().parent.parent if p: return p
env_val = os.environ.get("SLOP_CONDUCTOR_DIR")
if env_val:
p = Path(env_val)
if not p.is_absolute(): p = root_dir / p
_RESOLVED["conductor_dir"] = p.resolve()
else:
try:
with open(get_config_path(), "rb") as f:
cfg = tomllib.load(f)
if "paths" in cfg and "conductor_dir" in cfg["paths"]:
p = Path(cfg["paths"]["conductor_dir"])
if not p.is_absolute(): p = root_dir / p
_RESOLVED["conductor_dir"] = p.resolve()
except: pass
if "conductor_dir" in _RESOLVED:
return _RESOLVED["conductor_dir"]
if project_path: return (project_root / "conductor").resolve()
return (Path(project_path).resolve() / "conductor").resolve()
root_dir = Path(__file__).resolve().parent.parent
return (root_dir / "conductor").resolve()
def get_logs_dir() -> Path: def get_logs_dir() -> Path:
if "logs_dir" not in _RESOLVED: if "logs_dir" not in _RESOLVED:
@@ -179,7 +155,6 @@ def _resolve_path_info(env_var: str, config_key: str, default: str) -> dict[str,
def get_full_path_info() -> dict[str, dict[str, Any]]: def get_full_path_info() -> dict[str, dict[str, Any]]:
return { return {
'conductor_dir': _resolve_path_info('SLOP_CONDUCTOR_DIR', 'conductor_dir', 'conductor'),
'logs_dir': _resolve_path_info('SLOP_LOGS_DIR', 'logs_dir', 'logs/sessions'), 'logs_dir': _resolve_path_info('SLOP_LOGS_DIR', 'logs_dir', 'logs/sessions'),
'scripts_dir': _resolve_path_info('SLOP_SCRIPTS_DIR', 'scripts_dir', 'scripts/generated') 'scripts_dir': _resolve_path_info('SLOP_SCRIPTS_DIR', 'scripts_dir', 'scripts/generated')
} }

View File

@@ -112,6 +112,11 @@ def close_session() -> None:
except Exception as e: except Exception as e:
print(f"Warning: Could not update auto-whitelist on close: {e}") print(f"Warning: Could not update auto-whitelist on close: {e}")
def reset_session(label: Optional[str] = None) -> None:
"""Closes the current session and opens a new one with the given label."""
close_session()
open_session(label)
def log_api_hook(method: str, path: str, payload: str) -> None: def log_api_hook(method: str, path: str, payload: str) -> None:
"""Log an API hook invocation.""" """Log an API hook invocation."""
if _api_fh is None: if _api_fh is None:

View File

@@ -4,33 +4,38 @@ from src import paths
# We mock App to avoid the heavy initialization logic # We mock App to avoid the heavy initialization logic
class MockApp: class MockApp:
def __init__(self): def __init__(self):
self.ui_conductor_dir = '/mock/conductor' self.ui_conductor_dir = '/mock/conductor'
self.ui_logs_dir = '/mock/logs' self.ui_logs_dir = '/mock/logs'
self.ui_scripts_dir = '/mock/scripts' self.ui_scripts_dir = '/mock/scripts'
self.config = {"paths": {}} self.config = {"paths": {}}
self.ai_status = "" self.ai_status = ""
from src.gui_2 import App def init_state(self):
_save_paths = App._save_paths pass
from src.gui_2 import App
_save_paths = App._save_paths
def test_save_paths(): def test_save_paths():
mock_app = MockApp() mock_app = MockApp()
with patch('src.models.save_config') as mock_save, \ with patch('src.models.save_config') as mock_save, \
patch('shutil.copy') as mock_copy, \ patch('shutil.copy') as mock_copy, \
patch('src.paths.get_config_path') as mock_get_cfg, \ patch('src.paths.get_config_path') as mock_get_cfg, \
patch('src.paths.reset_resolved') as mock_reset: patch('src.paths.reset_resolved') as mock_reset, \
patch.object(MockApp, 'init_state') as mock_init:
mock_get_cfg.return_value = MagicMock()
mock_get_cfg.return_value.exists.return_value = True mock_get_cfg.return_value = MagicMock()
mock_get_cfg.return_value.exists.return_value = True
mock_app.ui_conductor_dir = '/new/conductor'
mock_app._save_paths() mock_app.ui_conductor_dir = '/new/conductor'
mock_app._save_paths()
# Verify config update
assert mock_app.config['paths']['conductor_dir'] == '/new/conductor' # Verify config update
mock_save.assert_called_once() assert 'conductor_dir' not in mock_app.config['paths']
mock_copy.assert_called_once() mock_save.assert_called_once()
assert 'restart required' in mock_app.ai_status mock_copy.assert_called_once()
mock_reset.assert_called_once() assert 'applied' in mock_app.ai_status
mock_reset.assert_called_once()
mock_init.assert_called_once()

View File

@@ -9,25 +9,15 @@ def reset_paths():
yield yield
paths.reset_resolved() paths.reset_resolved()
def test_default_paths(): def test_default_paths(tmp_path, monkeypatch):
monkeypatch.setenv("SLOP_CONFIG", str(tmp_path / "non_existent.toml"))
root_dir = Path(paths.__file__).resolve().parent.parent root_dir = Path(paths.__file__).resolve().parent.parent
assert paths.get_conductor_dir() == root_dir / "conductor"
assert paths.get_logs_dir() == root_dir / "logs/sessions" assert paths.get_logs_dir() == root_dir / "logs/sessions"
assert paths.get_scripts_dir() == root_dir / "scripts/generated" assert paths.get_scripts_dir() == root_dir / "scripts/generated"
# config path should now be an absolute path relative to src/paths.py # config path should be what we set in env
assert paths.get_config_path() == root_dir / "config.toml" assert paths.get_config_path() == tmp_path / "non_existent.toml"
assert paths.get_tracks_dir() == root_dir / "conductor/tracks"
assert paths.get_archive_dir() == root_dir / "conductor/archive"
def test_env_var_overrides(tmp_path, monkeypatch): def test_env_var_overrides(tmp_path, monkeypatch):
root_dir = Path(paths.__file__).resolve().parent.parent
# Relative env var (resolved against root_dir)
monkeypatch.setenv("SLOP_CONDUCTOR_DIR", "custom_conductor")
assert paths.get_conductor_dir() == (root_dir / "custom_conductor").resolve()
paths.reset_resolved()
# Absolute env var # Absolute env var
abs_logs = (tmp_path / "abs_logs").resolve() abs_logs = (tmp_path / "abs_logs").resolve()
monkeypatch.setenv("SLOP_LOGS_DIR", str(abs_logs)) monkeypatch.setenv("SLOP_LOGS_DIR", str(abs_logs))
@@ -38,14 +28,12 @@ def test_config_overrides(tmp_path, monkeypatch):
config_file = tmp_path / "custom_config.toml" config_file = tmp_path / "custom_config.toml"
content = """ content = """
[paths] [paths]
conductor_dir = "cfg_conductor"
logs_dir = "cfg_logs" logs_dir = "cfg_logs"
scripts_dir = "cfg_scripts" scripts_dir = "cfg_scripts"
""" """
config_file.write_text(content) config_file.write_text(content)
monkeypatch.setenv("SLOP_CONFIG", str(config_file)) monkeypatch.setenv("SLOP_CONFIG", str(config_file))
assert paths.get_conductor_dir() == (root_dir / "cfg_conductor").resolve()
assert paths.get_logs_dir() == root_dir / "cfg_logs" assert paths.get_logs_dir() == root_dir / "cfg_logs"
assert paths.get_scripts_dir() == root_dir / "cfg_scripts" assert paths.get_scripts_dir() == root_dir / "cfg_scripts"
@@ -54,11 +42,20 @@ def test_precedence(tmp_path, monkeypatch):
config_file = tmp_path / "custom_config.toml" config_file = tmp_path / "custom_config.toml"
content = """ content = """
[paths] [paths]
conductor_dir = "cfg_conductor" logs_dir = "cfg_logs"
""" """
config_file.write_text(content) config_file.write_text(content)
monkeypatch.setenv("SLOP_CONFIG", str(config_file)) monkeypatch.setenv("SLOP_CONFIG", str(config_file))
monkeypatch.setenv("SLOP_CONDUCTOR_DIR", "env_conductor") monkeypatch.setenv("SLOP_LOGS_DIR", "env_logs")
# Env var should take precedence over config # Env var should take precedence over config
assert paths.get_conductor_dir() == (root_dir / "env_conductor").resolve() assert paths.get_logs_dir() == (root_dir / "env_logs").resolve()
def test_conductor_dir_project_relative(tmp_path):
# Should default to tmp_path/conductor
project_path = str(tmp_path)
base = (tmp_path / 'conductor').resolve()
assert paths.get_conductor_dir(project_path) == base
assert paths.get_tracks_dir(project_path) == base / "tracks"
assert paths.get_archive_dir(project_path) == base / "archive"
assert paths.get_track_state_dir("test_track", project_path) == base / "tracks" / "test_track"

View File

@@ -7,82 +7,53 @@ from src import paths
from src import project_manager from src import project_manager
def test_get_conductor_dir_default(): def test_get_conductor_dir_default():
paths.reset_resolved() paths.reset_resolved()
# Should return absolute path to "conductor" in project root # Should return absolute path to "conductor" in project root
expected = Path(__file__).resolve().parent.parent / "conductor" expected = Path(__file__).resolve().parent.parent / "conductor"
assert paths.get_conductor_dir() == expected assert paths.get_conductor_dir() == expected
def test_get_conductor_dir_project_specific_with_toml(tmp_path): def test_get_conductor_dir_project_specific_with_toml(tmp_path):
paths.reset_resolved() paths.reset_resolved()
project_root = tmp_path / "my_project" project_root = tmp_path / "my_project"
project_root.mkdir() project_root.mkdir()
# Create manual_slop.toml with custom conductor dir # Create manual_slop.toml with custom conductor dir
toml_path = project_root / "manual_slop.toml" toml_path = project_root / "manual_slop.toml"
config = { config = {
"conductor": { "conductor": {
"dir": "custom_tracks" "dir": "custom_tracks"
} }
} }
with open(toml_path, "wb") as f: with open(toml_path, "wb") as f:
f.write(tomli_w.dumps(config).encode()) f.write(tomli_w.dumps(config).encode())
res = paths.get_conductor_dir(project_path=str(project_root)) res = paths.get_conductor_dir(project_path=str(project_root))
assert res == project_root / "custom_tracks" assert res == project_root / "custom_tracks"
def test_get_all_tracks_project_specific(tmp_path): def test_get_all_tracks_project_specific(tmp_path):
paths.reset_resolved() paths.reset_resolved()
project_root = tmp_path / "my_project" project_root = tmp_path / "my_project"
project_root.mkdir() project_root.mkdir()
# Custom conductor dir # Custom conductor dir
custom_dir = project_root / "my_conductor" custom_dir = project_root / "my_conductor"
custom_dir.mkdir() custom_dir.mkdir()
tracks_dir = custom_dir / "tracks" tracks_dir = custom_dir / "tracks"
tracks_dir.mkdir() tracks_dir.mkdir()
# Create a dummy track # Create a dummy track
track_dir = tracks_dir / "test_track_20260312" track_dir = tracks_dir / "test_track_20260312"
track_dir.mkdir() track_dir.mkdir()
with open(track_dir / "metadata.json", "w") as f: with open(track_dir / "metadata.json", "w") as f:
json.dump({"id": "test_track", "title": "Test Track"}, f) json.dump({"id": "test_track", "title": "Test Track"}, f)
# Setup manual_slop.toml # Setup manual_slop.toml
toml_path = project_root / "manual_slop.toml" toml_path = project_root / "manual_slop.toml"
config = {"conductor": {"dir": "my_conductor"}} config = {"conductor": {"dir": "my_conductor"}}
with open(toml_path, "wb") as f: with open(toml_path, "wb") as f:
f.write(tomli_w.dumps(config).encode()) f.write(tomli_w.dumps(config).encode())
# project_manager.get_all_tracks(base_dir) should now find it # project_manager.get_all_tracks(base_dir) should now find it
tracks = project_manager.get_all_tracks(str(project_root)) tracks = project_manager.get_all_tracks(str(project_root))
assert len(tracks) == 1 assert len(tracks) == 1
assert tracks[0]["title"] == "Test Track" assert tracks[0]["title"] == "Test Track"
def test_get_all_tracks_global_fallback(tmp_path):
paths.reset_resolved()
# Create a directory without manual_slop.toml
empty_dir = tmp_path / "empty_project"
empty_dir.mkdir()
# Setup a fake global conductor
global_conductor = tmp_path / "global_conductor"
global_conductor.mkdir()
global_tracks = global_conductor / "tracks"
global_tracks.mkdir()
track_dir = global_tracks / "global_track"
track_dir.mkdir()
with open(track_dir / "metadata.json", "w") as f:
json.dump({"id": "global_track", "title": "Global Track"}, f)
# Override global conductor dir via env var
os.environ["SLOP_CONDUCTOR_DIR"] = str(global_conductor)
try:
paths.reset_resolved()
# Pass project_path pointing to a dir without TOML
tracks = project_manager.get_all_tracks(str(empty_dir))
# paths.get_conductor_dir(str(empty_dir)) should fall back to global
assert any(t["id"] == "global_track" for t in tracks)
finally:
del os.environ["SLOP_CONDUCTOR_DIR"]

View File

@@ -0,0 +1,51 @@
import pytest
import tomllib
from pathlib import Path
from typing import Generator
from src import session_logger
import time
@pytest.fixture
def temp_logs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Generator[Path, None, None]:
# Ensure closed before starting
session_logger.close_session()
monkeypatch.setattr(session_logger, "_comms_fh", None)
log_dir = tmp_path / "logs"
scripts_dir = tmp_path / "scripts" / "generated"
log_dir.mkdir(parents=True, exist_ok=True)
scripts_dir.mkdir(parents=True, exist_ok=True)
from src import paths
monkeypatch.setattr(paths, "get_logs_dir", lambda: log_dir)
monkeypatch.setattr(paths, "get_scripts_dir", lambda: scripts_dir)
yield log_dir
# Cleanup: Close handles if open
session_logger.close_session()
def test_reset_session(temp_logs: Path) -> None:
# 1. Open first session
label1 = "label1"
session_logger.open_session(label=label1)
subdirs = [d for d in temp_logs.iterdir() if d.is_dir()]
assert len(subdirs) == 1
session1_dir = subdirs[0]
assert session1_dir.name.endswith(f"_{label1}")
assert session_logger._comms_fh is not None
# 2. Reset to second session
time.sleep(1.1)
label2 = "label2"
session_logger.reset_session(label=label2)
subdirs = sorted([d for d in temp_logs.iterdir() if d.is_dir()], key=lambda x: x.name)
assert len(subdirs) == 2
session2_dir = subdirs[1]
assert session2_dir.name.endswith(f"_{label2}")
assert session_logger._comms_fh is not None
# Verify new handle points to new dir
assert str(session2_dir) in str(Path(session_logger._comms_fh.name).resolve())