fix: warn when populated legacy ~/.claude/homunculus exists (#2036)

After the XDG migration the CLI reads from
$XDG_DATA_HOME/ecc-homunculus/ (default ~/.local/share/ecc-homunculus/),
but installs that retained an older manual copy at ~/.claude/homunculus/
saw observations land in the new location while `instinct-cli.py status`
silently continued to look at the new directory only. The reporter
spent hours tracing the divergence because nothing surfaced the legacy
tree.

Add `_detect_legacy_homunculus_dir()` which returns the legacy path
when ~/.claude/homunculus/ contains real ECC state (projects/,
instincts/, evolved/, or observations.jsonl) and the active
HOMUNCULUS_DIR is different. `cmd_status` calls it at the bottom of
its output and prints a one-time, scoped warning that names both
paths and points at the existing migrate-homunculus.sh script.

Empty placeholder directories (just ~/.claude/homunculus/ with no
content) are ignored so users who have already migrated and left the
empty shell behind don't get nagged on every status call.

Tests cover three branches: populated legacy dir triggers the warning,
absent legacy dir stays silent, and empty placeholder legacy dir stays
silent.
This commit is contained in:
Matt Van Horn
2026-05-27 00:26:27 -07:00
committed by Affaan Mustafa
parent 3c32a017a2
commit 6048821a0f
2 changed files with 363 additions and 0 deletions

View File

@@ -67,6 +67,35 @@ def _ensure_global_dirs():
d.mkdir(parents=True, exist_ok=True)
def _detect_legacy_homunculus_dir() -> "Path | None":
"""Detect a populated legacy ~/.claude/homunculus tree that the current
XDG-based CLI does NOT read from.
Issue #2036: when ECC migrated to XDG_DATA_HOME (default
~/.local/share/ecc-homunculus/), users with an older manual install at
~/.claude/homunculus/ silently had observations written to the new path
while `instinct-cli.py status` continued to look at the old one. We warn
explicitly when both paths exist so the divergence is visible instead of
appearing as "system is broken".
"""
legacy = Path.home() / ".claude" / "homunculus"
if legacy == HOMUNCULUS_DIR:
return None
try:
if not legacy.is_dir():
return None
# Treat the directory as populated only if it contains real ECC
# state -- projects/, instincts/, evolved/, or observations.jsonl.
# An empty placeholder dir is not worth warning about.
for marker in ("projects", "instincts", "evolved", "observations.jsonl"):
target = legacy / marker
if target.exists():
return legacy
except OSError:
return None
return None
# ─────────────────────────────────────────────
# Path Validation
# ─────────────────────────────────────────────
@@ -460,6 +489,24 @@ def cmd_status(args) -> int:
days_left = max(0, PENDING_TTL_DAYS - item["age_days"])
print(f" - {item['name']} ({days_left}d remaining)")
# Issue #2036: legacy ~/.claude/homunculus is invisible to the current
# XDG-based CLI. Surface the divergence so users debugging "no instincts
# found" don't spend hours tracing path mismatches.
legacy_dir = _detect_legacy_homunculus_dir()
if legacy_dir is not None:
migrate_script = (
Path(__file__).resolve().parent / "migrate-homunculus.sh"
)
print(f"\n{'-'*60}")
print(f" WARNING: Legacy data found at {legacy_dir}")
print(f" The current CLI reads from {HOMUNCULUS_DIR}")
print( " and does NOT see anything under the legacy path.")
if migrate_script.exists():
print(f" Run: {migrate_script}")
else:
print(f" Migrate by moving {legacy_dir} into {HOMUNCULUS_DIR}")
print( " (or remove the legacy directory once you've confirmed it's empty).")
print(f"\n{'='*60}\n")
return 0