diff --git a/src/plugins/pi/content/README.md b/src/plugins/pi/content/README.md index 119d1705..803ca684 100644 --- a/src/plugins/pi/content/README.md +++ b/src/plugins/pi/content/README.md @@ -74,9 +74,14 @@ package name when needed. Settings load in this order, with later values taking precedence: 1. Defaults -2. `~/.pi/agent/braintrust.json` -3. `.pi/braintrust.json` in the project (or Pi's configured project config directory) -4. `bt trace run` settings for that invocation +2. `~/.pi/agent/braintrust.json`, the file `bt trace enable pi` writes +3. `braintrust.json` in the running host's agent directory, when it differs from `~/.pi/agent` +4. `.pi/braintrust.json` in the project (or the running host's configured config directory) +5. `bt trace run` settings for that invocation + +A host package's `piConfig.configDir` replaces `.pi` in steps 3 and 4. A host that +sets `PI_CODING_AGENT_DIR` (for example, for named profiles) uses that directory in +step 3. Example: diff --git a/src/plugins/pi/content/src/config.test.ts b/src/plugins/pi/content/src/config.test.ts index 09428c71..a3241165 100644 --- a/src/plugins/pi/content/src/config.test.ts +++ b/src/plugins/pi/content/src/config.test.ts @@ -1,7 +1,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { loadConfig } from "./config.ts"; const ENVIRONMENT_KEYS = [ @@ -13,6 +13,7 @@ const ENVIRONMENT_KEYS = [ "BRAINTRUST_SHOW_TRACE_LINK", "TRACE_TO_BRAINTRUST", "BT_TRACE_INVOCATION_SETTINGS", + "PI_CODING_AGENT_DIR", ] as const; const originalEnvironment = new Map(); @@ -105,6 +106,59 @@ describe("loadConfig", () => { }); }); + it("honors the host's explicit agent directory without changing project precedence", () => { + process.env.PI_CODING_AGENT_DIR = join(home, ".omp", "profiles", "work", "agent"); + writeJson(join(process.env.PI_CODING_AGENT_DIR, "braintrust.json"), { + trace_to_braintrust: true, + profile: "host-profile", + project: "profile-project", + }); + expect(loadConfig(cwd)).toMatchObject({ + enabled: true, + profile: "host-profile", + projectName: "profile-project", + }); + writeJson(join(cwd, ".pi", "braintrust.json"), { project: "project-override" }); + expect(loadConfig(cwd)).toMatchObject({ + profile: "host-profile", + projectName: "project-override", + }); + }); + + it("uses the running host's config directory for global and project settings", async () => { + vi.doMock("./pi-package.ts", () => ({ + runningPiPackage: () => ({ configDir: ".omp", version: "18.12.6" }), + })); + vi.resetModules(); + try { + // Reload the module boundary so its cached config directory sees this host. + const { loadConfig: loadHostConfig } = await import("./config.ts"); + // `bt trace enable pi` writes Pi's global file; hosts still honor it. + writeJson(join(home, ".pi", "agent", "braintrust.json"), { + trace_to_braintrust: true, + project: "bt-written", + }); + expect(loadHostConfig(cwd)).toMatchObject({ enabled: true, projectName: "bt-written" }); + writeJson(join(home, ".omp", "agent", "braintrust.json"), { + profile: "host-profile", + project: "host-project", + }); + expect(loadHostConfig(cwd)).toMatchObject({ + enabled: true, + profile: "host-profile", + projectName: "host-project", + }); + writeJson(join(cwd, ".omp", "braintrust.json"), { project: "project-override" }); + expect(loadHostConfig(cwd)).toMatchObject({ + profile: "host-profile", + projectName: "project-override", + }); + } finally { + vi.doUnmock("./pi-package.ts"); + vi.resetModules(); + } + }); + it("prefers a canonical route over stale top-level routing", () => { writeJson(join(home, ".pi", "agent", "braintrust.json"), { trace_to_braintrust: true, diff --git a/src/plugins/pi/content/src/config.ts b/src/plugins/pi/content/src/config.ts index fe7d26af..b66fc04f 100644 --- a/src/plugins/pi/content/src/config.ts +++ b/src/plugins/pi/content/src/config.ts @@ -1,6 +1,6 @@ import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; import { type DaemonSessionRoute, resolveDaemonTraceSettings } from "./runtime/daemon-client.ts"; import { runningPiPackage } from "./pi-package.ts"; @@ -17,7 +17,7 @@ export interface PiConfig { type ConfigRecord = Record; -const PROJECT_CONFIG_DIR_NAME = runningPiPackage().configDir; +const CONFIG_DIR_NAME = runningPiPackage().configDir; function record(value: unknown): ConfigRecord | undefined { return value !== null && typeof value === "object" && !Array.isArray(value) @@ -101,8 +101,15 @@ export function loadConfig(cwd = process.cwd()): PiConfig { }, }; - applyConfig(config, readConfig(join(homedir(), ".pi", "agent", "braintrust.json"))); - applyConfig(config, readConfig(join(cwd, PROJECT_CONFIG_DIR_NAME, "braintrust.json"))); + // `bt trace enable pi` writes Pi's global file, so it stays the base layer. A host + // with its own agent directory layers its settings on top. + const piAgentDir = join(homedir(), ".pi", "agent"); + const hostAgentDir = process.env.PI_CODING_AGENT_DIR || join(homedir(), CONFIG_DIR_NAME, "agent"); + applyConfig(config, readConfig(join(piAgentDir, "braintrust.json"))); + if (resolve(hostAgentDir) !== resolve(piAgentDir)) { + applyConfig(config, readConfig(join(hostAgentDir, "braintrust.json"))); + } + applyConfig(config, readConfig(join(cwd, CONFIG_DIR_NAME, "braintrust.json"))); config.showUi = boolean(process.env.BRAINTRUST_SHOW_UI) ?? config.showUi; config.showTraceLink = boolean(process.env.BRAINTRUST_SHOW_TRACE_LINK) ?? config.showTraceLink;