From 7cb0cf0433d239a5226e9999acd37a99d2d26f80 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Sun, 31 May 2026 02:38:31 -0400 Subject: [PATCH] Revert "fix: warn when populated legacy ~/.claude/homunculus exists (#2036)" This reverts commit 6048821a0faa7737f91489d2692ef38e10dd0b1f. --- .../scripts/instinct-cli.py | 47 --- tests/scripts/instinct-cli-projects.test.js | 316 ------------------ 2 files changed, 363 deletions(-) delete mode 100644 tests/scripts/instinct-cli-projects.test.js diff --git a/skills/continuous-learning-v2/scripts/instinct-cli.py b/skills/continuous-learning-v2/scripts/instinct-cli.py index 1704c93a..22cfc968 100755 --- a/skills/continuous-learning-v2/scripts/instinct-cli.py +++ b/skills/continuous-learning-v2/scripts/instinct-cli.py @@ -67,35 +67,6 @@ 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 # ───────────────────────────────────────────── @@ -489,24 +460,6 @@ 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 diff --git a/tests/scripts/instinct-cli-projects.test.js b/tests/scripts/instinct-cli-projects.test.js deleted file mode 100644 index 41285a4f..00000000 --- a/tests/scripts/instinct-cli-projects.test.js +++ /dev/null @@ -1,316 +0,0 @@ -const assert = require('assert'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const crypto = require('crypto'); -const { spawnSync } = require('child_process'); - -let passed = 0; -let failed = 0; - -const repoRoot = path.resolve(__dirname, '..', '..'); -const cliPath = path.join( - repoRoot, - 'skills', - 'continuous-learning-v2', - 'scripts', - 'instinct-cli.py' -); - -function test(name, fn) { - try { - fn(); - console.log(` ✓ ${name}`); - passed += 1; - } catch (error) { - console.log(` ✗ ${name}`); - console.log(` Error: ${error.message}`); - failed += 1; - } -} - -function createTempDir() { - return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-instinct-cli-projects-')); -} - -function cleanupDir(dir) { - fs.rmSync(dir, { recursive: true, force: true }); -} - -function writeJson(filePath, payload) { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`); -} - -function readJson(filePath) { - return JSON.parse(fs.readFileSync(filePath, 'utf8')); -} - -function writeInstinct(filePath, id, confidence = 0.9) { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync( - filePath, - [ - '---', - `id: ${id}`, - 'trigger: "when repeated"', - `confidence: ${confidence}`, - 'domain: workflow', - '---', - '', - `Action for ${id}.`, - '', - ].join('\n') - ); -} - -function seedProject(root, id, options = {}) { - const projectDir = path.join(root, 'projects', id); - const personalDir = path.join(projectDir, 'instincts', 'personal'); - const inheritedDir = path.join(projectDir, 'instincts', 'inherited'); - fs.mkdirSync(personalDir, { recursive: true }); - fs.mkdirSync(inheritedDir, { recursive: true }); - - for (const instinct of options.personal || []) { - writeInstinct(path.join(personalDir, `${instinct}.yaml`), instinct); - } - for (const instinct of options.inherited || []) { - writeInstinct(path.join(inheritedDir, `${instinct}.yaml`), instinct); - } - if (options.observations) { - fs.writeFileSync( - path.join(projectDir, 'observations.jsonl'), - options.observations.map(row => JSON.stringify(row)).join('\n') + '\n' - ); - } - - return projectDir; -} - -function projectHash(value) { - return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12); -} - -function runGit(cwd, args) { - const result = spawnSync('git', args, { - cwd, - encoding: 'utf8', - }); - assert.strictEqual(result.status, 0, result.stderr); - return result.stdout.trim(); -} - -function runCli(root, args, options = {}) { - return spawnSync('python3', [cliPath, ...args], { - cwd: options.cwd || repoRoot, - encoding: 'utf8', - env: { - ...process.env, - CLV2_HOMUNCULUS_DIR: root, - HOME: path.join(root, 'home'), - USERPROFILE: path.join(root, 'home'), - CLAUDE_PROJECT_DIR: '', - ...(options.env || {}), - }, - }); -} - -console.log('\n=== Testing instinct-cli.py projects maintenance ===\n'); - -test('projects delete --dry-run preserves registry and project files', () => { - const root = createTempDir(); - try { - const registryPath = path.join(root, 'projects.json'); - seedProject(root, 'alpha123', { - personal: ['keep-me'], - observations: [{ event: 'tool_complete' }], - }); - writeJson(registryPath, { - alpha123: { name: 'alpha', root: '/repo/alpha', remote: '', last_seen: '2026-01-01T00:00:00Z' }, - }); - - const result = runCli(root, ['projects', 'delete', 'alpha123', '--dry-run']); - assert.strictEqual(result.status, 0, result.stderr); - assert.match(result.stdout, /would delete/i); - assert.ok(fs.existsSync(path.join(root, 'projects', 'alpha123'))); - assert.ok(readJson(registryPath).alpha123); - } finally { - cleanupDir(root); - } -}); - -test('projects delete --force removes registry entry and project directory', () => { - const root = createTempDir(); - try { - const registryPath = path.join(root, 'projects.json'); - seedProject(root, 'alpha123', { personal: ['delete-me'] }); - writeJson(registryPath, { - alpha123: { name: 'alpha', root: '/repo/alpha', remote: '', last_seen: '2026-01-01T00:00:00Z' }, - }); - - const result = runCli(root, ['projects', 'delete', 'alpha123', '--force']); - assert.strictEqual(result.status, 0, result.stderr); - assert.ok(!fs.existsSync(path.join(root, 'projects', 'alpha123'))); - assert.ok(!readJson(registryPath).alpha123); - } finally { - cleanupDir(root); - } -}); - -test('projects gc --force removes only zero-value project entries', () => { - const root = createTempDir(); - try { - const registryPath = path.join(root, 'projects.json'); - seedProject(root, 'empty000'); - seedProject(root, 'active999', { personal: ['active'] }); - writeJson(registryPath, { - empty000: { name: 'empty', root: '/tmp/empty', remote: '', last_seen: '2026-01-01T00:00:00Z' }, - active999: { name: 'active', root: '/repo/active', remote: '', last_seen: '2026-01-02T00:00:00Z' }, - }); - - const result = runCli(root, ['projects', 'gc', '--force']); - assert.strictEqual(result.status, 0, result.stderr); - const registry = readJson(registryPath); - assert.ok(!registry.empty000); - assert.ok(registry.active999); - assert.ok(!fs.existsSync(path.join(root, 'projects', 'empty000'))); - assert.ok(fs.existsSync(path.join(root, 'projects', 'active999'))); - } finally { - cleanupDir(root); - } -}); - -test('projects merge deduplicates instincts, appends observations, and removes source', () => { - const root = createTempDir(); - try { - const registryPath = path.join(root, 'projects.json'); - seedProject(root, 'from111', { - personal: ['shared', 'from-only'], - observations: [{ event: 'from-event' }], - }); - seedProject(root, 'into222', { - personal: ['shared', 'into-only'], - observations: [{ event: 'into-event' }], - }); - writeJson(registryPath, { - from111: { name: 'from', root: '/repo/from', remote: '', last_seen: '2026-01-01T00:00:00Z' }, - into222: { name: 'into', root: '/repo/into', remote: '', last_seen: '2026-01-02T00:00:00Z' }, - }); - - const result = runCli(root, ['projects', 'merge', 'from111', 'into222', '--force']); - assert.strictEqual(result.status, 0, result.stderr); - assert.ok(!fs.existsSync(path.join(root, 'projects', 'from111'))); - assert.ok(!readJson(registryPath).from111); - assert.ok(readJson(registryPath).into222); - - const intoPersonal = path.join(root, 'projects', 'into222', 'instincts', 'personal'); - assert.ok(fs.existsSync(path.join(intoPersonal, 'shared.yaml'))); - assert.ok(fs.existsSync(path.join(intoPersonal, 'from-only.yaml'))); - assert.ok(fs.existsSync(path.join(intoPersonal, 'into-only.yaml'))); - - const observations = fs.readFileSync( - path.join(root, 'projects', 'into222', 'observations.jsonl'), - 'utf8' - ); - assert.match(observations, /from-event/); - assert.match(observations, /into-event/); - } finally { - cleanupDir(root); - } -}); - -test('status migrates legacy no-remote linked worktree project dirs to main worktree id', () => { - const root = createTempDir(); - const repoParent = createTempDir(); - try { - const mainWorktree = path.join(repoParent, 'main'); - const linkedWorktree = path.join(repoParent, 'linked'); - fs.mkdirSync(mainWorktree, { recursive: true }); - runGit(mainWorktree, ['init']); - runGit(mainWorktree, ['config', 'user.email', 'ecc@example.test']); - runGit(mainWorktree, ['config', 'user.name', 'ECC Test']); - fs.writeFileSync(path.join(mainWorktree, 'README.md'), 'test\n'); - runGit(mainWorktree, ['add', 'README.md']); - runGit(mainWorktree, ['commit', '-m', 'init']); - runGit(mainWorktree, ['worktree', 'add', linkedWorktree]); - - const mainRoot = runGit(mainWorktree, ['rev-parse', '--show-toplevel']); - const linkedRoot = runGit(linkedWorktree, ['rev-parse', '--show-toplevel']); - const oldLinkedId = projectHash(linkedRoot); - const mainId = projectHash(mainRoot); - seedProject(root, oldLinkedId, { personal: ['legacy-worktree'] }); - - const result = runCli(root, ['status'], { cwd: linkedRoot }); - assert.strictEqual(result.status, 0, result.stderr); - assert.ok(!fs.existsSync(path.join(root, 'projects', oldLinkedId))); - assert.ok(fs.existsSync(path.join(root, 'projects', mainId))); - assert.ok( - fs.existsSync(path.join(root, 'projects', mainId, 'instincts', 'personal', 'legacy-worktree.yaml')) - ); - assert.match(result.stdout, new RegExp(`\\(${mainId}\\)`)); - } finally { - cleanupDir(root); - cleanupDir(repoParent); - } -}); - -test('status warns when populated legacy ~/.claude/homunculus exists alongside XDG path (#2036)', () => { - const root = createTempDir(); - try { - const home = path.join(root, 'home'); - const legacyHomunculus = path.join(home, '.claude', 'homunculus', 'projects'); - fs.mkdirSync(legacyHomunculus, { recursive: true }); - // Drop a sentinel project dir so the helper sees a populated tree. - fs.mkdirSync(path.join(legacyHomunculus, 'deadbeef'), { recursive: true }); - - const result = runCli(root, ['status']); - assert.strictEqual(result.status, 0, result.stderr); - assert.match( - result.stdout, - /Legacy data found at .*\.claude\/homunculus/, - 'status should warn about the populated legacy directory' - ); - } finally { - cleanupDir(root); - } -}); - -test('status stays quiet when no legacy ~/.claude/homunculus exists (#2036)', () => { - const root = createTempDir(); - try { - const home = path.join(root, 'home'); - fs.mkdirSync(home, { recursive: true }); - const result = runCli(root, ['status']); - assert.strictEqual(result.status, 0, result.stderr); - assert.doesNotMatch( - result.stdout, - /Legacy data found/, - 'status should not warn when no legacy directory exists' - ); - } finally { - cleanupDir(root); - } -}); - -test('status stays quiet when legacy ~/.claude/homunculus exists but is empty (#2036)', () => { - const root = createTempDir(); - try { - const home = path.join(root, 'home'); - const legacy = path.join(home, '.claude', 'homunculus'); - fs.mkdirSync(legacy, { recursive: true }); - const result = runCli(root, ['status']); - assert.strictEqual(result.status, 0, result.stderr); - assert.doesNotMatch( - result.stdout, - /Legacy data found/, - 'empty placeholder legacy directory should not trigger the warning' - ); - } finally { - cleanupDir(root); - } -}); - -console.log(`\nPassed: ${passed}`); -console.log(`Failed: ${failed}`); - -process.exit(failed > 0 ? 1 : 0);