diff --git a/docs/proposals/client-side-experimentation-framework.md b/docs/proposals/client-side-experimentation-framework.md new file mode 100644 index 0000000000..fd277614f0 --- /dev/null +++ b/docs/proposals/client-side-experimentation-framework.md @@ -0,0 +1,1179 @@ +# Client-Side Experimentation Framework Proposal for Gemini CLI + +> **Status:** Proposal **Author:** Generated Analysis **Date:** January 2026 + +--- + +## Executive Summary + +This document proposes a standardized framework for implementing client-side +experimental features in Gemini CLI. The goal is to enable developers to ship +features that users can opt into in a controlled, standardized way while +protecting users who don't opt-in. + +--- + +## Table of Contents + +1. [Current State Summary](#current-state-summary) +2. [Proposed Design](#proposed-design) +3. [Implementation Details](#implementation-details) +4. [Workflow Integration](#workflow-integration) +5. [User Experience](#user-experience) +6. [Graduation Process](#graduation-process) +7. [Industry Comparison](#industry-comparison) +8. [References](#references) + +--- + +## Current State Summary + +### What Gemini CLI Already Has + +| Capability | Location | Description | +| ------------------------- | ------------------- | --------------------------------------------------- | +| `experimental.*` settings | `settingsSchema.ts` | Boolean toggles for experimental features | +| `general.previewFeatures` | `settingsSchema.ts` | Preview model access control | +| Settings merge hierarchy | `settings.ts` | Schema defaults → System → User → Workspace → Admin | +| Agent experimental flags | `registry.ts` | `definition.experimental` on agent definitions | +| Remote admin controls | `settings.ts` | Server-side overrides for enterprise | + +### Current Experimental Features + +| Feature | Setting | Default | Purpose | +| ---------------------- | ----------------------------------------- | ------- | ------------------------------ | +| Agents | `experimental.enableAgents` | `false` | Enable subagent system | +| JIT Context | `experimental.jitContext` | `false` | Just-in-time context loading | +| Event-Driven Scheduler | `experimental.enableEventDrivenScheduler` | `true` | Event-based task orchestration | +| Extension Reloading | `experimental.extensionReloading` | `false` | Runtime extension loading | +| Extension Config | `experimental.extensionConfig` | `false` | Extension settings management | +| Extension Management | `experimental.extensionManagement` | `true` | Extension management features | +| Plan Mode | `experimental.plan` | `false` | Read-only planning mode | +| OSC 52 Paste | `experimental.useOSC52Paste` | `false` | Remote session clipboard | +| Preview Models | `general.previewFeatures` | `false` | Access to preview models | + +### What's Missing + +- ❌ Standardized lifecycle for experimental features +- ❌ Clear graduation criteria (experimental → stable) +- ❌ Discoverability of experimental features +- ❌ Telemetry integration for experiment usage tracking +- ❌ Documentation automation for experimental features +- ❌ CLI-level opt-in flags (like Cargo's `-Z` flags) +- ❌ Feature dependency management +- ❌ Deprecation tracking and warnings + +--- + +## Proposed Design + +### Multi-Tier Feature Gate System + +#### Tier 1: Feature Lifecycle Stages + +Adopt a **Kubernetes-inspired maturity model** with clear stages: + +| Stage | Default | Stability | Can Remove? | Flag Required | +| -------------- | ------------ | ---------------------------------- | ------------------------ | --------------- | +| **Alpha** | `false` | May break, incomplete | Yes, any time | Explicit opt-in | +| **Beta** | `false` | Mostly stable, collecting feedback | Yes, with deprecation | Explicit opt-in | +| **GA** | `true` | Stable | No (breaking change) | None | +| **Deprecated** | `true→false` | Stable but discouraged | After deprecation period | None | + +#### Tier 2: Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Feature Gate Registry │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Feature Definition │ │ +│ │ - name: string │ │ +│ │ - stage: 'alpha' | 'beta' | 'ga' | 'deprecated' │ │ +│ │ - description: string │ │ +│ │ - owner: string (GitHub team/individual) │ │ +│ │ - trackingIssue: string (GitHub issue URL) │ │ +│ │ - addedIn: string (version) │ │ +│ │ - targetGAVersion?: string │ │ +│ │ - telemetryKey?: string │ │ +│ │ - requiresRestart: boolean │ │ +│ │ - dependencies?: string[] (other feature names) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Activation Methods │ +│ │ +│ 1. Settings File (persistent) │ +│ experimental.: true │ +│ │ +│ 2. CLI Flag (session-only, like Cargo's -Z) │ +│ gemini --feature= --feature= │ +│ gemini -X │ +│ │ +│ 3. Environment Variable (CI/testing) │ +│ GEMINI_FEATURES=, │ +│ │ +│ 4. Admin Override (enterprise) │ +│ Remote admin controls can force-enable/disable │ +└─────────────────────────────────────────────────────────────────┘ +``` + +#### Tier 3: Activation Priority (highest to lowest) + +1. **Admin Override** - Enterprise admins can force-enable or force-disable +2. **CLI Flags** - Session-specific activation via `--feature=X` +3. **Environment Variables** - `GEMINI_FEATURES=X,Y` for CI/testing +4. **Workspace Settings** - `.gemini/settings.json` (if workspace is trusted) +5. **User Settings** - `~/.gemini/settings.json` +6. **System Defaults** - Built-in defaults from feature definition + +--- + +## Implementation Details + +### 1. Feature Gate Registry + +**File:** `packages/core/src/features/featureGate.ts` + +```typescript +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum FeatureStage { + /** Experimental, may break, can be removed any time */ + ALPHA = 'alpha', + /** Mostly stable, collecting feedback, requires deprecation to remove */ + BETA = 'beta', + /** Stable, always enabled, feature gate no longer needed */ + GA = 'ga', + /** Stable but discouraged, will be removed in future version */ + DEPRECATED = 'deprecated', +} + +export interface FeatureDefinition { + /** Unique identifier, kebab-case (e.g., "jit-context") */ + name: string; + + /** Current lifecycle stage */ + stage: FeatureStage; + + /** Human-readable description */ + description: string; + + /** GitHub team or individual responsible */ + owner: string; + + /** GitHub issue URL for tracking */ + trackingIssue?: string; + + /** Version when this feature was added */ + addedInVersion: string; + + /** Target version for GA (if alpha/beta) */ + targetGAVersion?: string; + + /** Version when deprecated (if deprecated) */ + deprecatedInVersion?: string; + + /** Version when feature will be removed (if deprecated) */ + removalVersion?: string; + + /** Whether enabling/disabling requires CLI restart */ + requiresRestart: boolean; + + /** Other features this depends on */ + dependencies?: string[]; + + /** Key for telemetry tracking */ + telemetryKey?: string; + + /** Warning message shown when feature is enabled */ + warningMessage?: string; + + /** Features that are mutually exclusive with this one */ + conflictsWith?: string[]; +} + +/** + * Central registry of all feature gates. + * + * To add a new feature: + * 1. Create a tracking issue + * 2. Add entry here with stage: ALPHA + * 3. Implement feature gated by FeatureGateService.isEnabled() + * 4. Graduate through stages based on feedback + */ +export const FEATURE_GATES: Record = { + 'jit-context': { + name: 'jit-context', + stage: FeatureStage.ALPHA, + description: 'Just-in-time context loading for improved memory usage', + owner: '@anthropics/gemini-cli', + trackingIssue: 'https://github.com/google-gemini/gemini-cli/issues/XXX', + addedInVersion: '0.25.0', + targetGAVersion: '1.0.0', + requiresRestart: true, + telemetryKey: 'feature_jit_context', + warningMessage: + 'JIT context is experimental and may affect response quality.', + }, + + agents: { + name: 'agents', + stage: FeatureStage.ALPHA, + description: 'Enable subagent system for complex multi-step tasks', + owner: '@anthropics/gemini-cli', + trackingIssue: 'https://github.com/google-gemini/gemini-cli/issues/XXX', + addedInVersion: '0.20.0', + requiresRestart: false, + telemetryKey: 'feature_agents', + warningMessage: 'Agents run in YOLO mode and will auto-approve tool calls.', + }, + + 'plan-mode': { + name: 'plan-mode', + stage: FeatureStage.BETA, + description: + 'Read-only planning mode for reviewing changes before execution', + owner: '@anthropics/gemini-cli', + trackingIssue: 'https://github.com/google-gemini/gemini-cli/issues/XXX', + addedInVersion: '0.22.0', + targetGAVersion: '0.30.0', + requiresRestart: false, + telemetryKey: 'feature_plan_mode', + }, + + 'event-driven-scheduler': { + name: 'event-driven-scheduler', + stage: FeatureStage.BETA, + description: 'Event-based task orchestration system', + owner: '@anthropics/gemini-cli', + addedInVersion: '0.18.0', + requiresRestart: true, + telemetryKey: 'feature_event_scheduler', + }, + + // Example of a deprecated feature + 'legacy-context-loading': { + name: 'legacy-context-loading', + stage: FeatureStage.DEPRECATED, + description: 'Legacy context loading (use jit-context instead)', + owner: '@anthropics/gemini-cli', + addedInVersion: '0.10.0', + deprecatedInVersion: '0.25.0', + removalVersion: '1.0.0', + requiresRestart: true, + warningMessage: + 'This feature is deprecated and will be removed in v1.0.0. Migrate to jit-context.', + }, +}; + +/** + * Get all features at a specific stage + */ +export function getFeaturesByStage(stage: FeatureStage): FeatureDefinition[] { + return Object.values(FEATURE_GATES).filter((f) => f.stage === stage); +} + +/** + * Get a feature definition by name + */ +export function getFeature(name: string): FeatureDefinition | undefined { + return FEATURE_GATES[name]; +} + +/** + * Check if a feature name is valid + */ +export function isValidFeature(name: string): boolean { + return name in FEATURE_GATES; +} +``` + +### 2. Feature Gate Service + +**File:** `packages/core/src/features/featureGateService.ts` + +```typescript +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + FEATURE_GATES, + FeatureStage, + getFeature, + isValidFeature, + type FeatureDefinition, +} from './featureGate.js'; +import { debugLogger } from '../utils/debugLogger.js'; + +export interface FeatureGateServiceConfig { + /** Features enabled via settings file */ + settingsFeatures: Record; + + /** Features enabled via CLI flags */ + cliFeatures: string[]; + + /** Features enabled via environment variable */ + envFeatures: string[]; + + /** Admin overrides (highest priority) */ + adminOverrides: Record; +} + +export class FeatureGateService { + private readonly enabledFeatures: Set = new Set(); + private readonly config: FeatureGateServiceConfig; + + constructor(config: FeatureGateServiceConfig) { + this.config = config; + this.computeEnabledFeatures(); + } + + /** + * Check if a feature is enabled + */ + isEnabled(featureName: string): boolean { + const definition = getFeature(featureName); + + if (!definition) { + debugLogger.warn(`Unknown feature gate: ${featureName}`); + return false; + } + + // GA features are always enabled + if (definition.stage === FeatureStage.GA) { + return true; + } + + // Admin override takes highest priority + if (this.config.adminOverrides[featureName] !== undefined) { + return this.config.adminOverrides[featureName]; + } + + return this.enabledFeatures.has(featureName); + } + + /** + * Get all currently enabled features + */ + getEnabledFeatures(): FeatureDefinition[] { + return [...this.enabledFeatures] + .map((name) => getFeature(name)) + .filter((f): f is FeatureDefinition => f !== undefined); + } + + /** + * Get enabled features at a specific stage + */ + getEnabledFeaturesAtStage(stage: FeatureStage): FeatureDefinition[] { + return this.getEnabledFeatures().filter((f) => f.stage === stage); + } + + /** + * Get warnings for enabled experimental features + */ + getStartupWarnings(): string[] { + const warnings: string[] = []; + + for (const feature of this.getEnabledFeatures()) { + if (feature.stage === FeatureStage.ALPHA && feature.warningMessage) { + warnings.push(`⚠️ [ALPHA] ${feature.name}: ${feature.warningMessage}`); + } else if ( + feature.stage === FeatureStage.DEPRECATED && + feature.warningMessage + ) { + warnings.push( + `⚠️ [DEPRECATED] ${feature.name}: ${feature.warningMessage}`, + ); + } + } + + return warnings; + } + + /** + * Validate feature dependencies + */ + validateDependencies(): string[] { + const errors: string[] = []; + + for (const featureName of this.enabledFeatures) { + const feature = getFeature(featureName); + if (!feature?.dependencies) continue; + + for (const dep of feature.dependencies) { + if (!this.isEnabled(dep)) { + errors.push( + `Feature "${featureName}" requires "${dep}" to be enabled`, + ); + } + } + + // Check for conflicts + if (feature.conflictsWith) { + for (const conflict of feature.conflictsWith) { + if (this.isEnabled(conflict)) { + errors.push( + `Feature "${featureName}" conflicts with "${conflict}"`, + ); + } + } + } + } + + return errors; + } + + private computeEnabledFeatures(): void { + this.enabledFeatures.clear(); + + // Process in order of priority (lowest to highest) + // 1. Settings file + for (const [name, enabled] of Object.entries( + this.config.settingsFeatures, + )) { + if (enabled && isValidFeature(name)) { + this.enabledFeatures.add(name); + } + } + + // 2. Environment variables + for (const name of this.config.envFeatures) { + if (isValidFeature(name)) { + this.enabledFeatures.add(name); + } else { + debugLogger.warn(`Unknown feature in GEMINI_FEATURES: ${name}`); + } + } + + // 3. CLI flags (can also disable with --no-feature=X) + for (const name of this.config.cliFeatures) { + if (name.startsWith('no-')) { + const featureName = name.slice(3); + this.enabledFeatures.delete(featureName); + } else if (isValidFeature(name)) { + this.enabledFeatures.add(name); + } else { + debugLogger.warn(`Unknown feature flag: ${name}`); + } + } + + // Note: Admin overrides are checked at runtime in isEnabled() + } +} +``` + +### 3. CLI Flag Support + +**File:** `packages/cli/src/config/config.ts` (additions) + +```typescript +// Add to yargs options +.option('feature', { + alias: 'X', + type: 'array', + string: true, + description: 'Enable experimental feature(s) for this session. Use --feature= or -X ', + coerce: (features: string[]) => + features.flatMap(f => f.split(',').map(x => x.trim())), +}) +.option('no-feature', { + type: 'array', + string: true, + description: 'Disable a feature for this session', + coerce: (features: string[]) => + features.flatMap(f => f.split(',').map(x => x.trim())), +}) +.option('list-features', { + type: 'boolean', + description: 'List all available feature gates and exit', +}) +``` + +### 4. Environment Variable Support + +```typescript +// In loadCliConfig or similar +function parseEnvFeatures(): string[] { + const envValue = process.env['GEMINI_FEATURES']; + if (!envValue) return []; + + return envValue + .split(',') + .map((f) => f.trim()) + .filter((f) => f.length > 0); +} +``` + +### 5. `/features` Slash Command + +**File:** `packages/cli/src/commands/features.ts` + +```typescript +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + FEATURE_GATES, + FeatureStage, + getFeaturesByStage, +} from '@google/gemini-cli-core'; +import type { Config } from '@google/gemini-cli-core'; + +export async function featuresCommand(config: Config): Promise { + const featureService = config.getFeatureGateService(); + + console.log('\n📋 Gemini CLI Feature Gates\n'); + console.log( + 'Feature gates allow you to opt-in to experimental functionality.\n', + ); + + // Alpha features + const alphaFeatures = getFeaturesByStage(FeatureStage.ALPHA); + if (alphaFeatures.length > 0) { + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('ALPHA Features (experimental, may change without notice)'); + console.log( + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n', + ); + + for (const feature of alphaFeatures) { + const enabled = featureService.isEnabled(feature.name); + const status = enabled ? '✓ ENABLED' : '○ disabled'; + + console.log(` ${feature.name}`); + console.log(` Status: ${status}`); + console.log(` ${feature.description}`); + if (feature.trackingIssue) { + console.log(` Tracking: ${feature.trackingIssue}`); + } + if (feature.targetGAVersion) { + console.log(` Target GA: v${feature.targetGAVersion}`); + } + console.log(); + } + } + + // Beta features + const betaFeatures = getFeaturesByStage(FeatureStage.BETA); + if (betaFeatures.length > 0) { + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('BETA Features (mostly stable, feedback welcome)'); + console.log( + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n', + ); + + for (const feature of betaFeatures) { + const enabled = featureService.isEnabled(feature.name); + const status = enabled ? '✓ ENABLED' : '○ disabled'; + + console.log(` ${feature.name}`); + console.log(` Status: ${status}`); + console.log(` ${feature.description}`); + if (feature.trackingIssue) { + console.log(` Tracking: ${feature.trackingIssue}`); + } + console.log(); + } + } + + // Deprecated features + const deprecatedFeatures = getFeaturesByStage(FeatureStage.DEPRECATED); + if (deprecatedFeatures.length > 0) { + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('DEPRECATED Features (will be removed)'); + console.log( + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n', + ); + + for (const feature of deprecatedFeatures) { + const enabled = featureService.isEnabled(feature.name); + const status = enabled ? '✓ ENABLED' : '○ disabled'; + + console.log(` ${feature.name}`); + console.log(` Status: ${status}`); + console.log(` ${feature.description}`); + if (feature.removalVersion) { + console.log(` ⚠️ Will be removed in: v${feature.removalVersion}`); + } + console.log(); + } + } + + // Usage instructions + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('How to Enable Features'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + console.log(' CLI flag (session only):'); + console.log(' gemini --feature='); + console.log(' gemini -X '); + console.log(); + console.log(' Environment variable (CI/testing):'); + console.log(' GEMINI_FEATURES=, gemini'); + console.log(); + console.log(' Settings file (persistent):'); + console.log(' Add to ~/.gemini/settings.json:'); + console.log(' { "experimental": { "": true } }'); + console.log(); +} +``` + +### 6. Settings Schema Auto-Generation + +**File:** `packages/cli/src/config/settingsSchema.ts` (additions) + +```typescript +import { FEATURE_GATES, FeatureStage } from '@google/gemini-cli-core'; + +/** + * Auto-generate experimental settings schema from feature definitions. + * This ensures the settings schema stays in sync with feature gates. + */ +function generateExperimentalSchema(): Record { + const schema: Record = {}; + + for (const [name, def] of Object.entries(FEATURE_GATES)) { + // GA features don't need settings (always enabled) + if (def.stage === FeatureStage.GA) continue; + + // Convert kebab-case to camelCase for settings + const settingName = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + + schema[settingName] = { + type: 'boolean', + default: false, + label: `[${def.stage.toUpperCase()}] ${def.name}`, + description: + def.description + + (def.warningMessage ? ` ⚠️ ${def.warningMessage}` : ''), + showInDialog: def.stage === FeatureStage.BETA, // Only show beta in settings UI + requiresRestart: def.requiresRestart, + ignoreInDocs: def.stage === FeatureStage.ALPHA, // Don't document alpha features + }; + } + + return schema; +} + +// Use in schema definition +export const settingsSchema = { + // ... other settings ... + + experimental: { + type: 'object', + label: 'Experimental Features', + description: + 'Enable experimental features. Use /features to see all available.', + properties: generateExperimentalSchema(), + }, +}; +``` + +### 7. Telemetry Integration + +**File:** `packages/core/src/telemetry/featureTelemetry.ts` + +```typescript +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getFeature } from '../features/featureGate.js'; +import type { Config } from '../config/config.js'; + +export interface FeatureUsageEvent { + featureName: string; + stage: string; + action: 'enabled' | 'used' | 'error'; + clientVersion: string; + sessionId: string; + timestamp: string; +} + +/** + * Log when a feature is enabled at startup + */ +export function logFeatureEnabled(config: Config, featureName: string): void { + const definition = getFeature(featureName); + if (!definition?.telemetryKey) return; + + // Use existing telemetry infrastructure + config.getTelemetryService()?.recordEvent({ + name: 'feature_enabled', + attributes: { + feature: featureName, + stage: definition.stage, + version: config.getClientVersion(), + }, + }); +} + +/** + * Log when a feature is actively used + */ +export function logFeatureUsed(config: Config, featureName: string): void { + const definition = getFeature(featureName); + if (!definition?.telemetryKey) return; + + config.getTelemetryService()?.recordEvent({ + name: 'feature_used', + attributes: { + feature: featureName, + stage: definition.stage, + version: config.getClientVersion(), + }, + }); +} +``` + +--- + +## Workflow Integration + +### 1. Feature Tracking Issue Template + +**File:** `.github/ISSUE_TEMPLATE/feature-gate.yml` + +```yaml +name: Feature Gate Proposal +description: Propose a new experimental feature gate +title: '[Feature Gate] ' +labels: ['type/feature-gate', 'stage/alpha'] +body: + - type: markdown + attributes: + value: | + ## Feature Gate Proposal + + Use this template to propose a new experimental feature for Gemini CLI. + All new features should start as Alpha and graduate through stages. + + - type: input + id: feature-name + attributes: + label: Feature Name + description: Lowercase, kebab-case identifier (e.g., "jit-context") + placeholder: my-feature-name + validations: + required: true + + - type: dropdown + id: initial-stage + attributes: + label: Initial Stage + description: Most features should start as Alpha + options: + - alpha + - beta + default: 0 + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: What does this feature do? (This will be shown to users) + placeholder: A brief description of the feature's functionality + validations: + required: true + + - type: textarea + id: motivation + attributes: + label: Motivation + description: Why is this feature needed? What problem does it solve? + validations: + required: true + + - type: input + id: target-ga + attributes: + label: Target GA Version + description: When do you expect this to be stable? (e.g., "1.0.0") + placeholder: '1.0.0' + + - type: textarea + id: graduation-criteria + attributes: + label: Graduation Criteria + description: What needs to happen for this to move from Alpha → Beta → GA? + value: | + ### Alpha → Beta + - [ ] Feature is functionally complete + - [ ] No critical bugs reported for 2 weeks + - [ ] Basic documentation exists + - [ ] Telemetry shows stable usage patterns + + ### Beta → GA + - [ ] Feature has been in Beta for 4+ weeks + - [ ] Documentation is complete + - [ ] No breaking changes planned + - [ ] Team consensus achieved + validations: + required: true + + - type: textarea + id: warning-message + attributes: + label: Warning Message + description: Optional warning shown when users enable this feature + placeholder: 'This feature may affect performance in large codebases.' + + - type: checkboxes + id: requirements + attributes: + label: Requirements + options: + - label: Requires CLI restart when toggled + - label: Has dependencies on other features + - label: Conflicts with other features +``` + +### 2. Automated Documentation Workflow + +**File:** `.github/workflows/docs-features.yml` + +```yaml +name: Update Feature Documentation + +on: + push: + branches: [main] + paths: + - 'packages/core/src/features/featureGate.ts' + workflow_dispatch: + +jobs: + update-docs: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate feature documentation + run: npm run docs:generate-features + + - name: Check for changes + id: changes + run: | + if [[ -n $(git status --porcelain docs/experimental-features.md) ]]; then + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'docs: Update experimental features documentation' + title: 'docs: Update experimental features documentation' + body: | + This PR was automatically generated to update the experimental features documentation. + + The feature gate definitions in `packages/core/src/features/featureGate.ts` have changed. + branch: docs/update-features + labels: documentation,automated +``` + +### 3. Feature Gate Validation in CI + +**File:** `.github/workflows/ci.yml` (additions) + +```yaml +validate-features: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Validate feature gates + run: npm run validate:features + # This script should: + # - Check all features have tracking issues + # - Check deprecated features have removal versions + # - Check feature names follow conventions + # - Check for orphaned feature flags in code +``` + +--- + +## User Experience + +### Enabling via CLI Flag + +```bash +# Single feature +gemini --feature=jit-context "Analyze this codebase" + +# Short form +gemini -X jit-context "Analyze this codebase" + +# Multiple features +gemini --feature=jit-context --feature=agents "Help me refactor" + +# Comma-separated +gemini -X jit-context,agents + +# Disable a feature for this session +gemini --no-feature=event-driven-scheduler +``` + +### Enabling via Environment Variable + +```bash +# For CI/CD or testing +GEMINI_FEATURES=jit-context,agents gemini -p "Run tests" + +# In a shell profile for persistent enablement +export GEMINI_FEATURES=jit-context +``` + +### Enabling via Settings File + +```json +// ~/.gemini/settings.json +{ + "experimental": { + "jitContext": true, + "agents": true, + "planMode": true + } +} +``` + +### Listing Features + +``` +$ gemini --list-features + +📋 Gemini CLI Feature Gates + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +ALPHA Features (experimental, may change without notice) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + jit-context + Status: ○ disabled + Just-in-time context loading for improved memory usage + Tracking: https://github.com/google-gemini/gemini-cli/issues/XXX + Target GA: v1.0.0 + + agents + Status: ○ disabled + Enable subagent system for complex multi-step tasks + Tracking: https://github.com/google-gemini/gemini-cli/issues/XXX + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +BETA Features (mostly stable, feedback welcome) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + plan-mode + Status: ○ disabled + Read-only planning mode for reviewing changes before execution + Tracking: https://github.com/google-gemini/gemini-cli/issues/XXX + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +How to Enable Features +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + CLI flag (session only): + gemini --feature= + gemini -X + + Environment variable (CI/testing): + GEMINI_FEATURES=, gemini + + Settings file (persistent): + Add to ~/.gemini/settings.json: + { "experimental": { "": true } } +``` + +### Startup Warnings + +``` +$ gemini --feature=agents + +⚠️ Experimental features enabled: + • [ALPHA] agents: Agents run in YOLO mode and will auto-approve tool calls. + +Type /features to learn more about experimental features. + +> +``` + +--- + +## Graduation Process + +### Stage Transitions + +``` + ┌─────────────┐ + │ ALPHA │ + │ (2+ weeks) │ + └──────┬──────┘ + │ + ┌──────────────┼──────────────┐ + │ │ │ + ▼ │ ▼ + ┌─────────────┐ │ ┌─────────────┐ + │ BETA │ │ │ REMOVED │ + │ (4+ weeks) │ │ │ (failed) │ + └──────┬──────┘ │ └─────────────┘ + │ │ + │ │ + ▼ │ + ┌─────────────┐ │ + │ GA │◄──────┘ + │ (stable) │ + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ DEPRECATED │ + │ (optional) │ + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ REMOVED │ + └─────────────┘ +``` + +### Alpha → Beta Criteria + +- [ ] Feature is functionally complete +- [ ] At least 2 weeks since Alpha release +- [ ] No critical bugs reported +- [ ] Basic documentation exists +- [ ] Telemetry shows no major issues +- [ ] Tracking issue updated with feedback summary + +### Beta → GA Criteria + +- [ ] At least 4 weeks in Beta +- [ ] Telemetry shows stable usage patterns +- [ ] Documentation is complete +- [ ] No breaking changes planned +- [ ] Team consensus in tracking issue +- [ ] Feature gate can be removed (always enabled) + +### GA → Deprecated Criteria + +- [ ] Replacement feature available (if applicable) +- [ ] Deprecation warning added to feature definition +- [ ] Migration guide published +- [ ] Removal version announced (typically N+2 major versions) + +### Deprecation Timeline + +| Action | Timeline | +| ------------------------ | ----------- | +| Mark as deprecated | Version N | +| Log deprecation warnings | Version N | +| Disable by default | Version N+1 | +| Remove feature | Version N+2 | + +--- + +## Industry Comparison + +| Aspect | Gemini CLI (Proposed) | [Cargo (Rust)](https://doc.rust-lang.org/cargo/reference/unstable.html) | [Kubernetes](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/) | +| ------------------ | ------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| **Stages** | Alpha/Beta/GA/Deprecated | Unstable/Stable | Alpha/Beta/GA | +| **CLI Flag** | `--feature=X` / `-X` | `-Z flag` | `--feature-gates=X=true` | +| **Config File** | `experimental.X: true` | `[unstable] X = true` | Component flags | +| **Default Off** | Alpha, Beta | All unstable | Alpha only | +| **Nightly Only** | No (available in all) | Yes | No | +| **Tracking** | GitHub Issues | Tracking Issues | KEPs | +| **Telemetry** | Optional | None | Metrics endpoint | +| **Admin Override** | Yes | No | No | + +--- + +## References + +### Open Source Feature Flag Tools + +- [OpenFeature](https://openfeature.dev/) - Vendor-agnostic feature flagging + specification (CNCF) +- [GrowthBook](https://www.growthbook.io/) - Open source feature flags and A/B + testing +- [Flagsmith](https://www.flagsmith.com/) - Open source feature flag service +- [Unleash](https://www.getunleash.io/) - Open source feature management + +### CLI Tool Implementations + +- [Cargo Unstable Features](https://doc.rust-lang.org/cargo/reference/unstable.html) - + Rust's package manager experimental features +- [Kubernetes Feature Gates](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/) - + K8s feature gate system +- [Feature Flags Best Practices - LaunchDarkly](https://launchdarkly.com/blog/what-are-feature-flags/) + +### Internal References + +- Current experimental settings: `packages/cli/src/config/settingsSchema.ts` +- Settings merge logic: `packages/cli/src/config/settings.ts` +- Agent experimental flags: `packages/core/src/agents/registry.ts` +- Release workflows: `.github/workflows/release-*.yml` + +--- + +## Appendix: Migration Path + +### Migrating Existing Experimental Features + +The following existing experimental settings should be migrated to the new +feature gate system: + +| Current Setting | New Feature Gate | Stage | +| ----------------------------------------- | ------------------------ | ----- | +| `experimental.enableAgents` | `agents` | Alpha | +| `experimental.jitContext` | `jit-context` | Alpha | +| `experimental.plan` | `plan-mode` | Beta | +| `experimental.enableEventDrivenScheduler` | `event-driven-scheduler` | Beta | +| `experimental.extensionReloading` | `extension-reloading` | Alpha | +| `experimental.extensionConfig` | `extension-config` | Alpha | +| `experimental.useOSC52Paste` | `osc52-paste` | Alpha | +| `general.previewFeatures` | `preview-models` | Beta | + +A migration script should: + +1. Read existing settings +2. Map to new feature gate names +3. Preserve user preferences +4. Log migration actions diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 8db9175adb..d8a6dc9caa 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -29,10 +29,11 @@ vi.mock('./settings.js', async (importActual) => { }); // Mock trustedFolders +import * as trustedFolders from './trustedFolders.js'; vi.mock('./trustedFolders.js', () => ({ - isWorkspaceTrusted: vi - .fn() - .mockReturnValue({ isTrusted: true, source: 'file' }), + isWorkspaceTrusted: vi.fn(), + isFolderTrustEnabled: vi.fn(), + loadTrustedFolders: vi.fn(), })); vi.mock('./settingsSchema.js', async (importOriginal) => { @@ -151,7 +152,7 @@ describe('Settings Loading and Merging', () => { (mockFsExistsSync as Mock).mockReturnValue(false); (fs.readFileSync as Mock).mockReturnValue('{}'); // Return valid empty JSON (mockFsMkdirSync as Mock).mockImplementation(() => undefined); - vi.mocked(isWorkspaceTrusted).mockReturnValue({ + vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({ isTrusted: true, source: 'file', }); @@ -1635,7 +1636,7 @@ describe('Settings Loading and Merging', () => { }); it('should NOT merge workspace settings when workspace is not trusted', () => { - vi.mocked(isWorkspaceTrusted).mockReturnValue({ + vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({ isTrusted: false, source: 'file', }); @@ -1666,17 +1667,49 @@ describe('Settings Loading and Merging', () => { expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting expect(settings.merged.ui?.theme).toBe('dark'); // User setting }); + + it('should NOT merge workspace settings when workspace trust is undefined', () => { + vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({ + isTrusted: undefined, + source: undefined, + }); + (mockFsExistsSync as Mock).mockReturnValue(true); + const userSettingsContent = { + ui: { theme: 'dark' }, + tools: { sandbox: false }, + context: { fileName: 'USER.md' }, + }; + const workspaceSettingsContent = { + tools: { sandbox: true }, + context: { fileName: 'WORKSPACE.md' }, + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(workspaceSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + + expect(settings.merged.tools?.sandbox).toBe(false); // User setting + expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting + }); }); describe('loadEnvironment', () => { function setup({ isFolderTrustEnabled = true, - isWorkspaceTrustedValue = true, + isWorkspaceTrustedValue = true as boolean | undefined, }) { delete process.env['TESTTEST']; // reset const geminiEnvPath = path.join(MOCK_WORKSPACE_DIR, GEMINI_DIR, '.env'); - vi.mocked(isWorkspaceTrusted).mockReturnValue({ + vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({ isTrusted: isWorkspaceTrustedValue, source: 'file', }); @@ -1708,20 +1741,34 @@ describe('Settings Loading and Merging', () => { it('sets environment variables from .env files', () => { setup({ isFolderTrustEnabled: false, isWorkspaceTrustedValue: true }); - loadEnvironment( - loadSettings(MOCK_WORKSPACE_DIR).merged, - MOCK_WORKSPACE_DIR, - ); + const settings = { + security: { folderTrust: { enabled: false } }, + } as Settings; + loadEnvironment(settings, MOCK_WORKSPACE_DIR, isWorkspaceTrusted); expect(process.env['TESTTEST']).toEqual('1234'); }); it('does not load env files from untrusted spaces', () => { setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: false }); - loadEnvironment( - loadSettings(MOCK_WORKSPACE_DIR).merged, - MOCK_WORKSPACE_DIR, - ); + const settings = { + security: { folderTrust: { enabled: true } }, + } as Settings; + loadEnvironment(settings, MOCK_WORKSPACE_DIR, isWorkspaceTrusted); + + expect(process.env['TESTTEST']).not.toEqual('1234'); + }); + + it('does not load env files when trust is undefined', () => { + delete process.env['TESTTEST']; + // isWorkspaceTrusted returns {isTrusted: undefined} for matched rules with no trust value, or no matching rules. + setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: undefined }); + const settings = { + security: { folderTrust: { enabled: true } }, + } as Settings; + + const mockTrustFn = vi.fn().mockReturnValue({ isTrusted: undefined }); + loadEnvironment(settings, MOCK_WORKSPACE_DIR, mockTrustFn); expect(process.env['TESTTEST']).not.toEqual('1234'); }); @@ -1737,7 +1784,7 @@ describe('Settings Loading and Merging', () => { mockFsExistsSync.mockReturnValue(true); mockFsReadFileSync = vi.mocked(fs.readFileSync); mockFsReadFileSync.mockReturnValue('{}'); - vi.mocked(isWorkspaceTrusted).mockReturnValue({ + vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({ isTrusted: true, source: undefined, }); diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index f2513b7539..9242c56d0d 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -437,11 +437,12 @@ export function setUpCloudShellEnvironment(envFilePath: string | null): void { export function loadEnvironment( settings: Settings, workspaceDir: string, + isWorkspaceTrustedFn = isWorkspaceTrusted, ): void { const envFilePath = findEnvFile(workspaceDir); - const trustResult = isWorkspaceTrusted(settings, workspaceDir); + const trustResult = isWorkspaceTrustedFn(settings, workspaceDir); - if (trustResult.isTrusted === false) { + if (trustResult.isTrusted !== true) { return; } @@ -607,7 +608,7 @@ export function loadSettings( ); const isTrusted = isWorkspaceTrusted(initialTrustCheckSettings as Settings, workspaceDir) - .isTrusted ?? true; + .isTrusted ?? false; // Create a temporary merged settings object to pass to loadEnvironment. const tempMergedSettings = mergeSettings(