From e4dfc1679bbd0e20f995b62616381c8b8fc19b50 Mon Sep 17 00:00:00 2001 From: xiaoxi <111292018+mapleLeafOfficial@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:26:22 +0800 Subject: [PATCH] fix: surface legacy data warning in instinct-cli status (#2127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: surface legacy data warning in instinct-cli status (#2036) When the data directory moved from ~/.claude/homunculus/ to the XDG-compliant ~/.local/share/ecc-homunculus/, legacy installs with data still in the old path saw "No instincts found" with no explanation. Add _warn_legacy_data() to cmd_status so users get a clear, actionable warning pointing them to the migration script or the CLV2_HOMUNCULUS_DIR override. Wrap the directory scan in try/except to handle permission errors gracefully. Closes #2036 Co-Authored-By: Claude Opus 4.7 * fix: address review feedback — drop unused f-strings, resolve absolute migrate path Remove extraneous f-prefix from strings without interpolation (ruff F541). Resolve migrate-homunculus.sh path relative to instinct-cli.py instead of hard-coding a repo-relative path that only works from the repo root. Co-Authored-By: Claude Opus 4.7 * fix: quote migrate script path to handle spaces Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: kky Co-authored-by: Claude Opus 4.7 --- .../scripts/instinct-cli.py | 37 ++++++++++++ tests/scripts/instinct-cli-projects.test.js | 59 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/skills/continuous-learning-v2/scripts/instinct-cli.py b/skills/continuous-learning-v2/scripts/instinct-cli.py index 710f4c69..8d87513a 100755 --- a/skills/continuous-learning-v2/scripts/instinct-cli.py +++ b/skills/continuous-learning-v2/scripts/instinct-cli.py @@ -703,10 +703,47 @@ def cmd_status(args) -> int: days_left = max(0, PENDING_TTL_DAYS - item["age_days"]) print(f" - {item['name']} ({days_left}d remaining)") + # Legacy data warning + _warn_legacy_data() + print(f"\n{'='*60}\n") return 0 +def _warn_legacy_data() -> None: + """Warn if legacy ~/.claude/homunculus/ contains data while the active + path has moved to the XDG directory.""" + legacy_dir = Path.home() / ".claude" / "homunculus" + if legacy_dir == HOMUNCULUS_DIR: + return # CLV2_HOMUNCULUS_DIR explicitly points at the legacy path + if not legacy_dir.is_dir(): + return + + # Count substantive files (skip empty dirs and the directory itself) + try: + legacy_files = [f for f in legacy_dir.rglob("*") if f.is_file()] + except (PermissionError, OSError): + print(f"\n Note: legacy directory exists but cannot be read: {legacy_dir}", file=sys.stderr) + return + if not legacy_files: + return + + migrate_script = Path(__file__).resolve().parent / "migrate-homunculus.sh" + + print(f"\n{'!'*60}") + print(" LEGACY DATA DETECTED") + print(f"{'!'*60}") + print(f" Found {len(legacy_files)} file(s) in legacy path:") + print(f" {legacy_dir}") + print(" Active data directory:") + print(f" {HOMUNCULUS_DIR}") + print() + print(" Run the migration script to move your data:") + print(f' bash "{migrate_script}"') + print(f" Or set CLV2_HOMUNCULUS_DIR={legacy_dir} to use the legacy path.") + print(f"{'!'*60}\n") + + def _print_instincts_by_domain(instincts: list[dict]) -> None: """Helper to print instincts grouped by domain.""" by_domain = defaultdict(list) diff --git a/tests/scripts/instinct-cli-projects.test.js b/tests/scripts/instinct-cli-projects.test.js index 3ebd8651..d2d95161 100644 --- a/tests/scripts/instinct-cli-projects.test.js +++ b/tests/scripts/instinct-cli-projects.test.js @@ -219,6 +219,65 @@ test('projects merge deduplicates instincts, appends observations, and removes s } }); +test('status warns when legacy ~/.claude/homunculus contains files', () => { + const root = createTempDir(); + try { + const legacyDir = path.join(root, 'home', '.claude', 'homunculus', 'instincts', 'personal'); + fs.mkdirSync(legacyDir, { recursive: true }); + fs.writeFileSync(path.join(legacyDir, 'old-instinct.yaml'), '---\nid: old\n---\nOld instinct.\n'); + + const result = runCli(root, ['status']); + assert.strictEqual(result.status, 0, result.stderr); + assert.match(result.stdout, /LEGACY DATA DETECTED/); + assert.match(result.stdout, /legacy path/i); + assert.match(result.stdout, /migration script/i); + } finally { + cleanupDir(root); + } +}); + +test('status does not warn when legacy dir is empty', () => { + const root = createTempDir(); + try { + const legacyDir = path.join(root, 'home', '.claude', 'homunculus'); + fs.mkdirSync(legacyDir, { recursive: true }); + + const result = runCli(root, ['status']); + assert.strictEqual(result.status, 0, result.stderr); + assert.doesNotMatch(result.stdout, /LEGACY DATA DETECTED/); + } finally { + cleanupDir(root); + } +}); + +test('status does not warn when no legacy dir exists', () => { + const root = createTempDir(); + try { + const result = runCli(root, ['status']); + assert.strictEqual(result.status, 0, result.stderr); + assert.doesNotMatch(result.stdout, /LEGACY DATA DETECTED/); + } finally { + cleanupDir(root); + } +}); + +test('status does not warn when CLV2_HOMUNCULUS_DIR points at legacy path', () => { + const root = createTempDir(); + try { + const legacyDir = path.join(root, 'home', '.claude', 'homunculus', 'instincts', 'personal'); + fs.mkdirSync(legacyDir, { recursive: true }); + fs.writeFileSync(path.join(legacyDir, 'active.yaml'), '---\nid: active\n---\nActive.\n'); + + const result = runCli(root, ['status'], { + env: { CLV2_HOMUNCULUS_DIR: path.join(root, 'home', '.claude', 'homunculus') }, + }); + assert.strictEqual(result.status, 0, result.stderr); + assert.doesNotMatch(result.stdout, /LEGACY DATA DETECTED/); + } finally { + cleanupDir(root); + } +}); + test('status migrates legacy no-remote linked worktree project dirs to main worktree id', () => { const root = createTempDir(); const repoParent = createTempDir();