From e240df2b698e601324b85956bd93cb304f6030ab Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 10:15:13 +0530 Subject: [PATCH] fix(learn/grok): detect a Windows absolute project path (#2283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description The Grok `learn` plugin can't detect a Windows project path, so on Windows it attributes every project's learnings to the wrong directory. `discover_projects` decodes the URL-encoded workspace directory name (which is the recorded absolute cwd) and decides whether it's absolute with a `startswith("/")` check: ```python decoded = unquote(workspace_dir.name) project_path = Path(decoded) if decoded.startswith("/") else Path.cwd() ``` A Windows absolute path (e.g. `C:\Users\me\proj`, URL-encoded as `C%3A%5CUsers%5Cme%5Cproj`) does not start with `/`, so the check fails and `project_path` silently falls back to `Path.cwd()`. The learnings are then attributed to whatever directory `headroom learn` happened to run in, and the plugin looks for `GROK.md` / `AGENTS.md` under the wrong path (so it never finds them). The rest of the codebase already handles Windows drive-letter paths: `memory/traffic_learner.py` guards with `ref.startswith("/") or (len(ref) > 2 and ref[1] == ":")`, and the Claude plugin has a full Windows-aware decode plus a session-cwd fallback. The Grok plugin's naive `startswith("/")` is the outlier. ## Fix Use `Path(decoded).is_absolute()`, which recognises both POSIX (`/...`) and Windows drive-letter (`C:\...`) absolute paths on their respective platforms: ```python decoded_path = Path(decoded) project_path = decoded_path if decoded_path.is_absolute() else Path.cwd() ``` On POSIX this is equivalent to the old check (no behavior change); on Windows the drive-letter path now resolves correctly instead of collapsing to cwd. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/learn/plugins/grok.py`: use `Path(decoded).is_absolute()` instead of `decoded.startswith("/")` in `discover_projects`. - `tests/test_learn_grok_plugin.py`: new test that an absolute workspace path resolves to that path (platform-aware: the Windows branch is the real guard, the POSIX branch confirms no regression). - `CHANGELOG.md`: Bug Fixes entry. ## Testing - [ ] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uvx ruff@0.15.17 check headroom/learn/plugins/grok.py tests/test_learn_grok_plugin.py All checks passed! $ uvx mypy@1.20.2 --ignore-missing-imports headroom/learn/plugins/grok.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: **Windows 11**, Python 3.12, `uvx ruff@0.15.17` / `uvx mypy@1.20.2`. This bug is Windows-specific, and I ran the proof on Windows where it actually reproduces. A full `pytest` OOM-kills this box (ML stack import), so I reproduced the decode+resolve with a dependency-free script and left the full pytest to CI. - Exact command / steps: took the URL-encoded workspace name `C%3A%5Cproj%5Capp`, decoded it, and ran it through the OLD `startswith("/")` and NEW `is_absolute()` resolution. Confirmed directly that `Path(r"C:\proj\app").is_absolute()` is `True` while `r"C:\proj\app".startswith("/")` is `False`. - Observed result: OLD → `cwd-fallback` (wrong); NEW → `C:\proj\app` (correct). A relative workspace name still falls back to cwd under both; a POSIX abs path resolves identically under both. - Not tested: a live Grok CLI history on Windows end-to-end; full local `pytest` deferred to CI (OOM). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes The "unit tests pass locally" box is unchecked because the full suite imports the ML stack, which I can't run here. Because the bug is Windows-specific and `Path.is_absolute()` is platform-dependent, the added test is platform-aware: on Windows (where I verified the fix) its drive-letter branch is the real regression guard; on the Linux CI runner it exercises the POSIX branch, confirming the change doesn't regress the existing behavior. The standalone proof above covers the Windows fix directly. --------- Co-authored-by: JerrettDavis --- headroom/learn/plugins/grok.py | 10 +++++++++- tests/test_learn_grok_plugin.py | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/headroom/learn/plugins/grok.py b/headroom/learn/plugins/grok.py index c274dcf29..898466126 100644 --- a/headroom/learn/plugins/grok.py +++ b/headroom/learn/plugins/grok.py @@ -58,7 +58,15 @@ class GrokPlugin(LearnPlugin, ConversationScanner): continue decoded = unquote(workspace_dir.name) - project_path = Path(decoded) if decoded.startswith("/") else Path.cwd() + # The workspace dir name is a URL-encoded absolute cwd. Use + # Path.is_absolute() rather than a `startswith("/")` check so a + # Windows drive-letter path (e.g. `C:\Users\...`) is recognised as + # absolute instead of silently falling back to cwd (which would + # attribute the learnings to the wrong project and miss its + # GROK.md/AGENTS.md). Mirrors the Windows-aware path handling in + # memory/traffic_learner.py. + decoded_path = Path(decoded) + project_path = decoded_path if decoded_path.is_absolute() else Path.cwd() agents_md = project_path / "AGENTS.md" grok_md = project_path / "GROK.md" diff --git a/tests/test_learn_grok_plugin.py b/tests/test_learn_grok_plugin.py index 3d774b5b7..2bab24cb2 100644 --- a/tests/test_learn_grok_plugin.py +++ b/tests/test_learn_grok_plugin.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import sys from pathlib import Path from headroom.learn.plugins.grok import GrokPlugin @@ -57,3 +58,26 @@ def test_grok_plugin_scans_tool_calls(tmp_path: Path) -> None: assert len(sessions) == 1 assert len(sessions[0].tool_calls) == 1 assert sessions[0].tool_calls[0].is_error is True + + +def test_grok_plugin_resolves_absolute_workspace_path(tmp_path: Path) -> None: + # The workspace dir name is a URL-encoded absolute cwd. It must resolve to + # that path, not fall back to the process cwd. The Windows branch is the + # real guard for the fix (a drive-letter path does not start with "/"); the + # POSIX branch confirms no regression. Detection uses Path.is_absolute(). + grok_dir = tmp_path / ".grok" + if sys.platform == "win32": + workspace = "C%3A%5Cproj%5Capp" + expected = Path(r"C:\proj\app") + else: + workspace = "%2Ftmp%2Fproj%2Fapp" + expected = Path("/tmp/proj/app") + session_dir = grok_dir / "sessions" / workspace / "session-1" + session_dir.mkdir(parents=True) + (session_dir / "updates.jsonl").write_text("{}\n", encoding="utf-8") + + plugin = GrokPlugin(grok_dir=grok_dir) + projects = plugin.discover_projects() + + assert len(projects) == 1 + assert projects[0].project_path == expected