Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ghost-package-dir-everywhere.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anarchitecture/ghost": patch
---

Honor `GHOST_PACKAGE_DIR` in the drift, ack, diverge, compare-history, and review-command emit paths so relocated fingerprint packages resolve consistently.
3 changes: 2 additions & 1 deletion packages/ghost/src/context/package-review-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
GhostFingerprintPrinciple,
GhostFingerprintSituation,
} from "#ghost-core";
import { resolveGhostDirDefault } from "../scan/index.js";
import type { PackageContext } from "./package-context.js";

export interface EmitPackageReviewInput {
Expand Down Expand Up @@ -285,7 +286,7 @@ Generated from \`${packageDir}/\` for ${context.name}. Re-run \`ghost emit revie
}

function displayPackageDir(context: PackageContext): string {
return displayPath(context.packageDir ?? ".ghost");
return displayPath(context.packageDir ?? resolveGhostDirDefault());
}

function displayPath(path: string): string {
Expand Down
13 changes: 7 additions & 6 deletions packages/ghost/src/core/evolution/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ import { existsSync } from "node:fs";
import { appendFile, mkdir, readFile } from "node:fs/promises";
import { resolve } from "node:path";
import type { FingerprintHistoryEntry } from "#ghost-core";
import { resolveGhostDirDefault } from "../../scan/index.js";

const GHOST_DIR = ".ghost";
const HISTORY_FILE = "history.jsonl";

function historyPath(cwd: string): string {
return resolve(cwd, GHOST_DIR, HISTORY_FILE);
return resolve(cwd, resolveGhostDirDefault(), HISTORY_FILE);
}

/**
* Append a fingerprint history entry to .ghost/history.jsonl.
* Creates the .ghost directory if it doesn't exist.
* Append a fingerprint history entry to the fingerprint package's
* history.jsonl (honors GHOST_PACKAGE_DIR; defaults to .ghost).
* Creates the package directory if it doesn't exist.
*/
export async function appendHistory(
entry: FingerprintHistoryEntry,
cwd: string = process.cwd(),
): Promise<void> {
const dir = resolve(cwd, GHOST_DIR);
const dir = resolve(cwd, resolveGhostDirDefault());
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
Expand All @@ -27,7 +28,7 @@ export async function appendHistory(
}

/**
* Read all history entries from .ghost/history.jsonl.
* Read all history entries from the package history.jsonl.
* Returns an empty array if no history exists.
*/
export async function readHistory(
Expand Down
9 changes: 7 additions & 2 deletions packages/ghost/src/drift-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
resolveTrackedFingerprint,
} from "./core/index.js";
import { resolveFingerprintPackage } from "./fingerprint.js";
import { resolveGhostDirDefault } from "./scan/index.js";

const DEFAULT_SYNC_PATH = ".ghost-sync.json";
const DRIFT_STATUS_SCHEMA = "ghost.drift.status/v1" as const;
Expand Down Expand Up @@ -243,14 +244,18 @@ async function loadLocalFingerprint(
localPath: string | undefined,
packageDir: string | undefined,
): Promise<Fingerprint> {
const source = localPath ?? packageDir ?? ".ghost";
const ghostDir = resolveGhostDirDefault();
const source = localPath ?? packageDir ?? ghostDir;
try {
return await loadComparableFingerprintFrom(cwd, source);
} catch (err) {
const defaultPackage = !localPath && !packageDir;
const manifestPath = resolve(cwd, source, "manifest.yml");
if (!defaultPackage || existsSync(manifestPath)) throw err;
return await loadComparableFingerprintFrom(cwd, ".ghost/fingerprint.md");
return await loadComparableFingerprintFrom(
cwd,
`${ghostDir}/fingerprint.md`,
);
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/ghost/src/evolution-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
loadConfig,
resolveTrackedFingerprint,
} from "./core/index.js";
import { resolveGhostDirDefault } from "./scan/index.js";

async function loadLocalFingerprint() {
return loadComparableFingerprint(".ghost");
return loadComparableFingerprint(resolveGhostDirDefault());
}

async function loadTrackedComparableFingerprint(
Expand Down
47 changes: 47 additions & 0 deletions packages/ghost/test/evolution/history-package-dir.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { existsSync } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { appendHistory, readHistory } from "../../src/core/evolution/index.js";

const ENV = "GHOST_PACKAGE_DIR";

describe("history honors GHOST_PACKAGE_DIR", () => {
let cwd: string;
let prev: string | undefined;

beforeEach(async () => {
cwd = await mkdtemp(join(tmpdir(), "ghost-history-"));
prev = process.env[ENV];
});

afterEach(async () => {
if (prev === undefined) delete process.env[ENV];
else process.env[ENV] = prev;
await rm(cwd, { recursive: true, force: true });
});

const entry = {
schema: "ghost.fingerprint.history/v1" as const,
timestamp: "2026-01-01T00:00:00.000Z",
target: "self",
} as unknown as Parameters<typeof appendHistory>[0];

it("defaults to .ghost/history.jsonl", async () => {
delete process.env[ENV];
await appendHistory(entry, cwd);
expect(existsSync(join(cwd, ".ghost", "history.jsonl"))).toBe(true);
expect(await readHistory(cwd)).toHaveLength(1);
});

it("writes under the configured package dir (.agents/ghost)", async () => {
process.env[ENV] = ".agents/ghost";
await appendHistory(entry, cwd);
expect(existsSync(join(cwd, ".agents", "ghost", "history.jsonl"))).toBe(
true,
);
expect(existsSync(join(cwd, ".ghost", "history.jsonl"))).toBe(false);
expect(await readHistory(cwd)).toHaveLength(1);
});
});
Loading