From a620c0b1040e644ded0614f8c6b8249ed1668edc Mon Sep 17 00:00:00 2001 From: Chai Landau Date: Tue, 16 Jun 2026 17:20:02 -0400 Subject: [PATCH] feat: add Ghost memory dir env default --- .changeset/ghost-memory-dir-env.md | 5 ++ README.md | 17 ++-- apps/docs/src/content/docs/cli-reference.mdx | 8 +- apps/docs/src/generated/cli-manifest.json | 20 ++--- packages/ghost/src/cli.ts | 4 +- packages/ghost/src/core/check.ts | 3 +- packages/ghost/src/fingerprint-commands.ts | 28 +++--- packages/ghost/src/relay.ts | 6 +- packages/ghost/src/review-packet.ts | 3 +- packages/ghost/src/scan-emit-command.ts | 8 +- packages/ghost/src/scan-stack-command.ts | 8 +- packages/ghost/src/scan/fingerprint-stack.ts | 13 +++ packages/ghost/src/scan/index.ts | 2 + packages/ghost/src/skill-bundle/SKILL.md | 12 +-- packages/ghost/test/cli.test.ts | 89 +++++++++++++++++++- 15 files changed, 170 insertions(+), 56 deletions(-) create mode 100644 .changeset/ghost-memory-dir-env.md diff --git a/.changeset/ghost-memory-dir-env.md b/.changeset/ghost-memory-dir-env.md new file mode 100644 index 00000000..a7b6a4da --- /dev/null +++ b/.changeset/ghost-memory-dir-env.md @@ -0,0 +1,5 @@ +--- +"@anarchitecture/ghost": minor +--- + +Adds `GHOST_MEMORY_DIR` as a host-wrapper default for Ghost fingerprint package storage. diff --git a/README.md b/README.md index 64239207..0a664e1c 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ Generation uses the three core layers: - `composition.yml` captures the patterns that make those materials feel like one intentional product. -Checks, memory, generated cache, nested packages, and custom `--memory-dir` -locations are supporting features. They do not replace the core fingerprint -layers. Generated cache answers what exists; curated fingerprint layers answer -what the surface is trying to preserve. +Checks, memory, generated cache, nested packages, and custom host-wrapper +package locations are supporting features. They do not replace the core +fingerprint layers. Generated cache answers what exists; curated fingerprint +layers answer what the surface is trying to preserve. Older `resources.yml`, `map.md`, `survey.json`, `patterns.yml`, and direct `fingerprint.yml` artifacts can still inform migration or cache workflows, but @@ -102,9 +102,12 @@ ghost lint .ghost ghost verify .ghost --root . ``` -Use `--with-intent`, `--with-config`, `--reference`, `--scope`, or -`--memory-dir` only when the repo needs those optional files, reference -libraries, nested product areas, or host-wrapper storage paths. +Use `--with-intent`, `--with-config`, `--reference`, or `--scope` only when the +repo needs those optional files, reference libraries, or nested product areas. +Host wrappers that need Ghost files somewhere other than `.ghost` may set +`GHOST_MEMORY_DIR=` on the child `ghost` process, or pass +`--memory-dir ` explicitly. Explicit `init [dir]` and +`--memory-dir ` values win over the environment default. Drafted fingerprint edits are just ordinary file changes until Git review accepts them. Checked-in `fingerprint/` core files are the Ghost source of diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx index ed55cdc1..6c667baf 100644 --- a/apps/docs/src/content/docs/cli-reference.mdx +++ b/apps/docs/src/content/docs/cli-reference.mdx @@ -41,8 +41,10 @@ The command tables below are generated from the CLI source. Run Create a `.ghost/fingerprint/` package with a manifest, raw core layer files, and enforcement checks. Use `--scope ` for nested package roots. Use -`--memory-dir ` when a host adapter stores Ghost package roots -under a different safe relative directory. +`GHOST_MEMORY_DIR=` only when a host wrapper stores Ghost package +roots under a different safe relative directory; raw `ghost` defaults to +`.ghost`. Explicit `init [dir]` and `--memory-dir ` values win +over the environment default. @@ -50,6 +52,7 @@ under a different safe relative directory. ghost init ghost init --with-intent ghost init --scope apps/checkout --with-intent +GHOST_MEMORY_DIR=.agents/ghost ghost init ghost init --scope apps/checkout --memory-dir .design/memory ``` @@ -64,6 +67,7 @@ toward inventory readiness. ```bash ghost scan ghost scan --format json +GHOST_MEMORY_DIR=.agents/ghost ghost scan --format json ghost scan --include-nested --memory-dir .design/memory --format json ``` diff --git a/apps/docs/src/generated/cli-manifest.json b/apps/docs/src/generated/cli-manifest.json index e7ee3ed4..28b69573 100644 --- a/apps/docs/src/generated/cli-manifest.json +++ b/apps/docs/src/generated/cli-manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-06-15T13:20:23.174Z", + "generatedAt": "2026-06-16T20:57:35.926Z", "tools": [ { "tool": "ghost", @@ -33,7 +33,7 @@ { "rawName": "--memory-dir ", "name": "memoryDir", - "description": "Relative fingerprint package directory for --all and default package lookup (flag name retained; default: .ghost)", + "description": "Relative fingerprint package directory for host wrappers, --all, and default package lookup (env: GHOST_MEMORY_DIR; default: .ghost)", "default": null, "takesValue": true, "negated": false @@ -61,7 +61,7 @@ { "rawName": "--memory-dir ", "name": "memoryDir", - "description": "Relative fingerprint package directory for init --scope or default root init (flag name retained; default: .ghost)", + "description": "Relative fingerprint package directory for host wrappers, init --scope, and default root init (env: GHOST_MEMORY_DIR; default: .ghost)", "default": null, "takesValue": true, "negated": false @@ -145,7 +145,7 @@ { "rawName": "--memory-dir ", "name": "memoryDir", - "description": "Relative fingerprint package directory for --all and default package lookup (flag name retained; default: .ghost)", + "description": "Relative fingerprint package directory for host wrappers, --all, and default package lookup (env: GHOST_MEMORY_DIR; default: .ghost)", "default": null, "takesValue": true, "negated": false @@ -181,7 +181,7 @@ { "rawName": "--memory-dir ", "name": "memoryDir", - "description": "Relative fingerprint package directory for nested discovery and default scan (flag name retained; default: .ghost)", + "description": "Relative fingerprint package directory for host wrappers, nested discovery, and default scan (env: GHOST_MEMORY_DIR; default: .ghost)", "default": null, "takesValue": true, "negated": false @@ -209,7 +209,7 @@ { "rawName": "--memory-dir ", "name": "memoryDir", - "description": "Relative fingerprint package directory for stack discovery (flag name retained; default: .ghost)", + "description": "Relative fingerprint package directory for host wrappers and stack discovery (env: GHOST_MEMORY_DIR; default: .ghost)", "default": null, "takesValue": true, "negated": false @@ -348,7 +348,7 @@ { "rawName": "--memory-dir ", "name": "memoryDir", - "description": "Relative fingerprint package directory for --path stack resolution (flag name retained; default: .ghost)", + "description": "Relative fingerprint package directory for host wrappers and --path stack resolution (env: GHOST_MEMORY_DIR; default: .ghost)", "default": null, "takesValue": true, "negated": false @@ -576,7 +576,7 @@ { "rawName": "--memory-dir ", "name": "memoryDir", - "description": "Relative fingerprint package directory for stack resolution (default: .ghost)", + "description": "Relative fingerprint package directory for host wrappers and stack resolution (env: GHOST_MEMORY_DIR; default: .ghost)", "default": null, "takesValue": true, "negated": false @@ -672,7 +672,7 @@ { "rawName": "--memory-dir ", "name": "memoryDir", - "description": "Relative fingerprint package directory for stack discovery (flag name retained; default: .ghost)", + "description": "Relative fingerprint package directory for host wrappers and stack discovery (env: GHOST_MEMORY_DIR; default: .ghost)", "default": null, "takesValue": true, "negated": false @@ -724,7 +724,7 @@ { "rawName": "--memory-dir ", "name": "memoryDir", - "description": "Relative fingerprint package directory for stack discovery (flag name retained; default: .ghost)", + "description": "Relative fingerprint package directory for host wrappers and stack discovery (env: GHOST_MEMORY_DIR; default: .ghost)", "default": null, "takesValue": true, "negated": false diff --git a/packages/ghost/src/cli.ts b/packages/ghost/src/cli.ts index 4dccdffc..4006db03 100644 --- a/packages/ghost/src/cli.ts +++ b/packages/ghost/src/cli.ts @@ -173,7 +173,7 @@ export function buildCli(): ReturnType { ) .option( "--memory-dir ", - "Relative fingerprint package directory for stack discovery (flag name retained; default: .ghost)", + "Relative fingerprint package directory for host wrappers and stack discovery (env: GHOST_MEMORY_DIR; default: .ghost)", ) .option("--format ", "Output format: markdown or json", { default: "markdown", @@ -229,7 +229,7 @@ export function buildCli(): ReturnType { ) .option( "--memory-dir ", - "Relative fingerprint package directory for stack discovery (flag name retained; default: .ghost)", + "Relative fingerprint package directory for host wrappers and stack discovery (env: GHOST_MEMORY_DIR; default: .ghost)", ) .option( "--include-memory", diff --git a/packages/ghost/src/core/check.ts b/packages/ghost/src/core/check.ts index 9649ad1e..52423f48 100644 --- a/packages/ghost/src/core/check.ts +++ b/packages/ghost/src/core/check.ts @@ -20,6 +20,7 @@ import { import { groupFingerprintStacksForPaths, mapFromFingerprint, + resolveMemoryDirDefault, } from "../scan/fingerprint-stack.js"; import { INLINE_COLOR_LITERAL_PATTERN, @@ -125,7 +126,7 @@ export async function runGhostDriftCheck( const groups = await groupFingerprintStacksForPaths( changedFiles.map((file) => file.path), cwd, - { memoryDir: options.memoryDir }, + { memoryDir: resolveMemoryDirDefault(options.memoryDir) }, ); const routedFiles: GhostDriftRoutedFile[] = []; const findings: GhostDriftCheckFinding[] = []; diff --git a/packages/ghost/src/fingerprint-commands.ts b/packages/ghost/src/fingerprint-commands.ts index aa327e15..278e4f44 100644 --- a/packages/ghost/src/fingerprint-commands.ts +++ b/packages/ghost/src/fingerprint-commands.ts @@ -38,6 +38,7 @@ import { fingerprintPackageDisplayPath, inventory, normalizeMemoryDir, + resolveMemoryDirDefault, scanStatus, } from "./scan/index.js"; import { registerEmitCommand } from "./scan-emit-command.js"; @@ -69,7 +70,7 @@ export function registerFingerprintCommands(cli: CAC): void { ) .option( "--memory-dir ", - "Relative fingerprint package directory for --all and default package lookup (flag name retained; default: .ghost)", + "Relative fingerprint package directory for host wrappers, --all, and default package lookup (env: GHOST_MEMORY_DIR; default: .ghost)", ) .action(async (path: string | undefined, opts) => { try { @@ -140,7 +141,7 @@ export function registerFingerprintCommands(cli: CAC): void { ) .option( "--memory-dir ", - "Relative fingerprint package directory for init --scope or default root init (flag name retained; default: .ghost)", + "Relative fingerprint package directory for host wrappers, init --scope, and default root init (env: GHOST_MEMORY_DIR; default: .ghost)", ) .option( "--with-intent", @@ -168,22 +169,23 @@ export function registerFingerprintCommands(cli: CAC): void { process.exit(2); return; } - const memoryDir = memoryDirFromOpts(opts); + const memoryDir = + typeof opts.scope === "string" || dirArg === undefined + ? memoryDirFromOpts(opts) + : undefined; const initOptions = { withIntent: Boolean(opts.withIntent), withConfig: Boolean(opts.withConfig || opts.reference), reference: typeof opts.reference === "string" ? opts.reference : undefined, force: Boolean(opts.force), - memoryDir, }; const paths = typeof opts.scope === "string" - ? await initScopedFingerprintPackage( - opts.scope, - process.cwd(), - initOptions, - ) + ? await initScopedFingerprintPackage(opts.scope, process.cwd(), { + ...initOptions, + memoryDir, + }) : await initFingerprintPackage( dirArg ?? memoryDir, process.cwd(), @@ -242,7 +244,7 @@ export function registerFingerprintCommands(cli: CAC): void { ) .option( "--memory-dir ", - "Relative fingerprint package directory for --all and default package lookup (flag name retained; default: .ghost)", + "Relative fingerprint package directory for host wrappers, --all, and default package lookup (env: GHOST_MEMORY_DIR; default: .ghost)", ) .action(async (dirArg: string | undefined, opts) => { try { @@ -295,7 +297,7 @@ export function registerFingerprintCommands(cli: CAC): void { ) .option( "--memory-dir ", - "Relative fingerprint package directory for nested discovery and default scan (flag name retained; default: .ghost)", + "Relative fingerprint package directory for host wrappers, nested discovery, and default scan (env: GHOST_MEMORY_DIR; default: .ghost)", ) .option("--format ", "Output format: cli or json", { default: "cli" }) .action(async (dirArg: string | undefined, opts) => { @@ -729,9 +731,7 @@ function dirnameForFingerprintPackageDir( } function memoryDirFromOpts(opts: { memoryDir?: unknown }): string { - return normalizeMemoryDir( - typeof opts.memoryDir === "string" ? opts.memoryDir : undefined, - ); + return resolveMemoryDirDefault(opts.memoryDir); } function initCommandOutput( diff --git a/packages/ghost/src/relay.ts b/packages/ghost/src/relay.ts index bb139d1c..cdf8f75b 100644 --- a/packages/ghost/src/relay.ts +++ b/packages/ghost/src/relay.ts @@ -13,7 +13,7 @@ import { fingerprintStackToPackageContext, type GhostFingerprintStack, loadFingerprintStackForPath, - normalizeMemoryDir, + resolveMemoryDirDefault, } from "./scan/fingerprint-stack.js"; export const RELAY_GATHER_SCHEMA = "ghost.relay.gather/v1" as const; @@ -75,7 +75,7 @@ export async function gatherRelayContext( }); } - const memoryDir = normalizeMemoryDir(options.memoryDir); + const memoryDir = resolveMemoryDirDefault(options.memoryDir); const stack = await loadFingerprintStackForPath(target, cwd, { memoryDir }); const context = fingerprintStackToPackageContext(stack, options.name); return gatherFromContext(context, { @@ -111,7 +111,7 @@ export function registerRelayCommand(cli: CAC): void { ) .option( "--memory-dir ", - "Relative fingerprint package directory for stack resolution (default: .ghost)", + "Relative fingerprint package directory for host wrappers and stack resolution (env: GHOST_MEMORY_DIR; default: .ghost)", ) .option( "--name ", diff --git a/packages/ghost/src/review-packet.ts b/packages/ghost/src/review-packet.ts index 506e970d..e9217c04 100644 --- a/packages/ghost/src/review-packet.ts +++ b/packages/ghost/src/review-packet.ts @@ -13,6 +13,7 @@ import { fingerprintStackToPackageContext, type GhostFingerprintStack, groupFingerprintStacksForPaths, + resolveMemoryDirDefault, } from "./scan/fingerprint-stack.js"; import { type GhostPackageConfig, @@ -84,7 +85,7 @@ async function buildStackReviewPacket(options: { const groups = await groupFingerprintStacksForPaths( changedFiles, process.cwd(), - { memoryDir: options.memoryDir }, + { memoryDir: resolveMemoryDirDefault(options.memoryDir) }, ); const stacks = groups.map((group) => reviewStackFromFingerprintStack(group.stack, group.changed_files), diff --git a/packages/ghost/src/scan-emit-command.ts b/packages/ghost/src/scan-emit-command.ts index dcf1a2f9..16e75766 100644 --- a/packages/ghost/src/scan-emit-command.ts +++ b/packages/ghost/src/scan-emit-command.ts @@ -10,7 +10,7 @@ import { resolveFingerprintPackage } from "./fingerprint.js"; import { fingerprintStackToPackageContext, loadFingerprintStackForPath, - normalizeMemoryDir, + resolveMemoryDirDefault, } from "./scan/fingerprint-stack.js"; const DEFAULT_REVIEW_OUT = ".claude/commands/design-review.md"; @@ -52,7 +52,7 @@ export function registerEmitCommand(cli: CAC): void { ) .option( "--memory-dir ", - "Relative fingerprint package directory for --path stack resolution (flag name retained; default: .ghost)", + "Relative fingerprint package directory for host wrappers and --path stack resolution (env: GHOST_MEMORY_DIR; default: .ghost)", ) .option( "-o, --out ", @@ -120,9 +120,7 @@ async function loadEmitPackageContext(opts: { typeof opts.path === "string" ? opts.path : ".", process.cwd(), { - memoryDir: normalizeMemoryDir( - typeof opts.memoryDir === "string" ? opts.memoryDir : undefined, - ), + memoryDir: resolveMemoryDirDefault(opts.memoryDir), }, ); return fingerprintStackToPackageContext(stack); diff --git a/packages/ghost/src/scan-stack-command.ts b/packages/ghost/src/scan-stack-command.ts index 39d7f839..2cfd2c41 100644 --- a/packages/ghost/src/scan-stack-command.ts +++ b/packages/ghost/src/scan-stack-command.ts @@ -3,7 +3,7 @@ import { fingerprintPackageDisplayPath, type GhostFingerprintStack, loadFingerprintStackForPath, - normalizeMemoryDir, + resolveMemoryDirDefault, } from "./scan/index.js"; export function registerStackCommand(cli: CAC): void { @@ -14,14 +14,12 @@ export function registerStackCommand(cli: CAC): void { ) .option( "--memory-dir ", - "Relative fingerprint package directory for stack discovery (flag name retained; default: .ghost)", + "Relative fingerprint package directory for host wrappers and stack discovery (env: GHOST_MEMORY_DIR; default: .ghost)", ) .option("--format ", "Output format: cli or json", { default: "cli" }) .action(async (paths: string[] | string | undefined, opts) => { try { - const memoryDir = normalizeMemoryDir( - typeof opts.memoryDir === "string" ? opts.memoryDir : undefined, - ); + const memoryDir = resolveMemoryDirDefault(opts.memoryDir); const requestedPaths = Array.isArray(paths) ? paths : typeof paths === "string" diff --git a/packages/ghost/src/scan/fingerprint-stack.ts b/packages/ghost/src/scan/fingerprint-stack.ts index 37678445..5f8df5ce 100644 --- a/packages/ghost/src/scan/fingerprint-stack.ts +++ b/packages/ghost/src/scan/fingerprint-stack.ts @@ -989,6 +989,19 @@ export function normalizeMemoryDir( return normalized; } +export const GHOST_MEMORY_DIR_ENV = "GHOST_MEMORY_DIR"; + +export function resolveMemoryDirDefault( + explicitMemoryDir?: unknown, + env: NodeJS.ProcessEnv = process.env, +): string { + return normalizeMemoryDir( + typeof explicitMemoryDir === "string" + ? explicitMemoryDir + : env[GHOST_MEMORY_DIR_ENV], + ); +} + export function fingerprintPackageDisplayPath( relativeRoot: string, memoryDir = FINGERPRINT_PACKAGE_DIR, diff --git a/packages/ghost/src/scan/index.ts b/packages/ghost/src/scan/index.ts index b987574a..62b59dbb 100644 --- a/packages/ghost/src/scan/index.ts +++ b/packages/ghost/src/scan/index.ts @@ -19,10 +19,12 @@ export { discoverFingerprintStack, discoverGhostPackages, fingerprintPackageDisplayPath, + GHOST_MEMORY_DIR_ENV, groupFingerprintStacksForPaths, loadFingerprintStackForPath, normalizeMemoryDir, resolveGitRoot, + resolveMemoryDirDefault, } from "./fingerprint-stack.js"; export { inventory } from "./inventory.js"; export type { diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md index d5e3c703..27b19f59 100644 --- a/packages/ghost/src/skill-bundle/SKILL.md +++ b/packages/ghost/src/skill-bundle/SKILL.md @@ -54,10 +54,12 @@ Optional support material lives under purpose folders: `fingerprint/sources/cache/` for generated observations. `.ghost/config.yml` stays outside the portable package as local routing config. -Advanced repos may contain nested fingerprint packages such as `apps/checkout/.ghost/`, and -host wrappers may use `--memory-dir `. Ghost stays -adapter-neutral: wrappers consume JSON and map severities into their own review -or check format. +Advanced repos may contain nested fingerprint packages such as +`apps/checkout/.ghost/`. Host wrappers may set +`GHOST_MEMORY_DIR=` on the child `ghost` process, or pass +`--memory-dir ` explicitly, when they need repo-local Ghost files +outside raw `ghost`'s `.ghost` default. Ghost stays adapter-neutral: wrappers +consume JSON and map severities into their own review or check format. ## Core CLI Verbs @@ -77,7 +79,7 @@ or check format. | Verb | Purpose | |---|---| -| `ghost init --scope ` / `--memory-dir ` | Create or resolve scoped/custom fingerprint packages. | +| `ghost init --scope ` / `--memory-dir ` | Create or resolve scoped/custom fingerprint packages for nested packages or host wrappers. | | `ghost stack [path...]` | Inspect resolved broad-to-local fingerprint stack and merged output. | | `ghost inventory [path]` | Emit raw repo signals for optional generated cache/source material. | | `ghost lint --all` / `ghost verify --all` | Validate nested stack merges. | diff --git a/packages/ghost/test/cli.test.ts b/packages/ghost/test/cli.test.ts index f53963b8..a44a7c6f 100644 --- a/packages/ghost/test/cli.test.ts +++ b/packages/ghost/test/cli.test.ts @@ -48,10 +48,14 @@ function fingerprintWithId(id: string): string { async function runCli( argv: string[], cwd: string, - options: { allowNoExit?: boolean } = {}, + options: { + allowNoExit?: boolean; + env?: Record; + } = {}, ) { const cli = buildCli(); const previousCwd = process.cwd(); + const previousEnv = new Map(); let stdout = ""; let stderr = ""; let exitCode: number | undefined; @@ -86,6 +90,16 @@ async function runCli( try { process.chdir(cwd); + if (options.env) { + for (const [key, value] of Object.entries(options.env)) { + previousEnv.set(key, process.env[key]); + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + } cli.parse(["node", "ghost", ...argv]); if (options.allowNoExit) { setTimeout(finish, 500); @@ -98,6 +112,13 @@ async function runCli( ]); } finally { process.chdir(previousCwd); + for (const [key, value] of previousEnv) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } stdoutSpy.mockRestore(); stderrSpy.mockRestore(); logSpy.mockRestore(); @@ -342,6 +363,72 @@ describe("ghost CLI", () => { expect(reviewCommand.code).toBe(0); }); + it("uses GHOST_MEMORY_DIR as the default fingerprint package directory for init", async () => { + const init = await runCli(["init", "--format", "json"], dir, { + env: { GHOST_MEMORY_DIR: ".agents/ghost" }, + }); + + expect(init.code).toBe(0); + const initOutput = JSON.parse(init.stdout); + expect(await realpath(initOutput.dir)).toBe( + await realpath(join(dir, ".agents", "ghost")), + ); + await expect( + readFile( + join(dir, ".agents", "ghost", "fingerprint", "manifest.yml"), + "utf-8", + ), + ).resolves.toContain("schema: ghost.fingerprint-package/v1"); + await expect( + readFile(join(dir, ".ghost", "fingerprint", "manifest.yml"), "utf-8"), + ).rejects.toThrow(); + }); + + it("keeps explicit init directory positional args ahead of GHOST_MEMORY_DIR", async () => { + const init = await runCli(["init", "custom-dir", "--format", "json"], dir, { + env: { GHOST_MEMORY_DIR: ".agents/ghost" }, + }); + + expect(init.code).toBe(0); + const initOutput = JSON.parse(init.stdout); + expect(await realpath(initOutput.dir)).toBe( + await realpath(join(dir, "custom-dir")), + ); + await expect( + readFile(join(dir, "custom-dir", "fingerprint", "manifest.yml"), "utf-8"), + ).resolves.toContain("schema: ghost.fingerprint-package/v1"); + await expect( + readFile( + join(dir, ".agents", "ghost", "fingerprint", "manifest.yml"), + "utf-8", + ), + ).rejects.toThrow(); + }); + + it("rejects invalid GHOST_MEMORY_DIR with memory-dir validation errors", async () => { + const init = await runCli(["init"], dir, { + env: { GHOST_MEMORY_DIR: "../outside" }, + }); + + expect(init.code).toBe(2); + expect(init.stderr).toContain("--memory-dir must not contain"); + }); + + it("uses GHOST_MEMORY_DIR as the default package lookup for scan", async () => { + await runCli(["init", ".agents/ghost"], dir); + + const scan = await runCli(["scan", "--format", "json"], dir, { + env: { GHOST_MEMORY_DIR: ".agents/ghost" }, + }); + + expect(scan.code).toBe(0); + const status = JSON.parse(scan.stdout); + expect(await realpath(status.dir)).toBe( + await realpath(join(dir, ".agents", "ghost")), + ); + expect(status.fingerprint.state).toBe("present"); + }); + it("refuses to overwrite existing fingerprint files unless forced", async () => { await runCli(["init"], dir); await writeFile(