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 CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<cwd>/.pi/` for project scope). For dogfood we write
Expand Down
32 changes: 24 additions & 8 deletions __tests__/e2e/hooks/pi-integration.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
NiveditJain marked this conversation as resolved.
} 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();
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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 },
Expand All @@ -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 },
Expand Down