feat(coding-agent): add experimental feature flag

This commit is contained in:
Vegard Stikbakke
2026-06-08 15:36:26 +02:00
Unverified
parent d8aef0feff
commit 54c49ddeb0
7 changed files with 86 additions and 3 deletions
+1
View File
@@ -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.
+2 -1
View File
@@ -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 |
+12 -2
View File
@@ -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)
+1
View File
@@ -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 |
+1
View File
@@ -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:")}
@@ -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 ?? [])];
}
@@ -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