diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 83b6fbc67..ad2a5cbc0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added the global `experimentalFeatures` setting and `PI_EXPERIMENTAL` environment override for early feature flags. - Added a `project_trust` extension event so global and CLI extensions can decide or defer project trust during startup and runtime cwd switches. - Added project trust gating for project-local settings, resources, instructions, and packages ([#5332](https://github.com/earendil-works/pi/pull/5332)). - Added the latest prompt cache hit rate to the interactive footer. diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 202b6f3e9..5b0948c84 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -285,7 +285,7 @@ Use `/settings` to modify common options, or edit JSON files directly: | Location | Scope | |----------|-------| | `~/.pi/agent/settings.json` | Global (all projects) | -| `.pi/settings.json` | Project (overrides global) | +| `.pi/settings.json` | Project (overrides global, except global-only settings) | See [docs/settings.md](docs/settings.md) for all options. @@ -662,6 +662,7 @@ pi --thinking high "Solve this complex problem" | `PI_OFFLINE` | Disable startup network operations, including update checks, package update checks, and install/update telemetry | | `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request | | `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers. Use `1`/`true`/`yes` to enable or `0`/`false`/`no` to disable. This does not disable update checks | +| `PI_EXPERIMENTAL` | Override `experimentalFeatures` for early features. Use `1`/`true`/`yes` to enable or `0`/`false`/`no` to disable | | `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) | | `VISUAL`, `EDITOR` | External editor for Ctrl+G | diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index df2d0bd61..b95178a9a 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -1,6 +1,6 @@ # Settings -Pi uses JSON settings files with project settings overriding global settings. +Pi uses JSON settings files with project settings overriding global settings unless a setting notes otherwise. | Location | Scope | |----------|-------| @@ -64,6 +64,16 @@ Use `/trust` in interactive mode to save a project trust decision for future ses Set `PI_SKIP_VERSION_CHECK=1` to disable the Pi version update check. Use `--offline` or `PI_OFFLINE=1` to disable all startup network operations described here, including update checks, package update checks, and install/update telemetry. +### Experimental Features + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `experimentalFeatures` | boolean | `false` | Enable early features that may change or break | + +`experimentalFeatures` is global-only. Set it in `~/.pi/agent/settings.json`; `.pi/settings.json` is ignored for this setting. It is not shown in `/settings`. + +Set `PI_EXPERIMENTAL=1` (or `true`/`yes`) to enable experimental features for one process. If `PI_EXPERIMENTAL` is set, it overrides `experimentalFeatures`; use `0`, `false`, or `no` to force-disable. + ### Warnings | Setting | Type | Default | Description | @@ -269,7 +279,7 @@ See [packages.md](packages.md) for package management details. ## Project Overrides -Project settings (`.pi/settings.json`) override global settings. Nested objects are merged: +Project settings (`.pi/settings.json`) override global settings except for global-only settings such as `experimentalFeatures`. Nested objects are merged: ```json // ~/.pi/agent/settings.json (global) diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index ad9b80e14..91d03eed9 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -288,6 +288,7 @@ pi --exclude-tools ask_question | `PI_OFFLINE` | Disable startup network operations, including update checks, package update checks, and install/update telemetry | | `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request | | `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no`. This does not disable update checks | +| `PI_EXPERIMENTAL` | Override `experimentalFeatures` for early features: `1`/`true`/`yes` or `0`/`false`/`no` | | `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache where supported | | `VISUAL`, `EDITOR` | External editor for Ctrl+G | diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 0258c0c1a..659dd551d 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -378,6 +378,7 @@ ${chalk.bold("Environment Variables:")} PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths) PI_OFFLINE - Disable startup network operations when set to 1/true/yes PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no + PI_EXPERIMENTAL - Override early features when set to 1/true/yes or 0/false/no PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/) ${chalk.bold("Built-in Tool Names:")} diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 2ef32d6d6..34b60ebf2 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -93,6 +93,7 @@ export interface Settings { npmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., ["mise", "exec", "node@20", "--", "npm"]) collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full) enableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates + experimentalFeatures?: boolean; // default: false - global-only opt-in for early features; overridden by PI_EXPERIMENTAL packages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering) extensions?: string[]; // Array of local extension file paths or directories skills?: string[]; // Array of local skill file paths or directories @@ -893,6 +894,21 @@ export class SettingsManager { this.save(); } + getExperimentalFeaturesEnabled(): boolean { + const envValue = process.env.PI_EXPERIMENTAL; + if (envValue !== undefined) { + const normalized = envValue.toLowerCase(); + return envValue === "1" || normalized === "true" || normalized === "yes"; + } + return this.globalSettings.experimentalFeatures === true; + } + + setExperimentalFeaturesEnabled(enabled: boolean): void { + this.globalSettings.experimentalFeatures = enabled; + this.markModified("experimentalFeatures"); + this.save(); + } + getPackages(): PackageSource[] { return [...(this.settings.packages ?? [])]; } diff --git a/packages/coding-agent/test/settings-manager.test.ts b/packages/coding-agent/test/settings-manager.test.ts index b28d086a1..b0ebd455e 100644 --- a/packages/coding-agent/test/settings-manager.test.ts +++ b/packages/coding-agent/test/settings-manager.test.ts @@ -9,8 +9,10 @@ describe("SettingsManager", () => { const testDir = join(process.cwd(), "test-settings-tmp"); const agentDir = join(testDir, "agent"); const projectDir = join(testDir, "project"); + const originalPiExperimental = process.env.PI_EXPERIMENTAL; beforeEach(() => { + delete process.env.PI_EXPERIMENTAL; // Clean up and create fresh directories if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); @@ -20,6 +22,11 @@ describe("SettingsManager", () => { }); afterEach(() => { + if (originalPiExperimental === undefined) { + delete process.env.PI_EXPERIMENTAL; + } else { + process.env.PI_EXPERIMENTAL = originalPiExperimental; + } if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } @@ -252,6 +259,52 @@ describe("SettingsManager", () => { }); }); + describe("experimentalFeatures", () => { + it("should default to false", () => { + const manager = SettingsManager.create(projectDir, agentDir); + + expect(manager.getExperimentalFeaturesEnabled()).toBe(false); + }); + + it("should load the global setting", () => { + writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ experimentalFeatures: true })); + const manager = SettingsManager.create(projectDir, agentDir); + + expect(manager.getExperimentalFeaturesEnabled()).toBe(true); + }); + + it("should ignore project settings", () => { + writeFileSync(join(projectDir, ".pi", "settings.json"), JSON.stringify({ experimentalFeatures: true })); + const manager = SettingsManager.create(projectDir, agentDir); + + expect(manager.getExperimentalFeaturesEnabled()).toBe(false); + }); + + it("should let PI_EXPERIMENTAL enable experimental features", () => { + process.env.PI_EXPERIMENTAL = "1"; + const manager = SettingsManager.create(projectDir, agentDir); + + expect(manager.getExperimentalFeaturesEnabled()).toBe(true); + }); + + it("should let PI_EXPERIMENTAL override the global setting", () => { + process.env.PI_EXPERIMENTAL = "0"; + writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ experimentalFeatures: true })); + const manager = SettingsManager.create(projectDir, agentDir); + + expect(manager.getExperimentalFeaturesEnabled()).toBe(false); + }); + + it("should save to global settings", async () => { + const settingsPath = join(agentDir, "settings.json"); + const manager = SettingsManager.create(projectDir, agentDir); + manager.setExperimentalFeaturesEnabled(true); + await manager.flush(); + + expect(JSON.parse(readFileSync(settingsPath, "utf-8")).experimentalFeatures).toBe(true); + }); + }); + describe("project settings directory creation", () => { it("should not create .pi folder when only reading project settings", () => { // Create agent dir with global settings, but NO .pi folder in project