diff --git a/CHANGELOG.md b/CHANGELOG.md index 064f58d6..5f36efff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.0.13-beta.3 — 2026-07-13 + +### Fixes +- Fix the Pi live `pi list` roundtrip e2e tests against pi-coding-agent ≥0.80: newer Pi no longer trusts project-local `.pi/settings.json` by default, so the tests now detect the installed Pi version and pass `pi list --approve` on ≥0.80 (older Pi trusts project settings without the flag and would reject it) to include the project-scope package failproofai's installer writes (previously `pi list` printed "No packages installed." — failing the install roundtrip and making the uninstall roundtrip pass vacuously). Gated behind `detectPiVersion()`, so it only runs where `pi` is installed. (#491) + ## 0.0.13-beta.2 — 2026-07-10 ### Fixes diff --git a/CLAUDE.md b/CLAUDE.md index 37b7df1c..b5e98fd5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -195,6 +195,17 @@ v0.72.1): Note: `~/.pi/settings.json` does NOT exist on a fresh install; user-scope settings live one level deeper under `~/.pi/agent/`. +**Project-local trust (pi ≥0.80).** Newer Pi (verified against pi-coding-agent +v0.80.3) no longer trusts a **project-local** `.pi/settings.json` by default: +`pi list` gained a `--approve`/`-a` flag ("Trust project-local files for this +command"), and without it project packages are silently ignored (`pi list` +prints `No packages installed.`). User-scope packages +(`~/.pi/agent/settings.json`) are always trusted. The live `pi list` roundtrip +tests in `__tests__/e2e/hooks/pi-integration.e2e.test.ts` detect the installed +Pi version and pass `--approve` only on ≥0.80 (older Pi trusts project files +unconditionally and rejects the unknown flag), so they see the project-scope +package failproofai's installer writes on either side of the cutoff. + **Schema** is a flat string array — `{"packages": ["./relative/path", ...]}`. Each entry is a path Pi resolves relative to the directory containing `settings.json` (so `/.pi/` for project scope). For dogfood we write diff --git a/__tests__/e2e/hooks/pi-integration.e2e.test.ts b/__tests__/e2e/hooks/pi-integration.e2e.test.ts index b0c64de9..fa630a80 100644 --- a/__tests__/e2e/hooks/pi-integration.e2e.test.ts +++ b/__tests__/e2e/hooks/pi-integration.e2e.test.ts @@ -52,15 +52,24 @@ function writeConfig(cwd: string, enabledPolicies: string[]): void { writeFileSync(configPath, JSON.stringify({ enabledPolicies }, null, 2)); } -function piIsAvailable(): boolean { +function detectPiVersion(): string | null { try { const probe = spawnSync("pi", ["--version"], { encoding: "utf8", timeout: 5_000 }); - return probe.status === 0; + if (probe.status !== 0) return null; + return probe.stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? null; } catch { - return false; + return null; } } +// pi ≥0.80 no longer trusts project-local .pi/settings.json by default — +// `--approve` opts in per command. Older pi trusts it unconditionally and +// rejects the (unknown) flag, so only pass it where it exists. +function piListArgs(version: string): string[] { + const [major = 0, minor = 0] = version.split(".").map(Number); + return major > 0 || minor >= 80 ? ["list", "--approve"] : ["list"]; +} + describe("E2E: Pi integration — hook protocol (handler-only)", () => { it("tool_call → PreToolUse: block-sudo emits {permission:'deny', reason}", () => { const env = createPiEnv(); @@ -432,8 +441,9 @@ describe("E2E: Pi integration — install/uninstall", () => { // Pi actually parses the settings.json failproofai writes. // ────────────────────────────────────────────────────────────────────────── -const piPresent = piIsAvailable(); -const describePi = piPresent ? describe : describe.skip; +const piVersion = detectPiVersion(); +const describePi = piVersion ? describe : describe.skip; +const PI_LIST_ARGS = piVersion ? piListArgs(piVersion) : ["list"]; describePi("E2E: Pi integration — live `pi list` roundtrip (real binary)", () => { it("after policies --install, `pi list` shows the failproofai entry", () => { @@ -444,8 +454,10 @@ describePi("E2E: Pi integration — live `pi list` roundtrip (real binary)", () { cwd: env.cwd, env: { ...process.env, HOME: env.home, FAILPROOFAI_TELEMETRY_DISABLED: "1", FAILPROOFAI_BINARY_OVERRIDE: BINARY_PATH } }, ); // Run `pi list` under the fixture HOME so it doesn't pick up the - // developer's real ~/.pi/agent/settings.json state. - const result = spawnSync("pi", ["list"], { + // developer's real ~/.pi/agent/settings.json state. PI_LIST_ARGS adds + // `--approve` on pi ≥0.80 so `pi list` includes the project package + // we wrote (older pi trusts project settings without it). + const result = spawnSync("pi", PI_LIST_ARGS, { cwd: env.cwd, encoding: "utf8", env: { ...process.env, HOME: env.home }, @@ -469,7 +481,11 @@ describePi("E2E: Pi integration — live `pi list` roundtrip (real binary)", () `bun ${BINARY_PATH} policies --uninstall --cli pi --scope project`, { cwd: env.cwd, env: baseEnv }, ); - const result = spawnSync("pi", ["list"], { + // PI_LIST_ARGS: on pi ≥0.80 `pi list` needs `--approve` to read the + // project-local .pi/settings.json (see the install roundtrip above). + // Without it, it prints "No packages installed." unconditionally, + // making this negative assertion pass vacuously. + const result = spawnSync("pi", PI_LIST_ARGS, { cwd: env.cwd, encoding: "utf8", env: { ...process.env, HOME: env.home },