fix(recovery): never write recovered logs into a git submodule
compute_output_path now parses .gitmodules and, for a project scope whose dir is a submodule (guru-rmm, guru-connect, youtube-sync-docker), falls back to the MAIN repo root session-logs/ per convention. Non-submodule projects (gururmm-agent, dataforth-dos) unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,71 @@ def _identity() -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
# Cache for parsed .gitmodules paths (keyed by repo root path string).
|
||||||
|
_SUBMODULE_PATHS_CACHE: dict[str, frozenset[str]] = {}
|
||||||
|
# Matches `path = projects/foo` lines in .gitmodules (leading whitespace, any
|
||||||
|
# spacing around `=`). The captured value is the repo-relative submodule path.
|
||||||
|
_GITMODULES_PATH_RE = re.compile(r"^\s*path\s*=\s*(.+?)\s*$", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def _submodule_paths() -> frozenset[str]:
|
||||||
|
"""Return the set of repo-relative submodule paths from ``.gitmodules``.
|
||||||
|
|
||||||
|
Parses the ``path = ...`` lines via a simple line scan (no git calls).
|
||||||
|
Paths are normalized to forward slashes with no trailing slash so they can
|
||||||
|
be compared against forward-slashed, repo-relative work paths. Robust to a
|
||||||
|
missing ``.gitmodules`` (returns an empty set -> no submodules). Cached per
|
||||||
|
repo root for the life of the process.
|
||||||
|
"""
|
||||||
|
root = repo_root()
|
||||||
|
key = str(root)
|
||||||
|
cached = _SUBMODULE_PATHS_CACHE.get(key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
paths: set[str] = set()
|
||||||
|
gitmodules = root / ".gitmodules"
|
||||||
|
try:
|
||||||
|
text = gitmodules.read_text(encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
text = ""
|
||||||
|
for m in _GITMODULES_PATH_RE.finditer(text):
|
||||||
|
rel = m.group(1).strip().replace("\\", "/").rstrip("/")
|
||||||
|
if rel:
|
||||||
|
paths.add(rel)
|
||||||
|
|
||||||
|
result = frozenset(paths)
|
||||||
|
_SUBMODULE_PATHS_CACHE[key] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _is_inside_submodule(target_dir: Path) -> bool:
|
||||||
|
"""True if ``target_dir`` is at or under any submodule path.
|
||||||
|
|
||||||
|
Comparison is done on repo-relative, forward-slashed path components so a
|
||||||
|
submodule ``projects/msp-tools/guru-rmm`` matches that directory and
|
||||||
|
anything beneath it (e.g. its ``session-logs/``), but does NOT match a
|
||||||
|
sibling like ``projects/msp-tools/guru-rmm-extra``.
|
||||||
|
"""
|
||||||
|
subs = _submodule_paths()
|
||||||
|
if not subs:
|
||||||
|
return False
|
||||||
|
root = repo_root()
|
||||||
|
try:
|
||||||
|
rel = target_dir.resolve().relative_to(root.resolve())
|
||||||
|
except (OSError, ValueError):
|
||||||
|
# target is not under the repo root (or cannot resolve) -> not a submodule.
|
||||||
|
return False
|
||||||
|
rel_parts = rel.parts
|
||||||
|
for sub in subs:
|
||||||
|
sub_parts = tuple(p for p in sub.split("/") if p)
|
||||||
|
if not sub_parts:
|
||||||
|
continue
|
||||||
|
if rel_parts[: len(sub_parts)] == sub_parts:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def transcript_base_dir() -> Path:
|
def transcript_base_dir() -> Path:
|
||||||
"""Compute ``~/.claude/projects/<slug>`` from identity's claudetools_root."""
|
"""Compute ``~/.claude/projects/<slug>`` from identity's claudetools_root."""
|
||||||
root = _identity().get("claudetools_root") or str(repo_root())
|
root = _identity().get("claudetools_root") or str(repo_root())
|
||||||
@@ -923,7 +988,14 @@ def compute_output_path(parsed: ParsedTranscript, scope: dict, title: str) -> Pa
|
|||||||
msp_dir = root / "projects" / "msp-tools" / slug
|
msp_dir = root / "projects" / "msp-tools" / slug
|
||||||
if msp_dir.exists():
|
if msp_dir.exists():
|
||||||
proj_dir = msp_dir
|
proj_dir = msp_dir
|
||||||
base = proj_dir / "session-logs"
|
# If the project dir is a git submodule (or inside one), its working
|
||||||
|
# tree must NOT be written to -- repo convention keeps those session
|
||||||
|
# logs in the MAIN repo root instead, and an unattended write would
|
||||||
|
# dirty the submodule. Fall back to the main root session-logs dir.
|
||||||
|
if _is_inside_submodule(proj_dir):
|
||||||
|
base = root / "session-logs"
|
||||||
|
else:
|
||||||
|
base = proj_dir / "session-logs"
|
||||||
else:
|
else:
|
||||||
base = root / "session-logs"
|
base = root / "session-logs"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user