fix(learn/grok): detect a Windows absolute project path (#2283)
## 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 <mxjerrett@gmail.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user