From 8219a5410d49bfb8e64b86824321dbc4af1d39e7 Mon Sep 17 00:00:00 2001 From: Divyam Talwar Date: Tue, 22 Sep 2026 04:39:43 +0530 Subject: [PATCH 1/2] Prevent Pi installer from deleting user skills The Pi installer removes a legacy skill path during install and uninstall, but that path can belong to the user rather than Hivemind. Preserve unowned paths while retaining cleanup for the historical installer state. Constraint: Preserve historical cleanup for paths carrying the old Hivemind version sentinel Rejected: Always preserve the path | leaves stale Hivemind legacy files behind Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep the preservation marker tied to the installer state before changing legacy cleanup again Tested: cli-install-pi-fs and install-prune Vitest suites, TypeScript noEmit Not-tested: Windows-specific filesystem semantics --- src/cli/install-pi.ts | 36 ++++++++++++++++++++++++----- tests/cli/cli-install-pi-fs.test.ts | 16 +++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/cli/install-pi.ts b/src/cli/install-pi.ts index bcfe6acdd..c68114bfd 100644 --- a/src/cli/install-pi.ts +++ b/src/cli/install-pi.ts @@ -1,6 +1,6 @@ import { existsSync, writeFileSync, rmSync, readFileSync, copyFileSync } from "node:fs"; import { join } from "node:path"; -import { HOME, pkgRoot, ensureDir, syncDir, reportPruned, writeVersionStamp, log } from "./util.js"; +import { HOME, pkgRoot, ensureDir, syncDir, reportPruned, writeVersionStamp, log, isLink, warn } from "./util.js"; import { getVersion } from "./version.js"; import { upsertMarkedBlock, @@ -38,6 +38,7 @@ const LEGACY_SKILL_DIR = join(PI_AGENT_DIR, "skills", "hivemind-memory"); const EXTENSIONS_DIR = join(PI_AGENT_DIR, "extensions"); const EXTENSION_PATH = join(EXTENSIONS_DIR, "hivemind.ts"); const VERSION_DIR = join(PI_AGENT_DIR, ".hivemind"); +const LEGACY_SKILL_PRESERVED = join(VERSION_DIR, ".legacy_skill_preserved"); // Worker bundles the extension spawns (wiki-worker shells `pi --print` for // the AI summary; skillify / autopull / skillopt / notifications workers are // the shared modules pi cannot import as raw .ts). CC/codex/cursor/hermes @@ -45,6 +46,26 @@ const VERSION_DIR = join(PI_AGENT_DIR, ".hivemind"); // they are installed as a sibling dir of the extension. const WIKI_WORKER_DIR = join(PI_AGENT_DIR, "hivemind"); +function legacySkillIsHivemindOwned(): boolean { + // The old installer always wrote this sentinel alongside its legacy skill. + // Once this installer sees a user-owned path, the preservation marker keeps + // later uninstall calls from mistaking the package version stamp for + // ownership of that path. + return existsSync(LEGACY_SKILL_DIR) && + !isLink(LEGACY_SKILL_DIR) && + !existsSync(LEGACY_SKILL_PRESERVED) && + existsSync(join(VERSION_DIR, ".hivemind_version")); +} + +function removeOwnedLegacySkill(): void { + if (!existsSync(LEGACY_SKILL_DIR)) return; + if (!legacySkillIsHivemindOwned()) { + warn(` pi preserving unowned legacy skill at ${LEGACY_SKILL_DIR}`); + return; + } + rmSync(LEGACY_SKILL_DIR, { recursive: true, force: true }); +} + const HIVEMIND_BLOCK_BODY = `${HIVEMIND_BLOCK_START} ## Hivemind Memory @@ -77,9 +98,7 @@ export function installPi(): void { // Clean up any per-agent SKILL.md left by an older installer — pi reads // skills from both ~/.pi/agent/skills/ and ~/.agents/skills/, so a local // drop collides with the codex installer's shared agentskills.io symlink. - if (existsSync(LEGACY_SKILL_DIR)) { - rmSync(LEGACY_SKILL_DIR, { recursive: true, force: true }); - } + removeOwnedLegacySkill(); // 1. AGENTS.md hivemind block (idempotent upsert). Pi auto-loads this every turn. const prior = existsSync(AGENTS_MD) ? readFileSync(AGENTS_MD, "utf-8") : null; @@ -105,6 +124,7 @@ export function installPi(): void { ensureDir(VERSION_DIR); writeVersionStamp(VERSION_DIR, getVersion()); + writeFileSync(LEGACY_SKILL_PRESERVED, "The current installer does not own ~/.pi/agent/skills/hivemind-memory.\n"); log(` pi AGENTS.md updated -> ${AGENTS_MD}`); log(` pi extension installed -> ${EXTENSION_PATH}`); @@ -115,8 +135,12 @@ export function installPi(): void { export function uninstallPi(): void { if (existsSync(LEGACY_SKILL_DIR)) { - rmSync(LEGACY_SKILL_DIR, { recursive: true, force: true }); - log(` pi removed ${LEGACY_SKILL_DIR}`); + if (!legacySkillIsHivemindOwned()) { + warn(` pi preserving unowned legacy skill at ${LEGACY_SKILL_DIR}`); + } else { + rmSync(LEGACY_SKILL_DIR, { recursive: true, force: true }); + log(` pi removed ${LEGACY_SKILL_DIR}`); + } } if (existsSync(EXTENSION_PATH)) { rmSync(EXTENSION_PATH, { force: true }); diff --git a/tests/cli/cli-install-pi-fs.test.ts b/tests/cli/cli-install-pi-fs.test.ts index 3bb6ed794..0bf8fa9be 100644 --- a/tests/cli/cli-install-pi-fs.test.ts +++ b/tests/cli/cli-install-pi-fs.test.ts @@ -112,10 +112,24 @@ describe("installPi — re-install / cleanup", () => { const legacy = join(tmpHome, ".pi", "agent", "skills", "hivemind-memory"); mkdirSync(legacy, { recursive: true }); writeFileSync(join(legacy, "SKILL.md"), "stale skill body"); + mkdirSync(join(tmpHome, ".pi", "agent", ".hivemind"), { recursive: true }); + writeFileSync(join(tmpHome, ".pi", "agent", ".hivemind", ".hivemind_version"), "6.0.0\n"); const { installPi } = await importInstaller(); installPi(); expect(existsSync(legacy)).toBe(false); }); + + it("preserves a same-named user skill when no Hivemind ownership marker exists", async () => { + const legacy = join(tmpHome, ".pi", "agent", "skills", "hivemind-memory"); + mkdirSync(legacy, { recursive: true }); + writeFileSync(join(legacy, "SKILL.md"), "user-authored skill"); + const { installPi, uninstallPi } = await importInstaller(); + installPi(); + expect(readFileSync(join(legacy, "SKILL.md"), "utf-8")).toBe("user-authored skill"); + + uninstallPi(); + expect(readFileSync(join(legacy, "SKILL.md"), "utf-8")).toBe("user-authored skill"); + }); }); describe("uninstallPi", () => { @@ -154,6 +168,8 @@ describe("uninstallPi", () => { const legacy = join(tmpHome, ".pi", "agent", "skills", "hivemind-memory"); mkdirSync(legacy, { recursive: true }); writeFileSync(join(legacy, "SKILL.md"), "stale"); + mkdirSync(join(tmpHome, ".pi", "agent", ".hivemind"), { recursive: true }); + writeFileSync(join(tmpHome, ".pi", "agent", ".hivemind", ".hivemind_version"), "6.0.0\n"); const { uninstallPi } = await importInstaller(); uninstallPi(); expect(existsSync(legacy)).toBe(false); From 38bf3bb9b3ed7377451dac6ed1179817c8e1e647 Mon Sep 17 00:00:00 2001 From: Divyam Talwar Date: Tue, 22 Sep 2026 05:18:09 +0530 Subject: [PATCH 2/2] Require local proof before removing legacy Pi skills A Pi-wide version stamp says only that Hivemind was installed; it does not establish ownership of a same-named skill directory. Restrict migration to the two exact historical skill payloads, and refuse recursive removal when the path has changed shape or acquired user content. Constraint: Preserve cleanup of verified, unmodified legacy Pi skill artifacts without deleting unknown files. Rejected: Treat ~/.pi/agent/.hivemind/.hivemind_version as ownership | the stamp is global to the Pi integration, not provenance for the skill path. Rejected: Add a preservation marker after inspection | that would label unknown content instead of proving its origin. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Do not broaden legacy cleanup beyond exact target-local provenance or historically shipped bytes. Tested: 113 adjacent installer tests, TypeScript noEmit, full TypeScript/esbuild build on Node 22.22.0. Not-tested: Filesystem mutation racing between verification and unlink. --- src/cli/install-pi.ts | 113 ++++++++++++++++++------ tests/cli/cli-install-pi-fs.test.ts | 129 ++++++++++++++++++++++++---- 2 files changed, 200 insertions(+), 42 deletions(-) diff --git a/src/cli/install-pi.ts b/src/cli/install-pi.ts index c68114bfd..051b29c8c 100644 --- a/src/cli/install-pi.ts +++ b/src/cli/install-pi.ts @@ -1,6 +1,17 @@ -import { existsSync, writeFileSync, rmSync, readFileSync, copyFileSync } from "node:fs"; +import { + copyFileSync, + existsSync, + lstatSync, + readFileSync, + readdirSync, + rmSync, + rmdirSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import { createHash } from "node:crypto"; import { join } from "node:path"; -import { HOME, pkgRoot, ensureDir, syncDir, reportPruned, writeVersionStamp, log, isLink, warn } from "./util.js"; +import { HOME, pkgRoot, ensureDir, syncDir, reportPruned, writeVersionStamp, log, warn } from "./util.js"; import { getVersion } from "./version.js"; import { upsertMarkedBlock, @@ -38,7 +49,14 @@ const LEGACY_SKILL_DIR = join(PI_AGENT_DIR, "skills", "hivemind-memory"); const EXTENSIONS_DIR = join(PI_AGENT_DIR, "extensions"); const EXTENSION_PATH = join(EXTENSIONS_DIR, "hivemind.ts"); const VERSION_DIR = join(PI_AGENT_DIR, ".hivemind"); -const LEGACY_SKILL_PRESERVED = join(VERSION_DIR, ".legacy_skill_preserved"); +const LEGACY_SKILL_PATH = join(LEGACY_SKILL_DIR, "SKILL.md"); +// Exact SHA-256 digests of the two SKILL.md bodies the historical Pi +// installer shipped (3b7d8a01 and 5ba761c0). The Pi-wide version stamp is +// not provenance for this particular path. +const LEGACY_SKILL_DIGESTS = new Set([ + "124e4881db8e8c7e78073c69fb7cde6969990792f20f436ee13eb6ef54fa8282", + "a3b8ffe6769096ff2cfc3d89049e66cd948de80ba2b2f0ba57da268ec6b3b5c2", +]); // Worker bundles the extension spawns (wiki-worker shells `pi --print` for // the AI summary; skillify / autopull / skillopt / notifications workers are // the shared modules pi cannot import as raw .ts). CC/codex/cursor/hermes @@ -46,24 +64,71 @@ const LEGACY_SKILL_PRESERVED = join(VERSION_DIR, ".legacy_skill_preserved"); // they are installed as a sibling dir of the extension. const WIKI_WORKER_DIR = join(PI_AGENT_DIR, "hivemind"); -function legacySkillIsHivemindOwned(): boolean { - // The old installer always wrote this sentinel alongside its legacy skill. - // Once this installer sees a user-owned path, the preservation marker keeps - // later uninstall calls from mistaking the package version stamp for - // ownership of that path. - return existsSync(LEGACY_SKILL_DIR) && - !isLink(LEGACY_SKILL_DIR) && - !existsSync(LEGACY_SKILL_PRESERVED) && - existsSync(join(VERSION_DIR, ".hivemind_version")); +type LegacySkillInspection = + | { state: "absent" } + | { state: "generated" } + | { state: "preserve"; reason: string }; + +function inspectLegacySkill(): LegacySkillInspection { + let dirStat; + try { + dirStat = lstatSync(LEGACY_SKILL_DIR); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return { state: "absent" }; + return { state: "preserve", reason: "the path could not be inspected" }; + } + + if (dirStat.isSymbolicLink()) { + return { state: "preserve", reason: "the path is a symlink" }; + } + if (!dirStat.isDirectory()) { + return { state: "preserve", reason: "the path is not a directory" }; + } + + let entries; + try { + entries = readdirSync(LEGACY_SKILL_DIR, { withFileTypes: true }); + } catch { + return { state: "preserve", reason: "the directory could not be inspected" }; + } + if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) { + return { state: "preserve", reason: "the directory has unverified contents" }; + } + + try { + const digest = createHash("sha256").update(readFileSync(LEGACY_SKILL_PATH)).digest("hex"); + return LEGACY_SKILL_DIGESTS.has(digest) + ? { state: "generated" } + : { state: "preserve", reason: "SKILL.md does not match a shipped legacy artifact" }; + } catch { + return { state: "preserve", reason: "SKILL.md could not be inspected" }; + } } -function removeOwnedLegacySkill(): void { - if (!existsSync(LEGACY_SKILL_DIR)) return; - if (!legacySkillIsHivemindOwned()) { - warn(` pi preserving unowned legacy skill at ${LEGACY_SKILL_DIR}`); - return; +function removeGeneratedLegacySkill(): boolean { + const inspection = inspectLegacySkill(); + if (inspection.state === "absent") return false; + if (inspection.state === "preserve") { + warn( + ` pi preserving legacy skill at ${LEGACY_SKILL_DIR}: ${inspection.reason}; ` + + "remove it manually if it is obsolete", + ); + return false; + } + + try { + // Avoid recursive deletion: the verified historical directory contained + // only this file, and rmdir will refuse to remove a now-nonempty directory. + unlinkSync(LEGACY_SKILL_PATH); + rmdirSync(LEGACY_SKILL_DIR); + return true; + } catch { + warn( + ` pi legacy skill cleanup stopped at ${LEGACY_SKILL_DIR}; ` + + "inspect the path and remove it manually if it is obsolete", + ); + return false; } - rmSync(LEGACY_SKILL_DIR, { recursive: true, force: true }); } const HIVEMIND_BLOCK_BODY = `${HIVEMIND_BLOCK_START} @@ -98,7 +163,7 @@ export function installPi(): void { // Clean up any per-agent SKILL.md left by an older installer — pi reads // skills from both ~/.pi/agent/skills/ and ~/.agents/skills/, so a local // drop collides with the codex installer's shared agentskills.io symlink. - removeOwnedLegacySkill(); + removeGeneratedLegacySkill(); // 1. AGENTS.md hivemind block (idempotent upsert). Pi auto-loads this every turn. const prior = existsSync(AGENTS_MD) ? readFileSync(AGENTS_MD, "utf-8") : null; @@ -124,7 +189,6 @@ export function installPi(): void { ensureDir(VERSION_DIR); writeVersionStamp(VERSION_DIR, getVersion()); - writeFileSync(LEGACY_SKILL_PRESERVED, "The current installer does not own ~/.pi/agent/skills/hivemind-memory.\n"); log(` pi AGENTS.md updated -> ${AGENTS_MD}`); log(` pi extension installed -> ${EXTENSION_PATH}`); @@ -134,13 +198,8 @@ export function installPi(): void { } export function uninstallPi(): void { - if (existsSync(LEGACY_SKILL_DIR)) { - if (!legacySkillIsHivemindOwned()) { - warn(` pi preserving unowned legacy skill at ${LEGACY_SKILL_DIR}`); - } else { - rmSync(LEGACY_SKILL_DIR, { recursive: true, force: true }); - log(` pi removed ${LEGACY_SKILL_DIR}`); - } + if (removeGeneratedLegacySkill()) { + log(` pi removed ${LEGACY_SKILL_DIR}`); } if (existsSync(EXTENSION_PATH)) { rmSync(EXTENSION_PATH, { force: true }); diff --git a/tests/cli/cli-install-pi-fs.test.ts b/tests/cli/cli-install-pi-fs.test.ts index 0bf8fa9be..250cd3615 100644 --- a/tests/cli/cli-install-pi-fs.test.ts +++ b/tests/cli/cli-install-pi-fs.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from "node:fs"; +import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { setFakeHome, clearFakeHome } from "../shared/fake-home.js"; @@ -53,6 +53,53 @@ async function importInstaller(): Promise.md\` +3. **If you need raw data**: Read the session JSONL at \`~/.deeplake/memory/sessions//.jsonl\` +4. **Keyword search**: \`grep -r "keyword" ~/.deeplake/memory/\` + +Do NOT jump straight to reading raw JSONL files. Always start with index.md and summaries. + +## Important Constraints + +- Use \`grep\` (NOT \`rg\`/ripgrep) for keyword search — \`rg\` may not be installed on the host system. +- Only use these bash builtins to interact with \`~/.deeplake/memory/\`: \`cat\`, \`ls\`, \`grep\`, \`echo\`, \`jq\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, \`wc\`, \`sort\`, \`find\`. The memory filesystem does NOT support \`rg\`, \`python\`, \`python3\`, \`node\`, or \`curl\`. +- If a file returns empty after 2 attempts, skip it and move on. Report what you found rather than retrying exhaustively. +`; + +const LEGACY_SKILL_V1 = LEGACY_SKILL_V2.replace( + "- Use `grep` (NOT `rg`/ripgrep) for keyword search — `rg` may not be installed on the host system.\n- Only use these bash builtins to interact with `~/.deeplake/memory/`: `cat`, `ls`, `grep`, `echo`, `jq`, `head`, `tail`, `sed`, `awk`, `wc`, `sort`, `find`. The memory filesystem does NOT support `rg`, `python`, `python3`, `node`, or `curl`.", + "- Only use bash builtins (`cat`, `ls`, `grep`, `echo`, `jq`, `head`, `tail`, `sed`, `awk`, `wc`, `sort`, `find`) to interact with `~/.deeplake/memory/`. The memory filesystem does NOT support `python`, `python3`, `node`, or `curl`.", +); + describe("installPi — cold install", () => { it("creates AGENTS.md with exactly one hivemind marker pair", async () => { const { installPi } = await importInstaller(); @@ -105,30 +152,84 @@ describe("installPi — re-install / cleanup", () => { expect((agents.match(new RegExp(END, "g")) ?? []).length).toBe(1); }); - it("cleans up the legacy per-agent SKILL.md drop on install", async () => { - // Older installer dropped a SKILL.md under skills/hivemind-memory/ — - // now removed because pi reads the shared agentskills location too, - // creating a collision with the codex installer. + it.each([LEGACY_SKILL_V1, LEGACY_SKILL_V2])( + "cleans up an unmodified legacy per-agent SKILL.md drop on install", + async (legacySkillBody) => { + // Older installer dropped a SKILL.md under skills/hivemind-memory/ — + // now removed because pi reads the shared agentskills location too, + // creating a collision with the codex installer. + const legacy = join(tmpHome, ".pi", "agent", "skills", "hivemind-memory"); + mkdirSync(legacy, { recursive: true }); + writeFileSync(join(legacy, "SKILL.md"), legacySkillBody); + const { installPi } = await importInstaller(); + installPi(); + expect(existsSync(legacy)).toBe(false); + }, + ); + + it("preserves all user files despite an old global Hivemind version stamp", async () => { const legacy = join(tmpHome, ".pi", "agent", "skills", "hivemind-memory"); mkdirSync(legacy, { recursive: true }); - writeFileSync(join(legacy, "SKILL.md"), "stale skill body"); + writeFileSync(join(legacy, "SKILL.md"), "user-authored skill"); + writeFileSync(join(legacy, "notes.txt"), "user notes"); mkdirSync(join(tmpHome, ".pi", "agent", ".hivemind"), { recursive: true }); writeFileSync(join(tmpHome, ".pi", "agent", ".hivemind", ".hivemind_version"), "6.0.0\n"); + const { installPi, uninstallPi } = await importInstaller(); + installPi(); + expect(readFileSync(join(legacy, "SKILL.md"), "utf-8")).toBe("user-authored skill"); + expect(readFileSync(join(legacy, "notes.txt"), "utf-8")).toBe("user notes"); + + uninstallPi(); + expect(readFileSync(join(legacy, "SKILL.md"), "utf-8")).toBe("user-authored skill"); + expect(readFileSync(join(legacy, "notes.txt"), "utf-8")).toBe("user notes"); + expect(process.stderr.write).toHaveBeenCalledWith(expect.stringContaining("remove it manually")); + }); + + it("preserves a modified historical skill instead of adopting it", async () => { + const legacy = join(tmpHome, ".pi", "agent", "skills", "hivemind-memory"); + mkdirSync(legacy, { recursive: true }); + writeFileSync(join(legacy, "SKILL.md"), `${LEGACY_SKILL_V2}\n# My customization\n`); const { installPi } = await importInstaller(); installPi(); - expect(existsSync(legacy)).toBe(false); + expect(readFileSync(join(legacy, "SKILL.md"), "utf-8")).toContain("# My customization"); }); - it("preserves a same-named user skill when no Hivemind ownership marker exists", async () => { + it("preserves a legacy skill directory containing any extra file", async () => { const legacy = join(tmpHome, ".pi", "agent", "skills", "hivemind-memory"); mkdirSync(legacy, { recursive: true }); - writeFileSync(join(legacy, "SKILL.md"), "user-authored skill"); + writeFileSync(join(legacy, "SKILL.md"), LEGACY_SKILL_V2); + writeFileSync(join(legacy, "user.txt"), "keep me"); + const { installPi } = await importInstaller(); + installPi(); + expect(readFileSync(join(legacy, "SKILL.md"), "utf-8")).toBe(LEGACY_SKILL_V2); + expect(readFileSync(join(legacy, "user.txt"), "utf-8")).toBe("keep me"); + }); + + it("preserves symlinks and file/directory mismatches at the legacy path", async () => { + const skills = join(tmpHome, ".pi", "agent", "skills"); + const legacy = join(skills, "hivemind-memory"); + const userSkill = join(tmpHome, "user-skill"); + mkdirSync(userSkill, { recursive: true }); + writeFileSync(join(userSkill, "SKILL.md"), LEGACY_SKILL_V2); + mkdirSync(skills, { recursive: true }); + symlinkSync(userSkill, legacy); + const { installPi, uninstallPi } = await importInstaller(); installPi(); - expect(readFileSync(join(legacy, "SKILL.md"), "utf-8")).toBe("user-authored skill"); + expect(lstatSync(legacy).isSymbolicLink()).toBe(true); + expect(readFileSync(join(userSkill, "SKILL.md"), "utf-8")).toBe(LEGACY_SKILL_V2); + uninstallPi(); + expect(lstatSync(legacy).isSymbolicLink()).toBe(true); + expect(readFileSync(join(userSkill, "SKILL.md"), "utf-8")).toBe(LEGACY_SKILL_V2); + rmSync(legacy); + writeFileSync(legacy, LEGACY_SKILL_V2); + installPi(); + expect(lstatSync(legacy).isFile()).toBe(true); + expect(readFileSync(legacy, "utf-8")).toBe(LEGACY_SKILL_V2); uninstallPi(); - expect(readFileSync(join(legacy, "SKILL.md"), "utf-8")).toBe("user-authored skill"); + expect(lstatSync(legacy).isFile()).toBe(true); + expect(readFileSync(legacy, "utf-8")).toBe(LEGACY_SKILL_V2); }); }); @@ -164,12 +265,10 @@ describe("uninstallPi", () => { expect(existsSync(join(tmpHome, ".pi", "agent", ".hivemind"))).toBe(false); }); - it("removes the legacy SKILL.md drop if it survived from an older installer", async () => { + it("removes an unmodified legacy SKILL.md drop if it survived from an older installer", async () => { const legacy = join(tmpHome, ".pi", "agent", "skills", "hivemind-memory"); mkdirSync(legacy, { recursive: true }); - writeFileSync(join(legacy, "SKILL.md"), "stale"); - mkdirSync(join(tmpHome, ".pi", "agent", ".hivemind"), { recursive: true }); - writeFileSync(join(tmpHome, ".pi", "agent", ".hivemind", ".hivemind_version"), "6.0.0\n"); + writeFileSync(join(legacy, "SKILL.md"), LEGACY_SKILL_V2); const { uninstallPi } = await importInstaller(); uninstallPi(); expect(existsSync(legacy)).toBe(false);