From cdc0f1405c752d4150d7e47ed1ebc7c41be09f57 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 2 Jul 2026 22:17:20 -0400 Subject: [PATCH] feat(directives): harvest 8 directives from feature_flags/RAG/cache/knowledge styleguides + 4 new styleguides --- .../directives/cache_stable_to_volatile/v1.md | 55 +++++++++++++ conductor/directives/chroma_cache_path/v1.md | 72 ++++++++++++++++ conductor/directives/config_state_owner/v1.md | 70 ++++++++++++++++ .../feature_flag_delete_to_turn_off/v1.md | 50 +++++++++++ .../knowledge_harvest_pattern/v1.md | 39 +++++++++ conductor/directives/rag_six_rules/v1.md | 19 +++++ conductor/directives/test_sandbox/v1.md | 49 +++++++++++ conductor/directives/workspace_paths/v1.md | 82 +++++++++++++++++++ 8 files changed, 436 insertions(+) create mode 100644 conductor/directives/cache_stable_to_volatile/v1.md create mode 100644 conductor/directives/chroma_cache_path/v1.md create mode 100644 conductor/directives/config_state_owner/v1.md create mode 100644 conductor/directives/feature_flag_delete_to_turn_off/v1.md create mode 100644 conductor/directives/knowledge_harvest_pattern/v1.md create mode 100644 conductor/directives/rag_six_rules/v1.md create mode 100644 conductor/directives/test_sandbox/v1.md create mode 100644 conductor/directives/workspace_paths/v1.md diff --git a/conductor/directives/cache_stable_to_volatile/v1.md b/conductor/directives/cache_stable_to_volatile/v1.md new file mode 100644 index 00000000..1f00a19f --- /dev/null +++ b/conductor/directives/cache_stable_to_volatile/v1.md @@ -0,0 +1,55 @@ +# cache_stable_to_volatile — v1 + +**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/cache_friendly_context.md` §"1. The 12-layer model" (lines 22-43) + §"2. The byte-comparison test" (lines 51-77). This is the baseline encoding — the layered-table-plus-contract-test style currently in production. +Future variants will test alternative encodings (rule-only, before/after) against this baseline. + +**Source:** `conductor/code_styleguides/cache_friendly_context.md:22-43 + 51-77` + +--- + +## 1. The 12-layer model (the stable-to-volatile ordering) + +| # | Layer | Stable across turns? | Source | SSDL | +|---|---|---|---|---| +| 1 | Role instructions (model + provider) | yes | `_get_combined_system_prompt` | `[I]` | +| 2 | Function-calling schema | yes | per provider | `[I]` | +| 3 | Discovered tool descriptions | yes | `mcp_client.get_tool_schemas()` | `[I]` | +| 4 | System prompt preset | yes | `app_state.ai_settings.system_prompt` | `[I]` | +| 5 | Persona profile | yes | `app_state.active_persona` | `[I]` | +| 6 | Project context (per `manual_slop.toml`) | yes | NEW (Candidate 14) | `[I]` | +| 7 | Knowledge digest (per `knowledge/digest.md`) | yes (within a gc cycle) | NEW (Candidate 8) | `[I]` | +| 8 | Discussion metadata (name, role count) | no (per turn) | `disc_entries[:1]` or `disc_meta` | `───` (data) | +| 9 | Active preset (FileItem set) | no (per turn) | `self.context_files` | `───` (data) | +| 10 | Per-file details (history, slices, notes) | no (per file) | per `FileItem` | `───` (data) | +| 11 | Tool-call results from prior turns | no (per turn) | per `_reread_file_items` | `───` (data) | +| 12 | The user message | no (per turn) | the input | `───` (data) | + +**The cache boundary is at layer 7/8.** Layers 1-7 are byte-identical across turns of the same discussion (and across discussions of the same mode). Layers 8-12 change per turn. + +--- + +## 2. The byte-comparison test (the design contract) + +The design rule "stable prefix is byte-identical" must be testable. The test: + +```python +# In tests/test_aggregate_caching.py (NEW) +def test_aggregate_stable_to_volatile_ordering(): + """The first N characters of the context should be identical across turns + of the same conversation, when no stable-layer inputs change.""" + ctrl = mock_app_controller() + ctrl.ai_settings.system_prompt = "Test system prompt" + ctrl.active_persona = mock_persona() + + # Turn 1 + turn1 = aggregate.build_initial_context(ctrl, user_message="first prompt") + + # Turn 2 (same stable inputs, different user message) + turn2 = aggregate.build_initial_context(ctrl, user_message="second prompt") + + # The first N characters should be identical (N = where the volatile layers start) + N = aggregate.stable_prefix_length(ctrl) + assert turn1[:N] == turn2[:N], f"Stable prefix mismatch: {turn1[:N]!r} != {turn2[:N]!r}" +``` + +**The test is the contract.** If a new layer is added in the middle of the stack, this test fails; the agent must either move the layer to the stable position or update the test (with written justification). \ No newline at end of file diff --git a/conductor/directives/chroma_cache_path/v1.md b/conductor/directives/chroma_cache_path/v1.md new file mode 100644 index 00000000..95722b74 --- /dev/null +++ b/conductor/directives/chroma_cache_path/v1.md @@ -0,0 +1,72 @@ +# chroma_cache_path — v1 + +**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/chroma_cache.md` §"The Rule" + §"The Pre-Cleanup Pattern" (lines 5-50). This is the baseline encoding — the rule-plus-rationale-plus-anti-patterns style currently in production. +Future variants will test alternative encodings (rule-only, before/after) against this baseline. + +**Source:** `conductor/code_styleguides/chroma_cache.md:5-50` + +--- + +# Chroma Cache Path Styleguide + +## The Rule + +The ChromaDB persistent vector cache lives at: + +``` +/tests/artifacts/.slop_cache/chroma_/ +``` + +**NOT** at the per-run `tests/artifacts/live_gui_workspace_/` subdir. + +Tests that interact with RAG **MUST** pre-clean the cache to avoid persistent state from prior tests in the batched run. + +## Why This Rule Exists + +The chroma cache path is auto-derived from `RAGEngine._init_vector_store()` (`src/rag_engine.py:108-125`): + +```python +db_path = os.path.abspath(os.path.join( + self.base_dir, ".slop_cache", f"chroma_{vs_config.collection_name}" +)) +``` + +`self.base_dir` is computed as `Path(active_project_path).parent`. **The trailing-slash bug**: when the test config produces a project path ending in `/` (e.g., from `os.path.join` with a trailing `/`), `Path(p).parent` returns the directory ONE LEVEL HIGHER than expected. So the chroma cache lands at `tests/artifacts/.slop_cache/` (the parent of the per-run `live_gui_workspace_/` subdir) instead of inside the per-run subdir. + +## The Pre-Cleanup Pattern + +RAG tests should wipe the chroma cache BEFORE pushing RAG config. The pattern is in `tests/test_rag_phase4_final_verify.py`: + +```python +from pathlib import Path +import shutil + +def test_phase4_final_verify(live_gui): + # Wipe any stale chroma from prior batched runs + cache = Path("tests/artifacts/.slop_cache/chroma_test_final_verify") + if cache.exists(): + shutil.rmtree(cache, ignore_errors=True) + # ... rest of test +``` + +`ignore_errors=True` is required because: +- On Windows, the chroma client may still hold file handles; `rmtree` may fail with `WinError 32` (sharing violation). +- If a parallel xdist worker is mid-write, the rmtree can race; `ignore_errors` lets the next worker's write retry. + +## Anti-Patterns + +❌ **Assuming the cache is per-run:** + +```python +def test_rag(live_gui, live_gui_workspace): + # WRONG: live_gui_workspace is a per-run subdir, but the chroma + # cache is at tests/artifacts/.slop_cache/, NOT under live_gui_workspace + cache = live_gui_workspace / ".slop_cache" / "chroma_test" + if cache.exists(): + shutil.rmtree(cache) # Doesn't find the actual cache +``` + +❌ **Not pre-cleaning at all:** +```python +def test_rag(live_gui): + # WRONG: no pre-cleanup. If a prior batched run with a different \ No newline at end of file diff --git a/conductor/directives/config_state_owner/v1.md b/conductor/directives/config_state_owner/v1.md new file mode 100644 index 00000000..dbc04adb --- /dev/null +++ b/conductor/directives/config_state_owner/v1.md @@ -0,0 +1,70 @@ +# config_state_owner — v1 + +**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/config_state_owner.md` (entire file, lines 1-82). This is the baseline encoding — the rule-plus-allowed/forbidden-lists style currently in production. +Future variants will test alternative encodings (rule-only, before/after) against this baseline. + +**Source:** `conductor/code_styleguides/config_state_owner.md:1-82` + +--- + +# Config I/O State Ownership + +**Rule:** The `AppController` is the single source of truth for the +in-memory config (`self.config`) and the only authorized caller of +the file I/O primitives (the config-load/save helpers, which moved out +of `src/models.py` to `src/project_manager.py` per +`module_taxonomy_refactor_20260627`). + +## Why + +1. **The controller owns the in-memory state.** If other modules + write to `config.toml` directly, the controller's `self.config` + silently drifts from disk. Tests can corrupt the user's TOML + files; users lose data without warning. +2. **Test isolation breaks.** When `models.save_config(...)` is + called from anywhere in `src/`, tests cannot intercept the + write without patching the I/O primitive. The test then + couples to the file format, not the controller's behavior. +3. **Path resolution can't be enforced.** The controller respects + `SLOP_CONFIG` env var at call time. Direct calls to + `models.save_config` would only respect it if the path is + re-resolved (which it is in `_save_config_to_disk`, but only + because someone remembered). + +## What is Forbidden in `src/` + +- `models.load_config(...)` (legacy public function) +- `models.save_config(...)` (legacy public function) +- `models._load_config_from_disk(...)` (private I/O primitive) +- `models._save_config_to_disk(...)` (private I/O primitive) + +The only allowed call sites are inside `AppController` itself +(`load_config()` and `save_config()` methods). + +## The Public API + +```python +# In AppController: +def load_config(self) -> Dict[str, Any]: + """Re-read the global config.toml from disk and update self.config.""" + self.config = models._load_config_from_disk() + return self.config + +def save_config(self) -> None: + """Flush self.config to disk.""" + models._save_config_to_disk(self.config) +``` + +Callers (including `gui_2.py`, `commands.py`, etc.) go through +the controller: + +```python +# In App class methods (gui_2.py): __getattr__ delegates to controller +self.save_config() # -> controller.save_config() +app.save_config() # -> controller.save_config() (via __getattr__) +app.load_config() # -> controller.load_config() (via __getattr__) + +# In AppController: +self.save_config() # direct +self.load_config() # direct +``` \ No newline at end of file diff --git a/conductor/directives/feature_flag_delete_to_turn_off/v1.md b/conductor/directives/feature_flag_delete_to_turn_off/v1.md new file mode 100644 index 00000000..4b9b14cf --- /dev/null +++ b/conductor/directives/feature_flag_delete_to_turn_off/v1.md @@ -0,0 +1,50 @@ +# feature_flag_delete_to_turn_off — v1 + +**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/feature_flags.md` §"0. The two patterns (the one-glance table)" (lines 13-21) + §1 + §2 + §3 (lines 22-143). This is the baseline encoding — the one-glance-table-plus-rationale style currently in production. +Future variants will test alternative encodings (rule-only, decision-tree) against this baseline. + +**Source:** `conductor/code_styleguides/feature_flags.md:13-143` + +--- + +## 0. The two patterns (the one-glance table) + +| Pattern | How it works | How to turn off | How to turn on | +|---|---|---|---| +| **File presence** | The feature checks for the file's existence; the file is the switch | `rm ` | Touch the file (or run the generator that creates it) | +| **Config flag** | The feature checks a setting in `[ai_settings.toml]` / `[manual_slop.toml]`; the GUI checkbox is the surface | Set `enabled = false` in the config; or uncheck the GUI box | Set `enabled = true`; or check the GUI box | +| **CLI flag** (a sub-pattern of config) | The CLI accepts a flag like `--no-cache`; the default behavior is "on" | Pass `--no-cache` on the CLI | Omit the flag (use the default) | +| **Feature flag in metadata** (a sub-pattern) | A `metadata.json` field for the feature's track declares `uses_rag: true` | Edit the metadata | Edit the metadata | + +## 1. When to use file presence (the "delete to turn off" pattern) + +**Use file presence when:** +- The feature generates a *side artifact* that the user might want to *turn off* by deleting the artifact +- The "off" state is *recoverable* — the artifact can be regenerated by running a command +- The user *expects* to be able to manage the feature via the filesystem (the user is on the command line; they know `rm`) +- The feature is *opt-in by default-off* (deleting the artifact means the feature is off; the absence of the file is the "off" state) + +**Examples in Manual Slop:** + +| Feature | The "on" state | The "off" state | The regeneration command | +|---|---|---|---| +| Knowledge digest injection | `~/.manual_slop/knowledge/digest.md` exists | File is deleted | `python -m src.knowledge_harvest --apply` | +| Per-file knowledge for file X | `~/.manual_slop/knowledge/files/{file_id}.md` exists | File is deleted | (the next harvest regenerates) | +| Saved conversations index | `~/.manual_slop/conversations/index-saved-conversations-*.json` exists | File is deleted | (n/a; user manually saves) | +| RAG index for project | `~/.manual_slop/.slop_cache/chroma_/` exists | Directory is deleted | `python -m src.rag_engine --rebuild-index` | +| Audit log | `~/.manual_slop/logs/sessions//comms.log` exists | File is deleted | (n/a; the log is auto-generated per turn) | + +**The principle (per the data-oriented foundation):** *the data is the thing*. If the feature produces a file, the file is the switch. Deleting the file is the natural way to turn off the feature. + +**The discovery surface:** the user can `ls ~/.manual_slop/knowledge/` and see `digest.md` (or not) and understand the state. + +**The ux surface:** the GUI shows the file state and provides a `[Delete to turn off]` button that does the same `rm` underneath. + +## 2. When to use config flags (the `[ai_settings.toml]` pattern) + +**Use config flags when:** +- The feature is *always on* by default; the flag is a way to *opt out* in special circumstances +- The "off" state is *not recoverable* by a single command (it's a persistent preference) +- The user *expects* to manage the feature via the GUI (they're not on the command line) +- The feature's behavior is *complex* (multiple settings, not just on/off) +- The setting is *user-specific* (different users might have different preferences) \ No newline at end of file diff --git a/conductor/directives/knowledge_harvest_pattern/v1.md b/conductor/directives/knowledge_harvest_pattern/v1.md new file mode 100644 index 00000000..0b81bdb3 --- /dev/null +++ b/conductor/directives/knowledge_harvest_pattern/v1.md @@ -0,0 +1,39 @@ +# knowledge_harvest_pattern — v1 + +**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/knowledge_artifacts.md` §"0. The one-glance directory layout" (lines 9-26) + §1 (lines 28-44). This is the baseline encoding — the directory-tree-plus-rationale style currently in production. +Future variants will test alternative encodings (rule-only, table-only) against this baseline. + +**Source:** `conductor/code_styleguides/knowledge_artifacts.md:9-44` + +--- + +## 0. The one-glance directory layout + +``` +~/.manual_slop/knowledge/ +├── facts.md # - {statement} {provenance} +├── decisions.md # - {statement, reason} {provenance} +├── questions.md # - {question} {provenance} +├── playbooks.md # - **{name}**: {steps} {provenance} +├── tasks.md # ## Open / ## Done +├── files/ +│ └── {file_id}.md # per-file notes (keyed by inode) +├── digest.md # bounded 4KB; the projection; "delete to turn off" +├── ledger.json # sha256-of-content audit log +└── prompts/ + └── harvest-conversation.md # user-editable harvest prompt +``` + +## 1. The category files (the source of truth) + +### 1.1 `facts.md` (durable statements) + +```markdown +# Facts + +- The MCP dispatch uses a flat if/elif chain. 4 places, 45 tools. [from: 2026-05-12-investigate-dispatch, 2026-05-12] +- ai_client.py has 5 separate per-provider history lists, each with their own lock. Switching providers mid-session loses history. [from: 2026-05-13-state-mutation-matrix, 2026-05-13] +- RAG is opt-in. Default-off in new projects. [from: 2026-06-12-rag-discipline, 2026-06-12] +``` + +**The shape:** `- {statement} {provenance}`. Plain markdown. Append-only. User-editable. \ No newline at end of file diff --git a/conductor/directives/rag_six_rules/v1.md b/conductor/directives/rag_six_rules/v1.md new file mode 100644 index 00000000..88da29b0 --- /dev/null +++ b/conductor/directives/rag_six_rules/v1.md @@ -0,0 +1,19 @@ +# rag_six_rules — v1 + +**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/rag_integration_discipline.md` §"0. The 6 rules (the one-glance table)" (lines 11-20). This is the baseline encoding — the one-glance-table-with-why-column style currently in production. +Future variants will test alternative encodings (rule-only, rationale-first) against this baseline. + +**Source:** `conductor/code_styleguides/rag_integration_discipline.md:11-20` + +--- + +## 0. The 6 rules (the one-glance table) + +| # | Rule | Why | +|---|---|---| +| 1 | RAG is **opt-in**. Default-off in new projects | Most features don't need it; the cost of unnecessary RAG is the embedding-provider round trip + the storage cost | +| 2 | RAG **complements**; it never **replaces** | Curation / Discussion / Knowledge are the durable, user-editable dimensions; RAG is the fuzzy, semantic search | +| 3 | RAG results display with **provenance** | The user needs to know which file and which chunk produced the result | +| 4 | RAG **never mutates state** | No auto-injection of RAG results into `disc_entries`; no auto-update of `FileItem`; no auto-write to disk | +| 5 | RAG integration is **feature-gated** | A feature must explicitly request RAG in its scope; RAG is not the default for "give me context" | +| 6 | RAG failure is **graceful** | A failed search returns `Result.empty` or an empty list; never crashes the request | \ No newline at end of file diff --git a/conductor/directives/test_sandbox/v1.md b/conductor/directives/test_sandbox/v1.md new file mode 100644 index 00000000..67b8a86f --- /dev/null +++ b/conductor/directives/test_sandbox/v1.md @@ -0,0 +1,49 @@ +# test_sandbox — v1 + +**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/test_sandbox.md` §"The 4-Layer Model" + §"The `--config` CLI Flag" + §"The `--basetemp` Rule" (lines 5-58). This is the baseline encoding — the layered-table-plus-mechanism style currently in production. +Future variants will test alternative encodings (rule-only, before/after) against this baseline. + +**Source:** `conductor/code_styleguides/test_sandbox.md:5-58` + +--- + +## The 4-Layer Model + +| Layer | Mechanism | Where | Default-on? | +|---|---|---|---| +| Layer 1 | Python runtime file-I/O guard (`sys.addaudithook`) | `tests/conftest.py:_sandbox_audit_hook` | Yes | +| Layer 2 | `isolate_workspace` autouse + `pyproject.toml --basetemp` | `tests/conftest.py` + `pyproject.toml` | Yes | +| Layer 3 | OS-level restricted-token PowerShell wrapper | `scripts/run_tests_sandboxed.ps1` | **Opt-in** | +| Layer 4 | Static audit script (CI gate) | `scripts/audit_test_sandbox_violations.py` | Yes (informational) / opt-in (`--strict`) | + +Layer 1 + Layer 2 + Layer 4 are file-presence-on = enabled (delete the relevant file to disable). Layer 3 requires explicit invocation. + +## The `--config` CLI Flag (replaces `SLOP_CONFIG`) + +The historical `SLOP_CONFIG` env var has been removed from `src/paths.py`. The CLI flag `--config ` is now the ONLY supported mechanism for overriding the default `/config.toml` location. + +```bash +# Use the default /config.toml +uv run python sloppy.py + +# Override +uv run python sloppy.py --config /path/to/your/config.toml +``` + +`sloppy.py` calls `paths.set_config_override(Path(args.config).resolve())` AFTER `parse_args()` and BEFORE any `from src.gui_2 import App` import. This is the only way to override the config path in production. + +## The `--basetemp` Rule + +`pyproject.toml` sets `addopts = "--basetemp=tests/artifacts/_pytest_tmp"`. This redirects pytest's `tmp_path` and `tmp_path_factory` fixtures (which default to `%TEMP%\pytest-of-\` on Windows) into `./tests/artifacts/`. This is what allows the Layer 1 allowlist to be a single rule: "anything under `./tests/` is allowed." + +## Layer 1 Audit Hook Contract + +`tests/conftest.py:_sandbox_audit_hook` is a `sys.addaudithook` callback. It fires on every `open()` call. Behavior: + +- **Reads** (mode `r`, `rb`): pass through, no check +- **Writes** (mode contains `w`, `a`, `x`, `+`): check path +- **Allowed** if path resolves under: + - `/tests/` + - Path contains `.pytest_cache`, `__pycache__`, `.coverage`, `.slop_cache`, or `.ruff_cache` + - Original path string starts with `\\.\` (Windows device namespace) or `/dev/` (Unix device namespace) +- **Blocked** otherwise: raises `RuntimeError("TEST_SANDBOX_VIOLATION: attempted to write to ...")` \ No newline at end of file diff --git a/conductor/directives/workspace_paths/v1.md b/conductor/directives/workspace_paths/v1.md new file mode 100644 index 00000000..d940bd50 --- /dev/null +++ b/conductor/directives/workspace_paths/v1.md @@ -0,0 +1,82 @@ +# workspace_paths — v1 + +**Why this iteration:** Lifted verbatim from `conductor/code_styleguides/workspace_paths.md` (lines 1-103). This is the baseline encoding — the rule-plus-forbidden-patterns-plus-rationale-history style currently in production. +Future variants will test alternative encodings (rule-only, tabular) against this baseline. + +**Source:** `conductor/code_styleguides/workspace_paths.md:1-103` + +--- + +# Test Workspace Paths — Hard Rule + +## TL;DR + +Test workspaces live in the project tree under `tests/artifacts/`. Conftest creates them. No env vars. No CLI args. No `tmp_path_factory`. No `%TEMP%`. No runner changes. **The user must be able to find every test workspace by looking in `tests/artifacts/`.** + +## The Rule + +When creating a test workspace, fixture, or scratch directory for any test infrastructure: + +```python +# CORRECT — conftest creates the path +from datetime import datetime +_RUN_ID = datetime.now().strftime("%Y%m%d_%H%M%S") +_RUN_WORKSPACE = Path(f"tests/artifacts/live_gui_workspace_{_RUN_ID}") + +@pytest.fixture(scope="session") +def live_gui(request): + temp_workspace = _RUN_WORKSPACE + ... +``` + +```python +# WRONG — env vars +import os +WORKSPACE = os.environ.get("LIVE_GUI_WORKSPACE", "tests/artifacts/live_gui_workspace") + +# WRONG — CLI args +def pytest_addoption(parser): + parser.addoption("--workspace", action="store", default="tests/artifacts/live_gui_workspace") + +# WRONG — tmp_path_factory (lives in %TEMP%, not in project tree) +def live_gui(request, tmp_path_factory): + temp_workspace = tmp_path_factory.mktemp("live_gui_workspace") + # Creates: C:\Users\\AppData\Local\Temp\pytest-of-\pytest-N\live_gui_workspace0 + # User CANNOT FIND THIS from the project tree. +``` + +## Why This Rule Exists + +This rule was added 2026-06-09 after a 4-day agent churn on workspace paths. The chain of decisions: + +1. Original conftest: `temp_workspace = Path("tests/artifacts/live_gui_workspace")`. Sims worked. User could find the workspace. **This was correct.** + +2. Phase 3 of test_infrastructure_hardening_20260609: agent changed it to `tmp_path_factory.mktemp("live_gui_workspace")`. The user did not catch this for 2 days. It moved the workspace to `%TEMP%/pytest-of-/...` which: + - The user cannot find from the project tree + - The sims (which compute `os.path.abspath("tests/artifacts/...")` from the project root) could not find the workspace either + - Caused `test_extended_sims.py::test_context_sim_live` to fail with "stale ui - ops disabled" because the sim's project path didn't match the controller's active_project_path + - The agent then spent 2 more days trying to fix the sim timing, the MMA state, the RAG state, the watchdog — none of which were the actual cause + +3. The user caught the regression. Their feedback: "we should be using a folder in `./tests/`" — i.e., the project tree, not the system temp dir. + +4. The agent tried `Path("tests/artifacts/live_gui_workspace")` (no timestamp). That solved the sim issue but was per-session, not per-run. Per-test pollution is desirable (it exposes fragility), so per-run isolation is what we want. + +5. The user pushed back on adding CLI args: "have conftest make it, conftest is the right place." The agent then tried env vars as an indirection layer. + +6. The user rejected env vars: "env vars are hidden global state, pass it to conftest directly." Conftest is the source of truth. + +7. Final solution: conftest creates a per-run timestamped folder under `tests/artifacts/`. One source of truth. No indirection. The user must be able to find every test workspace by looking in `tests/artifacts/`. + +## Forbidden Patterns (Hard Bans) + +### 1. `tmp_path_factory` for test infrastructure workspaces + +`tmp_path_factory` is for pytest's own test isolation (e.g., when a unit test needs a temp dir to write a file). It is **NOT** for test infrastructure workspaces (e.g., the `live_gui` subprocess's CWD). + +### 2. Environment variables for test paths + +Env vars are hidden global state. The user has explicitly banned them. + +### 3. CLI args for test paths + +The conftest is the right place. CLI args add a layer of indirection between the runner and the test, and they require the runner to be modified to pass them. \ No newline at end of file