feat(taxonomy): Redirect logs and artifacts to dedicated sub-folders

This commit is contained in:
2026-03-01 09:03:02 -05:00
parent 09bedbf4f0
commit 6326546005
10 changed files with 36 additions and 16 deletions

View File

@@ -16,5 +16,5 @@ This file tracks all major tracks for the project. Each track has its own detail
--- ---
- [ ] **Track: Consolidate Temp/Test Cruft & Log Taxonomy** - [~] **Track: Consolidate Temp/Test Cruft & Log Taxonomy**
*Link: [./tracks/consolidate_cruft_and_log_taxonomy_20260228/](./tracks/consolidate_cruft_and_log_taxonomy_20260228/)* *Link: [./tracks/consolidate_cruft_and_log_taxonomy_20260228/](./tracks/consolidate_cruft_and_log_taxonomy_20260228/)*

View File

@@ -6,7 +6,7 @@
- [x] Task: Conductor - User Manual Verification 'Phase 1: Directory Structure & Gitignore' (Protocol in workflow.md) (fab109e) - [x] Task: Conductor - User Manual Verification 'Phase 1: Directory Structure & Gitignore' (Protocol in workflow.md) (fab109e)
## Phase 2: App Logic Redirection ## Phase 2: App Logic Redirection
- [ ] Task: Update `session_logger.py` to use `logs/sessions/`, `logs/agents/`, and `logs/errors/` for its outputs. - [~] Task: Update `session_logger.py` to use `logs/sessions/`, `logs/agents/`, and `logs/errors/` for its outputs.
- [ ] Task: Modify `project_manager.py` to store temporary project TOMLs in `tests/artifacts/`. - [ ] Task: Modify `project_manager.py` to store temporary project TOMLs in `tests/artifacts/`.
- [ ] Task: Update `shell_runner.py` or `scripts/mma_exec.py` to use `tests/artifacts/` for its temporary scripts and outputs. - [ ] Task: Update `shell_runner.py` or `scripts/mma_exec.py` to use `tests/artifacts/` for its temporary scripts and outputs.
- [ ] Task: Add foundational support (e.g., in `metadata.json` for sessions) to store "annotated names" for logs. - [ ] Task: Add foundational support (e.g., in `metadata.json` for sessions) to store "annotated names" for logs.

View File

@@ -1224,9 +1224,8 @@ class App:
"""A dummy function that a custom_callback would execute for testing.""" """A dummy function that a custom_callback would execute for testing."""
# Note: This file path is relative to where the test is run. # Note: This file path is relative to where the test is run.
# This is for testing purposes only. # This is for testing purposes only.
with open("temp_callback_output.txt", "w") as f: with open("tests/artifacts/temp_callback_output.txt", "w") as f:
f.write(data) f.write(data)
def _recalculate_session_usage(self) -> None: def _recalculate_session_usage(self) -> None:
usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "total_tokens": 0, "last_latency": 0.0} usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "total_tokens": 0, "last_latency": 0.0}
for entry in ai_client.get_comms_log(): for entry in ai_client.get_comms_log():

View File

@@ -8,7 +8,7 @@ import tree_sitter_python
import ast import ast
import datetime import datetime
LOG_FILE: str = 'logs/mma_delegation.log' LOG_FILE: str = 'logs/errors/mma_delegation.log'
def generate_skeleton(code: str) -> str: def generate_skeleton(code: str) -> str:

View File

@@ -3,33 +3,39 @@
Opens timestamped log/script files at startup and keeps them open for the Opens timestamped log/script files at startup and keeps them open for the
lifetime of the process. The next run of the GUI creates new files; the lifetime of the process. The next run of the GUI creates new files; the
previous run's files are simply closed when the process exits. previous run's files are simply closed when the process exits.
File layout File layout
----------- -----------
logs/ logs/sessions/
comms_<ts>.log - every comms entry (direction/kind/payload) as JSON-L comms_<ts>.log - every comms entry (direction/kind/payload) as JSON-L
toolcalls_<ts>.log - sequential record of every tool invocation toolcalls_<ts>.log - sequential record of every tool invocation
clicalls_<ts>.log - sequential record of every CLI subprocess call clicalls_<ts>.log - sequential record of every CLI subprocess call
scripts/generated/ scripts/generated/
<ts>_<seq:04d>.ps1 - each PowerShell script the AI generated, in order <ts>_<seq:04d>.ps1 - each PowerShell script the AI generated, in order
Where <ts> = YYYYMMDD_HHMMSS of when this session was started. Where <ts> = YYYYMMDD_HHMMSS of when this session was started.
""" """
import atexit import atexit
import datetime import datetime
import json import json
import threading import threading
from typing import Any, Optional, TextIO from typing import Any, Optional, TextIO
from pathlib import Path from pathlib import Path
_LOG_DIR: Path = Path("./logs")
_LOG_DIR: Path = Path("./logs/sessions")
_SCRIPTS_DIR: Path = Path("./scripts/generated") _SCRIPTS_DIR: Path = Path("./scripts/generated")
_ts: str = "" # session timestamp string e.g. "20260301_142233" _ts: str = "" # session timestamp string e.g. "20260301_142233"
_session_id: str = "" # YYYYMMDD_HHMMSS[_Label] _session_id: str = "" # YYYYMMDD_HHMMSS[_Label]
_session_dir: Optional[Path] = None # Path to the sub-directory for this session _session_dir: Optional[Path] = None # Path to the sub-directory for this session
_seq: int = 0 # monotonic counter for script files this session _seq: int = 0 # monotonic counter for script files this session
_seq_lock: threading.Lock = threading.Lock() _seq_lock: threading.Lock = threading.Lock()
_comms_fh: Optional[TextIO] = None # file handle: logs/<session_id>/comms.log
_tool_fh: Optional[TextIO] = None # file handle: logs/<session_id>/toolcalls.log _comms_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/comms.log
_api_fh: Optional[TextIO] = None # file handle: logs/<session_id>/apihooks.log _tool_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/toolcalls.log
_cli_fh: Optional[TextIO] = None # file handle: logs/<session_id>/clicalls.log _api_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/apihooks.log
_cli_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/clicalls.log
def _now_ts() -> str: def _now_ts() -> str:
return datetime.datetime.now().strftime("%Y%m%d_%H%M%S") return datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -42,29 +48,35 @@ def open_session(label: Optional[str] = None) -> None:
global _ts, _session_id, _session_dir, _comms_fh, _tool_fh, _api_fh, _cli_fh, _seq global _ts, _session_id, _session_dir, _comms_fh, _tool_fh, _api_fh, _cli_fh, _seq
if _comms_fh is not None: if _comms_fh is not None:
return return
_ts = _now_ts() _ts = _now_ts()
_session_id = _ts _session_id = _ts
if label: if label:
safe_label = "".join(c if c.isalnum() or c in ("-", "_") else "_" for c in label) safe_label = "".join(c if c.isalnum() or c in ("-", "_") else "_" for c in label)
_session_id += f"_{safe_label}" _session_id += f"_{safe_label}"
_session_dir = _LOG_DIR / _session_id _session_dir = _LOG_DIR / _session_id
_session_dir.mkdir(parents=True, exist_ok=True) _session_dir.mkdir(parents=True, exist_ok=True)
_SCRIPTS_DIR.mkdir(parents=True, exist_ok=True) _SCRIPTS_DIR.mkdir(parents=True, exist_ok=True)
_seq = 0 _seq = 0
_comms_fh = open(_session_dir / "comms.log", "w", encoding="utf-8", buffering=1) _comms_fh = open(_session_dir / "comms.log", "w", encoding="utf-8", buffering=1)
_tool_fh = open(_session_dir / "toolcalls.log", "w", encoding="utf-8", buffering=1) _tool_fh = open(_session_dir / "toolcalls.log", "w", encoding="utf-8", buffering=1)
_api_fh = open(_session_dir / "apihooks.log", "w", encoding="utf-8", buffering=1) _api_fh = open(_session_dir / "apihooks.log", "w", encoding="utf-8", buffering=1)
_cli_fh = open(_session_dir / "clicalls.log", "w", encoding="utf-8", buffering=1) _cli_fh = open(_session_dir / "clicalls.log", "w", encoding="utf-8", buffering=1)
_tool_fh.write(f"# Tool-call log — session {_session_id}\n\n") _tool_fh.write(f"# Tool-call log — session {_session_id}\n\n")
_tool_fh.flush() _tool_fh.flush()
_cli_fh.write(f"# CLI Subprocess Call Log — session {_session_id}\n\n") _cli_fh.write(f"# CLI Subprocess Call Log — session {_session_id}\n\n")
_cli_fh.flush() _cli_fh.flush()
try: try:
from log_registry import LogRegistry from log_registry import LogRegistry
registry = LogRegistry(str(_LOG_DIR / "log_registry.toml")) registry = LogRegistry(str(_LOG_DIR / "log_registry.toml"))
registry.register_session(_session_id, str(_session_dir), datetime.datetime.now()) registry.register_session(_session_id, str(_session_dir), datetime.datetime.now())
except Exception as e: except Exception as e:
print(f"Warning: Could not register session in LogRegistry: {e}") print(f"Warning: Could not register session in LogRegistry: {e}")
atexit.register(close_session) atexit.register(close_session)
def close_session() -> None: def close_session() -> None:
@@ -72,6 +84,7 @@ def close_session() -> None:
global _comms_fh, _tool_fh, _api_fh, _cli_fh, _session_id, _LOG_DIR global _comms_fh, _tool_fh, _api_fh, _cli_fh, _session_id, _LOG_DIR
if _comms_fh is None: if _comms_fh is None:
return return
if _comms_fh: if _comms_fh:
_comms_fh.close() _comms_fh.close()
_comms_fh = None _comms_fh = None
@@ -84,6 +97,7 @@ def close_session() -> None:
if _cli_fh: if _cli_fh:
_cli_fh.close() _cli_fh.close()
_cli_fh = None _cli_fh = None
try: try:
from log_registry import LogRegistry from log_registry import LogRegistry
registry = LogRegistry(str(_LOG_DIR / "log_registry.toml")) registry = LogRegistry(str(_LOG_DIR / "log_registry.toml"))
@@ -122,17 +136,21 @@ def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optio
global _seq global _seq
if _tool_fh is None: if _tool_fh is None:
return script_path return script_path
with _seq_lock: with _seq_lock:
_seq += 1 _seq += 1
seq = _seq seq = _seq
ts_entry = datetime.datetime.now().strftime("%H:%M:%S") ts_entry = datetime.datetime.now().strftime("%H:%M:%S")
ps1_name = f"{_ts}_{seq:04d}.ps1" ps1_name = f"{_ts}_{seq:04d}.ps1"
ps1_path: Optional[Path] = _SCRIPTS_DIR / ps1_name ps1_path: Optional[Path] = _SCRIPTS_DIR / ps1_name
try: try:
ps1_path.write_text(script, encoding="utf-8") ps1_path.write_text(script, encoding="utf-8")
except Exception as exc: except Exception as exc:
ps1_path = None ps1_path = None
ps1_name = f"(write error: {exc})" ps1_name = f"(write error: {exc})"
try: try:
_tool_fh.write( _tool_fh.write(
f"## Call #{seq} [{ts_entry}]\n" f"## Call #{seq} [{ts_entry}]\n"
@@ -144,6 +162,7 @@ def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optio
_tool_fh.flush() _tool_fh.flush()
except Exception: except Exception:
pass pass
return str(ps1_path) if ps1_path else None return str(ps1_path) if ps1_path else None
def log_cli_call(command: str, stdin_content: Optional[str], stdout_content: Optional[str], stderr_content: Optional[str], latency: float) -> None: def log_cli_call(command: str, stdin_content: Optional[str], stdout_content: Optional[str], stderr_content: Optional[str], latency: float) -> None:

View File

@@ -26,7 +26,7 @@ class BaseSimulation:
self.client.click("btn_reset") self.client.click("btn_reset")
time.sleep(0.5) time.sleep(0.5)
git_dir = os.path.abspath(".") git_dir = os.path.abspath(".")
self.project_path = os.path.abspath(f"tests/temp_{project_name.lower()}.toml") self.project_path = os.path.abspath(f"tests/artifacts/temp_{project_name.lower()}.toml")
if os.path.exists(self.project_path): if os.path.exists(self.project_path):
os.remove(self.project_path) os.remove(self.project_path)
print(f"[BaseSim] Scaffolding Project: {project_name}") print(f"[BaseSim] Scaffolding Project: {project_name}")

View File

@@ -12,7 +12,7 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from api_hook_client import ApiHookClient from api_hook_client import ApiHookClient
# Define a temporary file path for callback testing # Define a temporary file path for callback testing
TEST_CALLBACK_FILE = Path("temp_callback_output.txt") TEST_CALLBACK_FILE = Path("tests/artifacts/temp_callback_output.txt")
@pytest.fixture(scope="function", autouse=True) @pytest.fixture(scope="function", autouse=True)
def cleanup_callback_file() -> None: def cleanup_callback_file() -> None:
@@ -75,3 +75,4 @@ def test_gui2_custom_callback_hook_works(live_gui: Any) -> None:
with open(TEST_CALLBACK_FILE, "r") as f: with open(TEST_CALLBACK_FILE, "r") as f:
content = f.read() content = f.read()
assert content == test_data, "Callback executed, but file content is incorrect." assert content == test_data, "Callback executed, but file content is incorrect."

View File

@@ -21,7 +21,7 @@ def test_full_live_workflow(live_gui) -> None:
client.click("btn_reset") client.click("btn_reset")
time.sleep(1) time.sleep(1)
# 2. Project Setup # 2. Project Setup
temp_project_path = os.path.abspath("tests/temp_project.toml") temp_project_path = os.path.abspath("tests/artifacts/temp_project.toml")
if os.path.exists(temp_project_path): if os.path.exists(temp_project_path):
os.remove(temp_project_path) os.remove(temp_project_path)
client.click("btn_project_new_automated", user_data=temp_project_path) client.click("btn_project_new_automated", user_data=temp_project_path)
@@ -74,3 +74,4 @@ def test_full_live_workflow(live_gui) -> None:
# Verify session is empty in new discussion # Verify session is empty in new discussion
session = client.get_session() session = client.get_session()
assert len(session.get('session', {}).get('entries', [])) == 0 assert len(session.get('session', {}).get('entries', [])) == 0

View File

@@ -27,4 +27,4 @@ def test_base_simulation_setup() -> None:
mock_client.wait_for_server.assert_called() mock_client.wait_for_server.assert_called()
mock_client.click.assert_any_call("btn_reset") mock_client.click.assert_any_call("btn_reset")
mock_sim.setup_new_project.assert_called() mock_sim.setup_new_project.assert_called()
assert sim.project_path.endswith("temp_testsim.toml") assert sim.project_path.endswith("tests/artifacts/temp_testsim.toml")

View File

@@ -24,7 +24,7 @@ def test_mma_complete_lifecycle(live_gui) -> None:
mock_cli_path = f'{sys.executable} {os.path.abspath("tests/mock_gemini_cli.py")}' mock_cli_path = f'{sys.executable} {os.path.abspath("tests/mock_gemini_cli.py")}'
client.set_value('gcli_path', mock_cli_path) client.set_value('gcli_path', mock_cli_path)
# Prevent polluting the real project directory with test tracks # Prevent polluting the real project directory with test tracks
client.set_value('files_base_dir', 'tests/temp_workspace') client.set_value('files_base_dir', 'tests/artifacts/temp_workspace')
client.click('btn_project_save') client.click('btn_project_save')
time.sleep(1) time.sleep(1)
except Exception as e: except Exception as e: