From 50f71a8df5ac8d8a728c2d08fba70b28f98dd2e3 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Thu, 19 Feb 2026 14:58:03 -0600 Subject: [PATCH] feat(skills): add experimentation skill and metadata Adds a new experimentation skill to allow users to easily manage, view, and override remote Gemini CLI experiments locally. This also introduces `ExperimentMetadata` to `flagNames.ts` to expose descriptions and default values for each experiment flag. --- .gemini/skills/experimentation/SKILL.md | 46 +++++++++++ .../scripts/override_experiment.cjs | 72 ++++++++++++++++++ experimentation.skill | Bin 0 -> 2168 bytes .../src/code_assist/experiments/flagNames.ts | 64 ++++++++++++++++ 4 files changed, 182 insertions(+) create mode 100644 .gemini/skills/experimentation/SKILL.md create mode 100644 .gemini/skills/experimentation/scripts/override_experiment.cjs create mode 100644 experimentation.skill diff --git a/.gemini/skills/experimentation/SKILL.md b/.gemini/skills/experimentation/SKILL.md new file mode 100644 index 0000000000..d3db0eb44e --- /dev/null +++ b/.gemini/skills/experimentation/SKILL.md @@ -0,0 +1,46 @@ +--- +name: experimentation +description: Manage, view, and override Gemini CLI remote experiments and feature flags locally. +--- + +# Experimentation Skill + +This skill allows you to safely manage, view, and locally override remote Gemini CLI experiments (feature flags). + +## Core Concepts + +Remote experimentation is enabled by default. `packages/core/src/code_assist/experiments/flagNames.ts` contains the active `ExperimentFlags` and `ExperimentMetadata` which describe each flag's purpose, type, and default value. + +Currently, Gemini CLI supports local overrides using the `GEMINI_EXP` environment variable pointing to a JSON file. + +## Workflow: Viewing Experiments + +When a user asks what experiments are active or available: +1. Search `packages/core/src/code_assist/experiments/flagNames.ts` for `ExperimentMetadata`. +2. Extract the descriptions, types, and defaults to answer the user's questions. +3. Check if there is an active local override file at `.gemini/experiments.json`. + +## Workflow: Local Overrides & Opt-Out + +When a user wants to override a flag locally (e.g., to turn off a preview feature) or opt-out: + +1. Use the `scripts/override_experiment.cjs` script bundled with this skill to safely update or create `.gemini/experiments.json`. +2. When the user asks to "opt out of experiments", use the script to set `experimentIds` to an empty array and clear flags, ensuring a sensible baseline. +3. **Important:** After updating the JSON file, instruct the user to run the CLI with `GEMINI_EXP` set, e.g.: + `GEMINI_EXP=.gemini/experiments.json gemini ` + +## Using the Scripts + +You have access to `scripts/override_experiment.cjs` to manage the local overrides safely without disrupting the file structure required by the CLI backend. + +Example usage: +```bash +# Enable an experiment locally +node .gemini/skills/experimentation/scripts/override_experiment.cjs set 45740196 true + +# Remove an override +node .gemini/skills/experimentation/scripts/override_experiment.cjs unset 45740196 + +# Opt out of all experiments (clear everything) +node .gemini/skills/experimentation/scripts/override_experiment.cjs clear +``` \ No newline at end of file diff --git a/.gemini/skills/experimentation/scripts/override_experiment.cjs b/.gemini/skills/experimentation/scripts/override_experiment.cjs new file mode 100644 index 0000000000..71a2ddf8fc --- /dev/null +++ b/.gemini/skills/experimentation/scripts/override_experiment.cjs @@ -0,0 +1,72 @@ +const fs = require('fs'); +const path = require('path'); + +const filePath = path.join(process.cwd(), '.gemini', 'experiments.json'); + +function readExperiments() { + if (fs.existsSync(filePath)) { + try { + const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); + return { + flags: data.flags || [], + experimentIds: data.experimentIds || [] + }; + } catch (e) { + console.error('Failed to parse existing experiments.json, starting fresh.'); + } + } + return { flags: [], experimentIds: [] }; +} + +function writeExperiments(data) { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8'); + console.log(`Updated local experiments at ${filePath}`); +} + +const args = process.argv.slice(2); +const command = args[0]; + +if (!command) { + console.error('Usage: override_experiment.cjs [flagId] [value]'); + process.exit(1); +} + +const data = readExperiments(); + +if (command === 'clear') { + data.flags = []; + data.experimentIds = []; + writeExperiments(data); +} else if (command === 'unset') { + const flagId = parseInt(args[1], 10); + data.flags = data.flags.filter(f => f.flagId !== flagId); + writeExperiments(data); +} else if (command === 'set') { + const flagId = parseInt(args[1], 10); + const rawValue = args[2]; + + if (isNaN(flagId) || rawValue === undefined) { + console.error('Invalid arguments for set: requires numeric flagId and value'); + process.exit(1); + } + + // Remove existing flag + data.flags = data.flags.filter(f => f.flagId !== flagId); + + // Parse value + const flag = { flagId }; + if (rawValue === 'true') flag.boolValue = true; + else if (rawValue === 'false') flag.boolValue = false; + else if (!isNaN(Number(rawValue))) flag.numberValue = Number(rawValue); + else flag.stringValue = rawValue; + + data.flags.push(flag); + writeExperiments(data); +} else { + console.error('Unknown command'); + process.exit(1); +} diff --git a/experimentation.skill b/experimentation.skill new file mode 100644 index 0000000000000000000000000000000000000000..5e1e7f4e9638cc8012e429c6e43fdce55c9be9c9 GIT binary patch literal 2168 zcmZ{mcR1Wx7sr1XqMMK?t9ODS(MzKDHqpYU!Hhah3lT~59%%+4-e5=sLDuf7A^Ip$ zBEc$SRtanL5H0LT_IZ+L^XA^?+~@prKj+*(zUOb0(okj1wgj$Q~;DhI77KZ4`fjYhgnxI9#aDV=@bA6 zen&uu!GnVQyy5Qfa11=i4+%$wK)n2ezkvBE(3AKNxY({2HK71*Jdjb*p)$Q)pk-y~ z^5H$O`kLhzJXf19cS=o;3Etc$ET;WJ>hd1*KMG>XXtF9j^zWI>~=9PdoTy{K?H z&2cJ{r1PMejcX;E-F)=!%Rs;_Q`QG2^H^dOq|51Xc2}S!SZI!sHG(g;+TZ{@+E>1X zUqWazHjmkoX@>+0-DLS+MpSbFu0=dseJ9U4f9^bkq?6@|)77SVSc zsP-1_4L_p*WIN8a@VRnqll&a4C6H|~DaH-qrMx4*o0_Oj=aeLwF`yYghL1 z2o;;tjV_gvKFFi4UMDHPClY<~vj!E1WIn+P2^+z=xi(OoTTNxc^XDQjTa>bz)SL@@ zFLGHvnXQuCc`PhLh9Nhc&ybB1C7)){UE)Cy{g;mX2uZJE;@<{2$X=}$hxg+O+US|p z|5%C;jM$QxZhA@Q!gO6DzTN#EDDNWCdPw|1t#U{>GruZJE8tKVPn?=_Hrs!}h>Y+m zSn?Sz$?piRA@4b*h$zC~IwIVQFBT}7;W-SBN4`PnBbi${!__|q=}x*Eh- z6RxRXGnkL9tVz6OgK<+;Sn;FOkI*_%{I^h8-~%6@d57(uMC ztV@Ex)Mlczv;uI|1O8u{P=gHe)$MJru#eXvc(N9a+)dd+65td zjuJ@67W3+5m;>pSrVY01*02VLJ0k6^lK`J#Ha_ltPl&Lh;p(G17NR4kE|Gg9GtLkR z?N@lp{bhHaJcqu<+a>kutk@hs6RZM9{GN)iRQF!%UOcR>uk?!T_nq;nTAI8uJ5}6i zT*@(qeVAsh)cH3Lq9Ks;oSO147WP;}pYtG0(yl$G6%#o-WdjnO2A9B1FARD59%Jb8 z&#pOc+5yg$z2NeqAmwItf#~4Ps4fSC0j5;aQge+*gGjILL5=w&nV}7oKrkqA-uC9~ z@!Vs8a;M4e*7>?#nDmwLn5fHF9qPGM@21okzEs$!#xaL<3?%R?m4cHi!~!37E*@-e z*Rx1_EoF_4uG`LYd*D^Q1N-`zcQ2_eTL*BJDu9+begls`8#9#RQ7!VAz)^ebR|+0k zo;PW3`xNiD*NW3#z~)Y;6*OC`Fm6b?&%y-sKumb4rLKz$g&}`OZGuVA2{k|5`@7-`F^8K;Itn-0h5C>lU`+cRLXKwK3m{DOTk}F~TT| z*ie((-E{p;=Co@RJ9Bjac$Xq$Dry0mpII#Tzh(;tR&-C&Z-z_z)o}mgiC<|yv)ivU zDhdZ5t@j=6r{+s3@_&r?`yxNip6`o1qD-W3lZZk)nM6OdOiOq2G#bjLqV#r^@-qN{ Fe*h(xrB?s| literal 0 HcmV?d00001 diff --git a/packages/core/src/code_assist/experiments/flagNames.ts b/packages/core/src/code_assist/experiments/flagNames.ts index 125ff005a9..67f063bd68 100644 --- a/packages/core/src/code_assist/experiments/flagNames.ts +++ b/packages/core/src/code_assist/experiments/flagNames.ts @@ -24,3 +24,67 @@ export const ExperimentFlags = { export type ExperimentFlagName = (typeof ExperimentFlags)[keyof typeof ExperimentFlags]; + +export interface ExperimentMetadataEntry { + description: string; + type: 'boolean' | 'number' | 'string'; + defaultValue: boolean | number | string; +} + +export const ExperimentMetadata: Record = { + [ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD]: { + description: 'Threshold at which context compression activates.', + type: 'number', + defaultValue: 0, + }, + [ExperimentFlags.USER_CACHING]: { + description: 'Enables caching of user contexts.', + type: 'boolean', + defaultValue: false, + }, + [ExperimentFlags.BANNER_TEXT_NO_CAPACITY_ISSUES]: { + description: 'Banner text displayed when there are no capacity issues.', + type: 'string', + defaultValue: '', + }, + [ExperimentFlags.BANNER_TEXT_CAPACITY_ISSUES]: { + description: 'Banner text displayed during capacity issues.', + type: 'string', + defaultValue: '', + }, + [ExperimentFlags.ENABLE_PREVIEW]: { + description: 'Enables preview features globally.', + type: 'boolean', + defaultValue: false, + }, + [ExperimentFlags.ENABLE_NUMERICAL_ROUTING]: { + description: 'Enables numerical routing strategies for the model.', + type: 'boolean', + defaultValue: false, + }, + [ExperimentFlags.CLASSIFIER_THRESHOLD]: { + description: 'Threshold for the intent classifier.', + type: 'number', + defaultValue: 0.5, + }, + [ExperimentFlags.ENABLE_ADMIN_CONTROLS]: { + description: 'Enables admin control features in the CLI.', + type: 'boolean', + defaultValue: false, + }, + [ExperimentFlags.MASKING_PROTECTION_THRESHOLD]: { + description: 'Threshold for masking protection logic.', + type: 'number', + defaultValue: 0, + }, + [ExperimentFlags.MASKING_PRUNABLE_THRESHOLD]: { + description: 'Threshold for prunable masking.', + type: 'number', + defaultValue: 0, + }, + [ExperimentFlags.MASKING_PROTECT_LATEST_TURN]: { + description: 'Protects the latest turn from being masked.', + type: 'boolean', + defaultValue: true, + }, +};