diff --git a/docs/admin/enterprise-controls.md b/docs/admin/enterprise-controls.md index 8c9ba60a13..2371ba20b1 100644 --- a/docs/admin/enterprise-controls.md +++ b/docs/admin/enterprise-controls.md @@ -21,7 +21,8 @@ preferred method for enforcing policy. **Enabled/Disabled** | Default: enabled -If enabled, users will not be able to enter yolo mode. +If enabled, users will not be able to use the `--yolo` flag or wildcard tool +policies. ### Extensions diff --git a/docs/cli/cli-reference.md b/docs/cli/cli-reference.md index 167801ca05..1b81ef5fb9 100644 --- a/docs/cli/cli-reference.md +++ b/docs/cli/cli-reference.md @@ -51,8 +51,8 @@ These commands are available within the interactive REPL. | `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. Forces non-interactive mode. | | `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode | | `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution | -| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` | -| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. | +| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `plan` | +| `--yolo` | `-y` | boolean | `false` | Auto-approve all actions. Equivalent to `--allowed-tools=*`. | | `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** | | `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** | | `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) | diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 9550e2a918..3374060925 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -46,8 +46,9 @@ To start Plan Mode while using Gemini CLI: calls the [`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode) tool to switch modes. - > **Note:** This tool is not available when Gemini CLI is in - > [YOLO mode](../reference/configuration.md#command-line-arguments). + > **Note:** This tool is not available when Gemini CLI has been instructed to + > [auto-approve all actions](../reference/configuration.md#command-line-arguments) + > (e.g. via `--yolo`). ## How to use Plan Mode diff --git a/docs/extensions/reference.md b/docs/extensions/reference.md index e6012f4d33..24fb63f316 100644 --- a/docs/extensions/reference.md +++ b/docs/extensions/reference.md @@ -253,10 +253,10 @@ Rules contributed by extensions run in their own tier (tier 2), alongside workspace-defined policies. This tier has higher priority than the default rules but lower priority than user or admin policies. -> **Warning:** For security, Gemini CLI ignores any `allow` decisions or `yolo` -> mode configurations in extension policies. This ensures that an extension -> cannot automatically approve tool calls or bypass security measures without -> your confirmation. +> **Warning:** For security, Gemini CLI ignores any `allow` decisions or +> `allow-all` wildcard configurations in extension policies. This ensures that +> an extension cannot automatically approve tool calls or bypass security +> measures without your confirmation. **Example `policies.toml`** diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 7df1de61f1..f02117c102 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1938,19 +1938,14 @@ for that specific session. - **`--help`** (or **`-h`**): - Displays help information about command-line arguments. - **`--yolo`**: - - Enables YOLO mode, which automatically approves all tool calls. + - Automatically approves all actions. Equivalent to `--allowed-tools=*`. - **`--approval-mode `**: - Sets the approval mode for tool calls. Available modes: - `default`: Prompt for approval on each tool call (default behavior) - `auto_edit`: Automatically approve edit tools (replace, write_file) while prompting for others - - `yolo`: Automatically approve all tool calls (equivalent to `--yolo`) - `plan`: Read-only mode for tool calls (requires experimental planning to be enabled). - > **Note:** This mode is currently under development and not yet fully - > functional. - - Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of - `--yolo` for the new unified approach. - Example: `gemini --approval-mode auto_edit` - **`--allowed-tools `**: - A comma-separated list of tool names that will bypass the confirmation @@ -2114,7 +2109,7 @@ Sandboxing is disabled by default, but you can enable it in a few ways: - Using `--sandbox` or `-s` flag. - Setting `GEMINI_SANDBOX` environment variable. -- Sandbox is enabled when using `--yolo` or `--approval-mode=yolo` by default. +- Sandbox is enabled when using `--yolo` by default. By default, it uses a pre-built `gemini-cli-sandbox` Docker image. diff --git a/docs/reference/keyboard-shortcuts.md b/docs/reference/keyboard-shortcuts.md index 2ca7a6bb39..c2116a8462 100644 --- a/docs/reference/keyboard-shortcuts.md +++ b/docs/reference/keyboard-shortcuts.md @@ -102,7 +102,6 @@ available combinations. | `app.showIdeContextDetail` | Show IDE context details. | `Ctrl+G` | | `app.toggleMarkdown` | Toggle Markdown rendering. | `Alt+M` | | `app.toggleCopyMode` | Toggle copy mode when in alternate buffer mode. | `Ctrl+S` | -| `app.toggleYolo` | Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl+Y` | | `app.cycleApprovalMode` | Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift+Tab` | | `app.showMoreLines` | Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl+O` | | `app.expandPaste` | Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl+O` | @@ -148,7 +147,6 @@ a `key` combination. }, { // prefix "-" to unbind a key - "command": "-app.toggleYolo", "key": "ctrl+y" }, { diff --git a/docs/reference/policy-engine.md b/docs/reference/policy-engine.md index fb97b5e071..085714ad84 100644 --- a/docs/reference/policy-engine.md +++ b/docs/reference/policy-engine.md @@ -157,9 +157,9 @@ For example: Approval modes allow the policy engine to apply different sets of rules based on the CLI's operational mode. A rule can be associated with one or more modes -(e.g., `yolo`, `autoEdit`, `plan`). The rule will only be active if the CLI is -running in one of its specified modes. If a rule has no modes specified, it is -always active. +(e.g., `autoEdit`, `plan`). The rule will only be active if the CLI is running +in one of its specified modes. If a rule has no modes specified, it is always +active. - `default`: The standard interactive mode where most write tools require confirmation. @@ -167,7 +167,6 @@ always active. auto-approved. - `plan`: A strict, read-only mode for research and design. See [Customizing Plan Mode Policies](../cli/plan-mode.md#customizing-policies). -- `yolo`: A mode where all tools are auto-approved (use with extreme caution). ## Rule matching @@ -424,6 +423,5 @@ out-of-the-box experience. checked individually. - **Write tools** (like `write_file`, `run_shell_command`) default to **`ask_user`**. -- In **`yolo`** mode, a high-priority rule allows all tools. - In **`autoEdit`** mode, rules allow certain write operations to happen without prompting. diff --git a/docs/tools/planning.md b/docs/tools/planning.md index 9e9ab3d044..77e500d9ee 100644 --- a/docs/tools/planning.md +++ b/docs/tools/planning.md @@ -11,7 +11,8 @@ by the agent when you ask it to "start a plan" using natural language. In this mode, the agent is restricted to read-only tools to allow for safe exploration and planning. -> **Note:** This tool is not available when the CLI is in YOLO mode. +> **Note:** This tool is disabled when all tools are auto-approved via `--yolo` +> or wildcard policies. - **Tool name:** `enter_plan_mode` - **Display name:** Enter Plan Mode diff --git a/packages/a2a-server/src/agent/task-event-driven.test.ts b/packages/a2a-server/src/agent/task-event-driven.test.ts index 86436fa811..8dde98e977 100644 --- a/packages/a2a-server/src/agent/task-event-driven.test.ts +++ b/packages/a2a-server/src/agent/task-event-driven.test.ts @@ -9,7 +9,6 @@ import { type Config, MessageBusType, ToolConfirmationOutcome, - ApprovalMode, Scheduler, type MessageBus, } from '@google/gemini-cli-core'; @@ -358,7 +357,7 @@ describe('Task Event-Driven Scheduler', () => { // Enable YOLO mode const yoloConfig = createMockConfig({ isEventDrivenSchedulerEnabled: () => true, - getApprovalMode: () => ApprovalMode.YOLO, + getAllowedTools: () => ['*'], }) as Config; const yoloMessageBus = yoloConfig.messageBus; diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index a76054263f..0adc665754 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -10,7 +10,6 @@ import { type GeminiClient, GeminiEventType, ToolConfirmationOutcome, - ApprovalMode, getAllMCPServerStatuses, MCPServerStatus, isNodeError, @@ -89,7 +88,8 @@ export class Task { autoExecute: boolean; private get isYoloMatch(): boolean { return ( - this.autoExecute || this.config.getApprovalMode() === ApprovalMode.YOLO + this.autoExecute || + (this.config.getAllowedTools()?.includes('*') ?? false) ); } @@ -880,9 +880,7 @@ export class Task { if ( part.kind !== 'data' || !part.data || - // eslint-disable-next-line no-restricted-syntax typeof part.data['callId'] !== 'string' || - // eslint-disable-next-line no-restricted-syntax typeof part.data['outcome'] !== 'string' ) { return false; diff --git a/packages/a2a-server/src/config/config.test.ts b/packages/a2a-server/src/config/config.test.ts index cfe77311ea..daf85e5740 100644 --- a/packages/a2a-server/src/config/config.test.ts +++ b/packages/a2a-server/src/config/config.test.ts @@ -19,8 +19,6 @@ import { AuthType, isHeadlessMode, FatalAuthenticationError, - PolicyDecision, - PRIORITY_YOLO_ALLOW_ALL, } from '@google/gemini-cli-core'; // Mock dependencies @@ -375,35 +373,24 @@ describe('loadConfig', () => { }); describe('YOLO mode', () => { - it('should enable YOLO mode and add policy rule when GEMINI_YOLO_MODE is true', async () => { + it('should enable wildcard allowedTools when GEMINI_YOLO_MODE is true', async () => { vi.stubEnv('GEMINI_YOLO_MODE', 'true'); await loadConfig(mockSettings, mockExtensionLoader, taskId); expect(Config).toHaveBeenCalledWith( expect.objectContaining({ - approvalMode: 'yolo', - policyEngineConfig: expect.objectContaining({ - rules: expect.arrayContaining([ - expect.objectContaining({ - decision: PolicyDecision.ALLOW, - priority: PRIORITY_YOLO_ALLOW_ALL, - modes: ['yolo'], - allowRedirection: true, - }), - ]), - }), + approvalMode: 'default', + allowedTools: expect.arrayContaining(['*']), }), ); }); - it('should use default approval mode and empty rules when GEMINI_YOLO_MODE is not true', async () => { + it('should use default approval mode and undefined allowedTools when GEMINI_YOLO_MODE is not true', async () => { vi.stubEnv('GEMINI_YOLO_MODE', 'false'); await loadConfig(mockSettings, mockExtensionLoader, taskId); expect(Config).toHaveBeenCalledWith( expect.objectContaining({ approvalMode: 'default', - policyEngineConfig: expect.objectContaining({ - rules: [], - }), + allowedTools: undefined, }), ); }); diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts index 9474c4d9c5..9abbd52aaf 100644 --- a/packages/a2a-server/src/config/config.ts +++ b/packages/a2a-server/src/config/config.ts @@ -26,8 +26,6 @@ import { isHeadlessMode, FatalAuthenticationError, isCloudShell, - PolicyDecision, - PRIORITY_YOLO_ALLOW_ALL, type TelemetryTarget, type ConfigParameters, type ExtensionLoader, @@ -62,11 +60,6 @@ export async function loadConfig( } } - const approvalMode = - process.env['GEMINI_YOLO_MODE'] === 'true' - ? ApprovalMode.YOLO - : ApprovalMode.DEFAULT; - const configParams: ConfigParameters = { sessionId: taskId, clientName: 'a2a-server', @@ -79,22 +72,12 @@ export async function loadConfig( coreTools: settings.coreTools || settings.tools?.core || undefined, excludeTools: settings.excludeTools || settings.tools?.exclude || undefined, - allowedTools: settings.allowedTools || settings.tools?.allowed || undefined, + allowedTools: + process.env['GEMINI_YOLO_MODE'] === 'true' + ? [...(settings.allowedTools || settings.tools?.allowed || []), '*'] + : settings.allowedTools || settings.tools?.allowed || undefined, showMemoryUsage: settings.showMemoryUsage || false, - approvalMode, - policyEngineConfig: { - rules: - approvalMode === ApprovalMode.YOLO - ? [ - { - decision: PolicyDecision.ALLOW, - priority: PRIORITY_YOLO_ALLOW_ALL, - modes: [ApprovalMode.YOLO], - allowRedirection: true, - }, - ] - : [], - }, + approvalMode: ApprovalMode.DEFAULT, mcpServers: settings.mcpServers, cwd: workspaceDir, telemetry: { diff --git a/packages/a2a-server/src/http/app.test.ts b/packages/a2a-server/src/http/app.test.ts index 4a883992b5..4de4a905b0 100644 --- a/packages/a2a-server/src/http/app.test.ts +++ b/packages/a2a-server/src/http/app.test.ts @@ -72,6 +72,7 @@ const getToolRegistrySpy = vi.fn().mockReturnValue({ getToolsByServer: vi.fn().mockReturnValue([]), }); const getApprovalModeSpy = vi.fn(); +const getAllowedToolsSpy = vi.fn(); const getShellExecutionConfigSpy = vi.fn(); const getExtensionsSpy = vi.fn(); @@ -83,6 +84,7 @@ vi.mock('../config/config.js', async () => { const mockConfig = createMockConfig({ getToolRegistry: getToolRegistrySpy, getApprovalMode: getApprovalModeSpy, + getAllowedTools: getAllowedToolsSpy, getShellExecutionConfig: getShellExecutionConfigSpy, getExtensions: getExtensionsSpy, }); @@ -118,6 +120,7 @@ describe('E2E Tests', () => { beforeEach(() => { getApprovalModeSpy.mockReturnValue(ApprovalMode.DEFAULT); + getAllowedToolsSpy.mockReturnValue([]); }); afterAll( @@ -406,7 +409,7 @@ describe('E2E Tests', () => { it('should handle multiple tool calls sequentially in YOLO mode', async () => { // Set YOLO mode to auto-approve tools and test sequential execution. - getApprovalModeSpy.mockReturnValue(ApprovalMode.YOLO); + getAllowedToolsSpy.mockReturnValue(['*']); // First call yields the tool request sendMessageStreamSpy.mockImplementationOnce(async function* () { @@ -697,7 +700,7 @@ describe('E2E Tests', () => { }); // Set approval mode to yolo - getApprovalModeSpy.mockReturnValue(ApprovalMode.YOLO); + getAllowedToolsSpy.mockReturnValue(['*']); const mockTool = new MockTool({ name: 'test-tool-yolo', diff --git a/packages/a2a-server/src/utils/testing_utils.ts b/packages/a2a-server/src/utils/testing_utils.ts index fd4d721732..114f70e643 100644 --- a/packages/a2a-server/src/utils/testing_utils.ts +++ b/packages/a2a-server/src/utils/testing_utils.ts @@ -126,8 +126,8 @@ export function createMockConfig( mockConfig.getPolicyEngine = vi.fn().mockReturnValue({ check: async () => { - const mode = mockConfig.getApprovalMode(); - if (mode === ApprovalMode.YOLO) { + const allowed = mockConfig.getAllowedTools?.() || []; + if (allowed.includes('*')) { return { decision: PolicyDecision.ALLOW }; } return { decision: PolicyDecision.ASK_USER }; diff --git a/packages/cli/src/acp/acpClient.test.ts b/packages/cli/src/acp/acpClient.test.ts index abad9d374d..976dbde9b5 100644 --- a/packages/cli/src/acp/acpClient.test.ts +++ b/packages/cli/src/acp/acpClient.test.ts @@ -355,7 +355,6 @@ describe('GeminiAgent', () => { name: 'Auto Edit', description: 'Auto-approves edit tools', }, - { id: 'yolo', name: 'YOLO', description: 'Auto-approves all tools' }, ], currentModeId: 'default', }); @@ -413,7 +412,7 @@ describe('GeminiAgent', () => { name: 'Auto Edit', description: 'Auto-approves edit tools', }, - { id: 'yolo', name: 'YOLO', description: 'Auto-approves all tools' }, + { id: 'plan', name: 'Plan', description: 'Read-only mode' }, ], currentModeId: 'plan', diff --git a/packages/cli/src/acp/acpClient.ts b/packages/cli/src/acp/acpClient.ts index 44c0373515..0d2516dc55 100644 --- a/packages/cli/src/acp/acpClient.ts +++ b/packages/cli/src/acp/acpClient.ts @@ -1577,11 +1577,6 @@ function buildAvailableModes(isPlanEnabled: boolean): acp.SessionMode[] { name: 'Auto Edit', description: 'Auto-approves edit tools', }, - { - id: ApprovalMode.YOLO, - name: 'YOLO', - description: 'Auto-approves all tools', - }, ]; if (isPlanEnabled) { diff --git a/packages/cli/src/acp/acpResume.test.ts b/packages/cli/src/acp/acpResume.test.ts index 9668ef74f8..18b417667d 100644 --- a/packages/cli/src/acp/acpResume.test.ts +++ b/packages/cli/src/acp/acpResume.test.ts @@ -199,11 +199,6 @@ describe('GeminiAgent Session Resume', () => { name: 'Auto Edit', description: 'Auto-approves edit tools', }, - { - id: ApprovalMode.YOLO, - name: 'YOLO', - description: 'Auto-approves all tools', - }, { id: ApprovalMode.PLAN, name: 'Plan', diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index a94d1f0a28..5493ad8bff 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1387,7 +1387,7 @@ describe('Approval mode tool exclusion logic', () => { await expect( loadCliConfig(settings, 'test-session', invalidArgv as CliArgs), ).rejects.toThrow( - 'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, plan, default', + 'Invalid approval mode: invalid_mode. Valid values are: auto_edit, plan, default (yolo is mapped to allowed-tools)', ); }); @@ -2559,7 +2559,7 @@ describe('loadCliConfig approval mode', () => { 'test-session', argv, ); - expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should set YOLO approval mode when -y flag is used', async () => { @@ -2570,7 +2570,7 @@ describe('loadCliConfig approval mode', () => { 'test-session', argv, ); - expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should set DEFAULT approval mode when --approval-mode=default', async () => { @@ -2603,7 +2603,7 @@ describe('loadCliConfig approval mode', () => { 'test-session', argv, ); - expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should prioritize --approval-mode over --yolo when both would be valid (but validation prevents this)', async () => { @@ -2629,7 +2629,7 @@ describe('loadCliConfig approval mode', () => { 'test-session', argv, ); - expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should set Plan approval mode when --approval-mode=plan is used and experimental.plan is enabled', async () => { @@ -2780,7 +2780,7 @@ describe('loadCliConfig approval mode', () => { }); const argv = await parseArguments(settings); const config = await loadCliConfig(settings, 'test-session', argv); - expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should respect plan mode from settings when experimental.plan is enabled', async () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 80c1e19443..8e323cf2d1 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -561,10 +561,13 @@ export async function loadCliConfig( ? settings.general?.defaultApprovalMode : undefined); + let isYoloRequested = false; + if (rawApprovalMode) { switch (rawApprovalMode) { case 'yolo': - approvalMode = ApprovalMode.YOLO; + approvalMode = ApprovalMode.DEFAULT; + isYoloRequested = true; break; case 'auto_edit': approvalMode = ApprovalMode.AUTO_EDIT; @@ -584,33 +587,37 @@ export async function loadCliConfig( break; default: throw new Error( - `Invalid approval mode: ${rawApprovalMode}. Valid values are: yolo, auto_edit, plan, default`, + `Invalid approval mode: ${rawApprovalMode}. Valid values are: auto_edit, plan, default (yolo is mapped to allowed-tools)`, ); } } else { approvalMode = ApprovalMode.DEFAULT; } - // Override approval mode if disableYoloMode is set. + let allowedTools = argv.allowedTools || settings.tools?.allowed || []; + if (settings.security?.disableYoloMode || settings.admin?.secureModeEnabled) { - if (approvalMode === ApprovalMode.YOLO) { + if (isYoloRequested || allowedTools.includes('*')) { if (settings.admin?.secureModeEnabled) { debugLogger.error( - 'YOLO mode is disabled by "secureModeEnabled" setting.', + 'YOLO mode (wildcard policies) are disabled by "secureModeEnabled" setting.', ); } else { debugLogger.error( - 'YOLO mode is disabled by the "disableYolo" setting.', + 'YOLO mode (wildcard policies) are disabled by the "disableYolo" setting.', ); } throw new FatalConfigError( getAdminErrorMessage('YOLO mode', undefined /* config */), ); } - } else if (approvalMode === ApprovalMode.YOLO) { + } else if (isYoloRequested) { debugLogger.warn( - 'YOLO mode is enabled. All tool calls will be automatically approved.', + 'YOLO mode is enabled via flag or setting. All tool calls will be automatically approved by a wildcard policy.', ); + if (!allowedTools.includes('*')) { + allowedTools = [...allowedTools, '*']; + } } // Force approval mode to default if the folder is not trusted. @@ -646,8 +653,6 @@ export async function loadCliConfig( (!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) && !argv.isCommand); - const allowedTools = argv.allowedTools || settings.tools?.allowed || []; - // In non-interactive mode, exclude tools that require a prompt. const extraExcludes: string[] = []; if (!interactive) { diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index fa957d8f7f..ff9e56d3cc 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -436,7 +436,7 @@ priority = 100 ); }); - it('should ignore ALLOW rules and YOLO mode from extension policies for security', async () => { + it('should ignore ALLOW rules from extension policies for security', async () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const extDir = createExtension({ extensionsDir: userExtensionsDir, @@ -452,20 +452,6 @@ priority = 100 toolName = "allow_tool" decision = "allow" priority = 100 - -[[rule]] -toolName = "yolo_tool" -decision = "ask_user" -priority = 100 -modes = ["yolo"] - -[[safety_checker]] -toolName = "yolo_check" -priority = 100 -modes = ["yolo"] -[safety_checker.checker] -type = "external" -name = "yolo-checker" `; fs.writeFileSync( path.join(policiesDir, 'policies.toml'), @@ -476,24 +462,15 @@ name = "yolo-checker" expect(extensions).toHaveLength(1); const extension = extensions[0]; - // ALLOW rules and YOLO rules/checkers should be filtered out + // ALLOW rules should be filtered out expect(extension.rules).toBeDefined(); expect(extension.rules).toHaveLength(0); expect(extension.checkers).toBeDefined(); - expect(extension.checkers).toHaveLength(0); // Should have logged warnings expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('attempted to contribute an ALLOW rule'), ); - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('attempted to contribute a rule for YOLO mode'), - ); - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining( - 'attempted to contribute a safety checker for YOLO mode', - ), - ); consoleSpy.mockRestore(); }); diff --git a/packages/cli/src/config/policy-engine.integration.test.ts b/packages/cli/src/config/policy-engine.integration.test.ts index 847b47bbe3..74b0c22fbd 100644 --- a/packages/cli/src/config/policy-engine.integration.test.ts +++ b/packages/cli/src/config/policy-engine.integration.test.ts @@ -274,20 +274,21 @@ describe('Policy Engine Integration Tests', () => { ).toBe(PolicyDecision.ASK_USER); }); - it('should handle YOLO mode correctly', async () => { + it('should handle wildcard policy (YOLO mode) correctly', async () => { const settings: Settings = { tools: { - exclude: ['dangerous-tool'], // Even in YOLO, excludes should be respected + allowed: ['*'], + exclude: ['dangerous-tool'], // Even in wildcard, excludes should be respected }, }; const config = await createPolicyEngineConfig( settings, - ApprovalMode.YOLO, + ApprovalMode.DEFAULT, ); const engine = new PolicyEngine(config); - // Most tools should be allowed in YOLO mode + // Most tools should be allowed in wildcard mode expect( (await engine.check({ name: 'run_shell_command' }, undefined)).decision, ).toBe(PolicyDecision.ALLOW); diff --git a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts index 84010ab625..9150ecea9f 100644 --- a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts +++ b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts @@ -222,7 +222,9 @@ describe('ShellProcessor', () => { decision: PolicyDecision.ALLOW, }); // Override the approval mode for this test (though PolicyEngine mock handles the decision) - (mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO); + (mockConfig.getApprovalMode as Mock).mockReturnValue( + ApprovalMode.AUTO_EDIT, + ); mockShellExecute.mockReturnValue({ result: Promise.resolve({ ...SUCCESS_RESULT, output: 'deleted' }), }); @@ -250,7 +252,9 @@ describe('ShellProcessor', () => { decision: PolicyDecision.DENY, }); // Set approval mode to YOLO - (mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO); + (mockConfig.getApprovalMode as Mock).mockReturnValue( + ApprovalMode.AUTO_EDIT, + ); await expect(processor.process(prompt, context)).rejects.toThrow( /Blocked command: "reboot". Reason: Blocked by policy/, diff --git a/packages/cli/src/ui/commands/policiesCommand.test.ts b/packages/cli/src/ui/commands/policiesCommand.test.ts index 554d5cd53d..2a61a9bccd 100644 --- a/packages/cli/src/ui/commands/policiesCommand.test.ts +++ b/packages/cli/src/ui/commands/policiesCommand.test.ts @@ -107,9 +107,6 @@ describe('policiesCommand', () => { expect(content).toContain( '### Auto Edit Mode Policies (combined with normal mode policies)', ); - expect(content).toContain( - '### Yolo Mode Policies (combined with normal mode policies)', - ); expect(content).toContain('### Plan Mode Policies'); expect(content).toContain( '**DENY** tool: `dangerousTool` [Priority: 10]', diff --git a/packages/cli/src/ui/commands/policiesCommand.ts b/packages/cli/src/ui/commands/policiesCommand.ts index f4bd13de28..87da76fd2a 100644 --- a/packages/cli/src/ui/commands/policiesCommand.ts +++ b/packages/cli/src/ui/commands/policiesCommand.ts @@ -11,7 +11,7 @@ import { MessageType } from '../types.js'; interface CategorizedRules { normal: PolicyRule[]; autoEdit: PolicyRule[]; - yolo: PolicyRule[]; + plan: PolicyRule[]; } @@ -21,7 +21,7 @@ const categorizeRulesByMode = ( const result: CategorizedRules = { normal: [], autoEdit: [], - yolo: [], + plan: [], }; const ALL_MODES = Object.values(ApprovalMode); @@ -30,7 +30,7 @@ const categorizeRulesByMode = ( const modeSet = new Set(modes); if (modeSet.has(ApprovalMode.DEFAULT)) result.normal.push(rule); if (modeSet.has(ApprovalMode.AUTO_EDIT)) result.autoEdit.push(rule); - if (modeSet.has(ApprovalMode.YOLO)) result.yolo.push(rule); + if (modeSet.has(ApprovalMode.PLAN)) result.plan.push(rule); }); return result; @@ -82,9 +82,6 @@ const listPoliciesCommand: SlashCommand = { const uniqueAutoEdit = categorized.autoEdit.filter( (rule) => !normalRulesSet.has(rule), ); - const uniqueYolo = categorized.yolo.filter( - (rule) => !normalRulesSet.has(rule), - ); const uniquePlan = categorized.plan.filter( (rule) => !normalRulesSet.has(rule), ); @@ -95,10 +92,6 @@ const listPoliciesCommand: SlashCommand = { 'Auto Edit Mode Policies (combined with normal mode policies)', uniqueAutoEdit, ); - content += formatSection( - 'Yolo Mode Policies (combined with normal mode policies)', - uniqueYolo, - ); content += formatSection('Plan Mode Policies', uniquePlan); context.ui.addItem( diff --git a/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx b/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx index 4386891c7a..f91b545869 100644 --- a/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx +++ b/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx @@ -39,7 +39,10 @@ describe('ApprovalModeIndicator', () => { it('renders correctly for YOLO mode', async () => { const { lastFrame, waitUntilReady } = render( - , + , ); await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); diff --git a/packages/cli/src/ui/components/ApprovalModeIndicator.tsx b/packages/cli/src/ui/components/ApprovalModeIndicator.tsx index 7e8f388c82..6c39f3e64c 100644 --- a/packages/cli/src/ui/components/ApprovalModeIndicator.tsx +++ b/packages/cli/src/ui/components/ApprovalModeIndicator.tsx @@ -14,43 +14,45 @@ import { Command } from '../key/keyBindings.js'; interface ApprovalModeIndicatorProps { approvalMode: ApprovalMode; allowPlanMode?: boolean; + isYoloMode?: boolean; } export const ApprovalModeIndicator: React.FC = ({ approvalMode, allowPlanMode, + isYoloMode, }) => { let textColor = ''; let textContent = ''; let subText = ''; const cycleHint = formatCommand(Command.CYCLE_APPROVAL_MODE); - const yoloHint = formatCommand(Command.TOGGLE_YOLO); - switch (approvalMode) { - case ApprovalMode.AUTO_EDIT: - textColor = theme.status.warning; - textContent = 'auto-accept edits'; - subText = allowPlanMode - ? `${cycleHint} to plan` - : `${cycleHint} to manual`; - break; - case ApprovalMode.PLAN: - textColor = theme.status.success; - textContent = 'plan'; - subText = `${cycleHint} to manual`; - break; - case ApprovalMode.YOLO: - textColor = theme.status.error; - textContent = 'YOLO'; - subText = yoloHint; - break; - case ApprovalMode.DEFAULT: - default: - textColor = theme.text.accent; - textContent = ''; - subText = `${cycleHint} to accept edits`; - break; + if (isYoloMode) { + textColor = theme.status.error; + textContent = 'YOLO'; + subText = ''; + } else { + switch (approvalMode) { + case ApprovalMode.AUTO_EDIT: + textColor = theme.status.warning; + textContent = 'auto-accept edits'; + subText = allowPlanMode + ? `${cycleHint} to plan` + : `${cycleHint} to manual`; + break; + case ApprovalMode.PLAN: + textColor = theme.status.success; + textContent = 'plan'; + subText = `${cycleHint} to manual`; + break; + case ApprovalMode.DEFAULT: + default: + textColor = theme.text.accent; + textContent = ''; + subText = `${cycleHint} to accept edits`; + break; + } } return ( diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index e0919947fb..c283a112c0 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -232,6 +232,7 @@ const createMockConfig = (overrides = {}): Config => getAccessibility: vi.fn(() => ({})), getMcpServers: vi.fn(() => ({})), isPlanEnabled: vi.fn(() => true), + getAllowedTools: vi.fn(() => []), getToolRegistry: () => ({ getTool: vi.fn(), }), @@ -621,7 +622,6 @@ describe('Composer', () => { [ApprovalMode.DEFAULT], [ApprovalMode.AUTO_EDIT], [ApprovalMode.PLAN], - [ApprovalMode.YOLO], ])( 'shows ApprovalModeIndicator when approval mode is %s and shell mode is inactive', async (mode) => { @@ -636,6 +636,20 @@ describe('Composer', () => { }, ); + it('shows ApprovalModeIndicator when YOLO mode is active and shell mode is inactive', async () => { + const config = createMockConfig({ + getAllowedTools: vi.fn(() => ['*']), + }); + const uiState = createMockUIState({ + showApprovalModeIndicator: ApprovalMode.DEFAULT, + shellModeActive: false, + }); + + const { lastFrame } = await renderComposer(uiState, undefined, config); + + expect(lastFrame()).toMatch(/ApprovalModeIndic[\s\S]*ator/); + }); + it('shows ShellModeIndicator when shell mode is active', async () => { const uiState = createMockUIState({ shellModeActive: true, @@ -667,7 +681,6 @@ describe('Composer', () => { }); it.each([ - [ApprovalMode.YOLO, 'YOLO'], [ApprovalMode.PLAN, 'plan'], [ApprovalMode.AUTO_EDIT, 'auto edit'], ])( @@ -683,6 +696,19 @@ describe('Composer', () => { }, ); + it('shows minimal mode badge "YOLO" when clean UI details are hidden and YOLO mode is active', async () => { + const config = createMockConfig({ + getAllowedTools: vi.fn(() => ['*']), + }); + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + showApprovalModeIndicator: ApprovalMode.DEFAULT, + }); + + const { lastFrame } = await renderComposer(uiState, undefined, config); + expect(lastFrame()).toContain('YOLO'); + }); + it('hides minimal mode badge while loading in clean mode', async () => { const uiState = createMockUIState({ cleanUiDetailsVisible: false, @@ -945,7 +971,7 @@ describe('Composer', () => { const uiState = createMockUIState({ cleanUiDetailsVisible: true, - showApprovalModeIndicator: ApprovalMode.YOLO, + showApprovalModeIndicator: ApprovalMode.AUTO_EDIT, }); const { lastFrame } = await renderComposer(uiState); diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 0864b8f02b..7de405709a 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -115,24 +115,26 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { const showApprovalIndicator = !uiState.shellModeActive && !hideUiDetailsForSuggestions; const showRawMarkdownIndicator = !uiState.renderMarkdown; + const isYoloMode = config.getAllowedTools()?.includes('*'); let modeBleedThrough: { text: string; color: string } | null = null; - switch (showApprovalModeIndicator) { - case ApprovalMode.YOLO: - modeBleedThrough = { text: 'YOLO', color: theme.status.error }; - break; - case ApprovalMode.PLAN: - modeBleedThrough = { text: 'plan', color: theme.status.success }; - break; - case ApprovalMode.AUTO_EDIT: - modeBleedThrough = { text: 'auto edit', color: theme.status.warning }; - break; - case ApprovalMode.DEFAULT: - modeBleedThrough = null; - break; - default: - checkExhaustive(showApprovalModeIndicator); - modeBleedThrough = null; - break; + if (isYoloMode) { + modeBleedThrough = { text: 'YOLO', color: theme.status.error }; + } else { + switch (showApprovalModeIndicator) { + case ApprovalMode.PLAN: + modeBleedThrough = { text: 'plan', color: theme.status.success }; + break; + case ApprovalMode.AUTO_EDIT: + modeBleedThrough = { text: 'auto edit', color: theme.status.warning }; + break; + case ApprovalMode.DEFAULT: + modeBleedThrough = null; + break; + default: + checkExhaustive(showApprovalModeIndicator); + modeBleedThrough = null; + break; + } } const hideMinimalModeHintWhileBusy = @@ -365,6 +367,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { )} {!showLoadingIndicator && ( @@ -449,6 +452,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { shellModeActive={uiState.shellModeActive} setShellModeActive={uiActions.setShellModeActive} approvalMode={showApprovalModeIndicator} + isYoloMode={isYoloMode} onEscapePromptChange={uiActions.onEscapePromptChange} focus={isFocused} vimHandleInput={uiActions.vimHandleInput} diff --git a/packages/cli/src/ui/components/Help.tsx b/packages/cli/src/ui/components/Help.tsx index 2569623c80..f9b55f4926 100644 --- a/packages/cli/src/ui/components/Help.tsx +++ b/packages/cli/src/ui/components/Help.tsx @@ -153,12 +153,6 @@ export const Help: React.FC = ({ commands }) => ( {' '} - Open input in external editor - - - {formatCommand(Command.TOGGLE_YOLO)} - {' '} - - Toggle YOLO mode - {formatCommand(Command.SUBMIT)} diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index b741506186..43772b9adb 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -3836,15 +3836,6 @@ describe('InputPrompt', () => { unmount(); }); - it('should render correctly in yolo mode', async () => { - props.approvalMode = ApprovalMode.YOLO; - const { stdout, unmount } = renderWithProviders( - , - ); - await waitFor(() => expect(stdout.lastFrame()).toContain('*')); - expect(stdout.lastFrame()).toMatchSnapshot(); - unmount(); - }); it('should not show inverted cursor when shell is focused', async () => { props.isEmbeddedShellFocused = true; props.focus = false; diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 0deb0c40d2..dfb548ada4 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -110,6 +110,7 @@ export interface InputPromptProps { shellModeActive: boolean; setShellModeActive: (value: boolean) => void; approvalMode: ApprovalMode; + isYoloMode?: boolean; onEscapePromptChange?: (showPrompt: boolean) => void; onSuggestionsVisibilityChange?: (visible: boolean) => void; vimHandleInput?: (key: Key) => boolean; @@ -203,6 +204,7 @@ export const InputPrompt: React.FC = ({ shellModeActive, setShellModeActive, approvalMode, + isYoloMode, onEscapePromptChange, onSuggestionsVisibilityChange, vimHandleInput, @@ -1455,8 +1457,7 @@ export const InputPrompt: React.FC = ({ const showAutoAcceptStyling = !shellModeActive && approvalMode === ApprovalMode.AUTO_EDIT; - const showYoloStyling = - !shellModeActive && approvalMode === ApprovalMode.YOLO; + const showYoloStyling = !shellModeActive && isYoloMode; const showPlanStyling = !shellModeActive && approvalMode === ApprovalMode.PLAN; diff --git a/packages/cli/src/ui/components/ShortcutsHelp.tsx b/packages/cli/src/ui/components/ShortcutsHelp.tsx index d94bf2b1d4..2ea6458f47 100644 --- a/packages/cli/src/ui/components/ShortcutsHelp.tsx +++ b/packages/cli/src/ui/components/ShortcutsHelp.tsx @@ -23,7 +23,6 @@ const buildShortcutItems = (): ShortcutItem[] => [ { key: '@', description: 'select file or folder' }, { key: 'Double Esc', description: 'clear & rewind' }, { key: formatCommand(Command.FOCUS_SHELL_INPUT), description: 'focus UI' }, - { key: formatCommand(Command.TOGGLE_YOLO), description: 'YOLO mode' }, { key: formatCommand(Command.CYCLE_APPROVAL_MODE), description: 'cycle mode', @@ -64,16 +63,14 @@ export const ShortcutsHelp: React.FC = () => { const itemsForDisplay = isNarrow ? items : [ - // Keep first column stable: !, @, Esc Esc, Tab Tab. items[0], - items[5], - items[6], - items[1], items[4], - items[7], + items[5], + items[1], + items[6], items[2], + items[7], items[8], - items[9], items[3], ]; diff --git a/packages/cli/src/ui/components/__snapshots__/ApprovalModeIndicator.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ApprovalModeIndicator.test.tsx.snap index 8ddb141478..4e1c6efcd6 100644 --- a/packages/cli/src/ui/components/__snapshots__/ApprovalModeIndicator.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ApprovalModeIndicator.test.tsx.snap @@ -26,6 +26,6 @@ exports[`ApprovalModeIndicator > renders correctly for PLAN mode 1`] = ` `; exports[`ApprovalModeIndicator > renders correctly for YOLO mode 1`] = ` -"YOLO Ctrl+Y +"YOLO " `; diff --git a/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap index 28929deee5..8d03baaa49 100644 --- a/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap @@ -18,20 +18,8 @@ Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more " `; -exports[`ConfigInitDisplay > truncates list of waiting servers if too many 2`] = ` -" -Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more -" -`; - exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = ` " Spinner Connecting to MCP servers... (1/2) - Waiting for: server2 " `; - -exports[`ConfigInitDisplay > updates message on McpClientUpdate event 2`] = ` -" -Spinner Connecting to MCP servers... (1/2) - Waiting for: server2 -" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap index 5a2819702e..6d233ebf66 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap @@ -92,13 +92,6 @@ exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = ` " `; -exports[`InputPrompt > snapshots > should render correctly in yolo mode 1`] = ` -"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - * Type your message or @path/to/file -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -" -`; - exports[`InputPrompt > snapshots > should render correctly when accepting edits 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ > Type your message or @path/to/file diff --git a/packages/cli/src/ui/components/__snapshots__/ShortcutsHelp.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ShortcutsHelp.test.tsx.snap index 9e65c72f69..d9abd9dd0c 100644 --- a/packages/cli/src/ui/components/__snapshots__/ShortcutsHelp.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ShortcutsHelp.test.tsx.snap @@ -7,7 +7,6 @@ exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'linux' 1`] = ` @ select file or folder Double Esc clear & rewind Tab focus UI - Ctrl+Y YOLO mode Shift+Tab cycle mode Ctrl+V paste images Alt+M raw markdown mode @@ -23,7 +22,6 @@ exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'mac' 1`] = ` @ select file or folder Double Esc clear & rewind Tab focus UI - Ctrl+Y YOLO mode Shift+Tab cycle mode Ctrl+V paste images Option+M raw markdown mode @@ -36,9 +34,8 @@ exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'linux' 1`] = ` "──────────────────────────────────────────────────────────────────────────────────────────────────── Shortcuts See /help for more ! shell mode Shift+Tab cycle mode Ctrl+V paste images - @ select file or folder Ctrl+Y YOLO mode Alt+M raw markdown mode - Double Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor - Tab focus UI + @ select file or folder Alt+M raw markdown mode Double Esc clear & rewind + Ctrl+R reverse-search history Ctrl+X open external editor Tab focus UI " `; @@ -46,8 +43,7 @@ exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'mac' 1`] = ` "──────────────────────────────────────────────────────────────────────────────────────────────────── Shortcuts See /help for more ! shell mode Shift+Tab cycle mode Ctrl+V paste images - @ select file or folder Ctrl+Y YOLO mode Option+M raw markdown mode - Double Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor - Tab focus UI + @ select file or folder Option+M raw markdown mode Double Esc clear & rewind + Ctrl+R reverse-search history Ctrl+X open external editor Tab focus UI " `; diff --git a/packages/cli/src/ui/constants/tips.ts b/packages/cli/src/ui/constants/tips.ts index 15aa86c118..61a64ee97f 100644 --- a/packages/cli/src/ui/constants/tips.ts +++ b/packages/cli/src/ui/constants/tips.ts @@ -87,7 +87,7 @@ export const INFORMATIVE_TIPS = [ 'Toggle the debug console display with F12…', 'Toggle the todo list display with Ctrl+T…', 'See full, untruncated responses with Ctrl+O…', - 'Toggle auto-approval (YOLO mode) for all tools with Ctrl+Y…', + 'Cycle through approval modes (Default, Auto-Edit, Plan) with Shift+Tab…', 'Toggle Markdown rendering (raw markdown mode) with Alt+M…', 'Toggle shell mode by typing ! in an empty prompt…', diff --git a/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts b/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts index 34802ad495..ab17917ffa 100644 --- a/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts @@ -162,19 +162,7 @@ describe('useApprovalModeIndicator', () => { expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1); }); - it('should initialize with ApprovalMode.YOLO if config.getApprovalMode returns ApprovalMode.YOLO', () => { - mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO); - const { result } = renderHook(() => - useApprovalModeIndicator({ - config: mockConfigInstance as unknown as ActualConfigType, - addItem: vi.fn(), - }), - ); - expect(result.current).toBe(ApprovalMode.YOLO); - expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1); - }); - - it('should cycle the indicator and update config when Shift+Tab or Ctrl+Y is pressed', () => { + it('should cycle the indicator and update config when Shift+Tab is pressed', () => { mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); const { result } = renderHook(() => useApprovalModeIndicator({ @@ -195,47 +183,6 @@ describe('useApprovalModeIndicator', () => { ApprovalMode.AUTO_EDIT, ); expect(result.current).toBe(ApprovalMode.AUTO_EDIT); - - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.YOLO, - ); - expect(result.current).toBe(ApprovalMode.YOLO); - - // Shift+Tab cycles back to AUTO_EDIT (from YOLO) - act(() => { - capturedUseKeypressHandler({ - name: 'tab', - shift: true, - } as Key); - }); - expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.AUTO_EDIT, - ); - expect(result.current).toBe(ApprovalMode.AUTO_EDIT); - - // Ctrl+Y toggles YOLO - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.YOLO, - ); - expect(result.current).toBe(ApprovalMode.YOLO); - - // Shift+Tab from YOLO jumps to AUTO_EDIT - act(() => { - capturedUseKeypressHandler({ - name: 'tab', - shift: true, - } as Key); - }); - expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.AUTO_EDIT, - ); - expect(result.current).toBe(ApprovalMode.AUTO_EDIT); }); it('should not toggle if only one key or other keys combinations are pressed', () => { @@ -326,36 +273,6 @@ describe('useApprovalModeIndicator', () => { mockConfigInstance.isTrustedFolder.mockReturnValue(false); }); - it('should not enable YOLO mode when Ctrl+Y is pressed', () => { - mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); - mockConfigInstance.setApprovalMode.mockImplementation(() => { - throw new Error( - 'Cannot enable privileged approval modes in an untrusted folder.', - ); - }); - const mockAddItem = vi.fn(); - const { result } = renderHook(() => - useApprovalModeIndicator({ - config: mockConfigInstance as unknown as ActualConfigType, - addItem: mockAddItem, - }), - ); - - expect(result.current).toBe(ApprovalMode.DEFAULT); - - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - - // We expect setApprovalMode to be called, and the error to be caught. - expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.YOLO, - ); - expect(mockAddItem).toHaveBeenCalled(); - // Verify the underlying config value was not changed - expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT); - }); - it('should not enable AUTO_EDIT mode when Shift+Tab is pressed', () => { mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); mockConfigInstance.setApprovalMode.mockImplementation(() => { @@ -389,26 +306,6 @@ describe('useApprovalModeIndicator', () => { expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT); }); - it('should disable YOLO mode when Ctrl+Y is pressed', () => { - mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO); - const mockAddItem = vi.fn(); - renderHook(() => - useApprovalModeIndicator({ - config: mockConfigInstance as unknown as ActualConfigType, - addItem: mockAddItem, - }), - ); - - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - - expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.DEFAULT, - ); - expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT); - }); - it('should disable AUTO_EDIT mode when Shift+Tab is pressed', () => { mockConfigInstance.getApprovalMode.mockReturnValue( ApprovalMode.AUTO_EDIT, @@ -450,19 +347,6 @@ describe('useApprovalModeIndicator', () => { }), ); - // Try to enable YOLO mode - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - - expect(mockAddItem).toHaveBeenCalledWith( - { - type: MessageType.INFO, - text: errorMessage, - }, - expect.any(Number), - ); - // Try to enable AUTO_EDIT mode act(() => { capturedUseKeypressHandler({ @@ -479,126 +363,10 @@ describe('useApprovalModeIndicator', () => { expect.any(Number), ); - expect(mockAddItem).toHaveBeenCalledTimes(2); + expect(mockAddItem).toHaveBeenCalledTimes(1); }); }); - describe('when YOLO mode is disabled by settings', () => { - beforeEach(() => { - // Ensure isYoloModeDisabled returns true for these tests - if (mockConfigInstance && mockConfigInstance.isYoloModeDisabled) { - mockConfigInstance.isYoloModeDisabled.mockReturnValue(true); - } - }); - - it('should not enable YOLO mode when Ctrl+Y is pressed and add an info message', () => { - mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); - mockConfigInstance.getRemoteAdminSettings.mockReturnValue({ - strictModeDisabled: true, - }); - const mockAddItem = vi.fn(); - const { result } = renderHook(() => - useApprovalModeIndicator({ - config: mockConfigInstance as unknown as ActualConfigType, - addItem: mockAddItem, - }), - ); - - expect(result.current).toBe(ApprovalMode.DEFAULT); - - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - - // setApprovalMode should not be called because the check should return early - expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); - // An info message should be added - expect(mockAddItem).toHaveBeenCalledWith( - { - type: MessageType.WARNING, - text: 'You cannot enter YOLO mode since it is disabled in your settings.', - }, - expect.any(Number), - ); - // The mode should not change - expect(result.current).toBe(ApprovalMode.DEFAULT); - }); - - it('should show admin error message when YOLO mode is disabled by admin', () => { - mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); - mockConfigInstance.getRemoteAdminSettings.mockReturnValue({ - mcpEnabled: true, - }); - - const mockAddItem = vi.fn(); - renderHook(() => - useApprovalModeIndicator({ - config: mockConfigInstance as unknown as ActualConfigType, - addItem: mockAddItem, - }), - ); - - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - - expect(mockAddItem).toHaveBeenCalledWith( - { - type: MessageType.WARNING, - text: '[Mock] YOLO mode is disabled', - }, - expect.any(Number), - ); - }); - - it('should show default error message when admin settings are empty', () => { - mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); - mockConfigInstance.getRemoteAdminSettings.mockReturnValue({}); - - const mockAddItem = vi.fn(); - renderHook(() => - useApprovalModeIndicator({ - config: mockConfigInstance as unknown as ActualConfigType, - addItem: mockAddItem, - }), - ); - - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - - expect(mockAddItem).toHaveBeenCalledWith( - { - type: MessageType.WARNING, - text: 'You cannot enter YOLO mode since it is disabled in your settings.', - }, - expect.any(Number), - ); - }); - }); - - it('should call onApprovalModeChange when switching to YOLO mode', () => { - mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); - - const mockOnApprovalModeChange = vi.fn(); - - renderHook(() => - useApprovalModeIndicator({ - config: mockConfigInstance as unknown as ActualConfigType, - onApprovalModeChange: mockOnApprovalModeChange, - }), - ); - - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - - expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.YOLO, - ); - expect(mockOnApprovalModeChange).toHaveBeenCalledWith(ApprovalMode.YOLO); - }); - it('should call onApprovalModeChange when switching to AUTO_EDIT mode', () => { mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); @@ -623,28 +391,6 @@ describe('useApprovalModeIndicator', () => { ); }); - it('should call onApprovalModeChange when switching to DEFAULT mode', () => { - mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO); - - const mockOnApprovalModeChange = vi.fn(); - - renderHook(() => - useApprovalModeIndicator({ - config: mockConfigInstance as unknown as ActualConfigType, - onApprovalModeChange: mockOnApprovalModeChange, - }), - ); - - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); // This should toggle from YOLO to DEFAULT - }); - - expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.DEFAULT, - ); - expect(mockOnApprovalModeChange).toHaveBeenCalledWith(ApprovalMode.DEFAULT); - }); - it('should not call onApprovalModeChange when callback is not provided', () => { mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); @@ -654,47 +400,14 @@ describe('useApprovalModeIndicator', () => { }), ); - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - - expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.YOLO, - ); - // Should not throw an error when callback is not provided - }); - - it('should handle multiple mode changes correctly', () => { - mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); - - const mockOnApprovalModeChange = vi.fn(); - - renderHook(() => - useApprovalModeIndicator({ - config: mockConfigInstance as unknown as ActualConfigType, - onApprovalModeChange: mockOnApprovalModeChange, - }), - ); - - // Switch to YOLO - act(() => { - capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); - }); - - // Switch to AUTO_EDIT act(() => { capturedUseKeypressHandler({ name: 'tab', shift: true } as Key); }); - expect(mockOnApprovalModeChange).toHaveBeenCalledTimes(2); - expect(mockOnApprovalModeChange).toHaveBeenNthCalledWith( - 1, - ApprovalMode.YOLO, - ); - expect(mockOnApprovalModeChange).toHaveBeenNthCalledWith( - 2, + expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( ApprovalMode.AUTO_EDIT, ); + // Should not throw an error when callback is not provided }); it('should cycle to PLAN when allowPlanMode is true', () => { diff --git a/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts b/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts index 1dd6c6468e..1f690aba22 100644 --- a/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts +++ b/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts @@ -5,11 +5,7 @@ */ import { useState, useEffect } from 'react'; -import { - ApprovalMode, - type Config, - getAdminErrorMessage, -} from '@google/gemini-cli-core'; +import { ApprovalMode, type Config } from '@google/gemini-cli-core'; import { useKeypress } from './useKeypress.js'; import { Command } from '../key/keyMatchers.js'; import { useKeyMatchers } from './useKeyMatchers.js'; @@ -42,36 +38,7 @@ export function useApprovalModeIndicator({ (key) => { let nextApprovalMode: ApprovalMode | undefined; - if (keyMatchers[Command.TOGGLE_YOLO](key)) { - if ( - config.isYoloModeDisabled() && - config.getApprovalMode() !== ApprovalMode.YOLO - ) { - if (addItem) { - let text = - 'You cannot enter YOLO mode since it is disabled in your settings.'; - const adminSettings = config.getRemoteAdminSettings(); - const hasSettings = - adminSettings && Object.keys(adminSettings).length > 0; - if (hasSettings && !adminSettings.strictModeDisabled) { - text = getAdminErrorMessage('YOLO mode', config); - } - - addItem( - { - type: MessageType.WARNING, - text, - }, - Date.now(), - ); - } - return; - } - nextApprovalMode = - config.getApprovalMode() === ApprovalMode.YOLO - ? ApprovalMode.DEFAULT - : ApprovalMode.YOLO; - } else if (keyMatchers[Command.CYCLE_APPROVAL_MODE](key)) { + if (keyMatchers[Command.CYCLE_APPROVAL_MODE](key)) { const currentMode = config.getApprovalMode(); switch (currentMode) { case ApprovalMode.DEFAULT: @@ -85,9 +52,7 @@ export function useApprovalModeIndicator({ case ApprovalMode.PLAN: nextApprovalMode = ApprovalMode.DEFAULT; break; - case ApprovalMode.YOLO: - nextApprovalMode = ApprovalMode.AUTO_EDIT; - break; + default: } } diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index c93eb53cd2..37c53c3edb 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -40,8 +40,6 @@ import { AuthType, GeminiEventType as ServerGeminiEventType, ToolErrorType, - ToolConfirmationOutcome, - MessageBusType, tokenLimit, debugLogger, coreEvents, @@ -2072,35 +2070,6 @@ describe('useGeminiStream', () => { }); describe('handleApprovalModeChange', () => { - it('should auto-approve all pending tool calls when switching to YOLO mode', async () => { - const awaitingApprovalToolCalls: TrackedToolCall[] = [ - createMockToolCall('replace', 'call1', 'edit'), - createMockToolCall('read_file', 'call2', 'info'), - ]; - - const { result } = renderTestHook(awaitingApprovalToolCalls); - - await act(async () => { - await result.current.handleApprovalModeChange(ApprovalMode.YOLO); - }); - - // Both tool calls should be auto-approved - expect(mockMessageBus.publish).toHaveBeenCalledTimes(2); - expect(mockMessageBus.publish).toHaveBeenCalledWith( - expect.objectContaining({ - type: MessageBusType.TOOL_CONFIRMATION_RESPONSE, - correlationId: 'corr-call1', - outcome: ToolConfirmationOutcome.ProceedOnce, - }), - ); - expect(mockMessageBus.publish).toHaveBeenCalledWith( - expect.objectContaining({ - correlationId: 'corr-call2', - outcome: ToolConfirmationOutcome.ProceedOnce, - }), - ); - }); - it('should only auto-approve edit tools when switching to AUTO_EDIT mode', async () => { const awaitingApprovalToolCalls: TrackedToolCall[] = [ createMockToolCall('replace', 'call1', 'edit'), @@ -2157,7 +2126,7 @@ describe('useGeminiStream', () => { const { result } = renderTestHook(awaitingApprovalToolCalls); await act(async () => { - await result.current.handleApprovalModeChange(ApprovalMode.YOLO); + await result.current.handleApprovalModeChange(ApprovalMode.AUTO_EDIT); }); // Both should be attempted despite first error @@ -2200,7 +2169,7 @@ describe('useGeminiStream', () => { // Should not throw an error await act(async () => { - await result.current.handleApprovalModeChange(ApprovalMode.YOLO); + await result.current.handleApprovalModeChange(ApprovalMode.AUTO_EDIT); }); }); @@ -2242,7 +2211,7 @@ describe('useGeminiStream', () => { const { result } = renderTestHook(mixedStatusToolCalls); await act(async () => { - await result.current.handleApprovalModeChange(ApprovalMode.YOLO); + await result.current.handleApprovalModeChange(ApprovalMode.AUTO_EDIT); }); // Only the awaiting_approval tool should be processed. diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 2034e14b87..78cd63b1d6 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -15,8 +15,6 @@ import { UnauthorizedError, UserPromptEvent, DEFAULT_GEMINI_FLASH_MODEL, - logConversationFinishedEvent, - ConversationFinishedEvent, ApprovalMode, parseAndFormatApiError, ToolConfirmationOutcome, @@ -604,27 +602,6 @@ export const useGeminiStream = ( prevActiveShellPtyIdRef.current = activeShellPtyId; }, [activeShellPtyId, addItem, setIsResponding]); - useEffect(() => { - if ( - config.getApprovalMode() === ApprovalMode.YOLO && - streamingState === StreamingState.Idle - ) { - const lastUserMessageIndex = history.findLastIndex( - (item: HistoryItem) => item.type === MessageType.USER, - ); - - const turnCount = - lastUserMessageIndex === -1 ? 0 : history.length - lastUserMessageIndex; - - if (turnCount > 0) { - logConversationFinishedEvent( - config, - new ConversationFinishedEvent(config.getApprovalMode(), turnCount), - ); - } - } - }, [streamingState, config, history]); - useEffect(() => { if (!isResponding) { setRetryStatus(null); @@ -1652,10 +1629,7 @@ export const useGeminiStream = ( previousApprovalModeRef.current = newApprovalMode; // Auto-approve pending tool calls when switching to auto-approval modes - if ( - newApprovalMode === ApprovalMode.YOLO || - newApprovalMode === ApprovalMode.AUTO_EDIT - ) { + if (newApprovalMode === ApprovalMode.AUTO_EDIT) { let awaitingApprovalCalls = toolCalls.filter( (call): call is TrackedWaitingToolCall => call.status === 'awaiting_approval', diff --git a/packages/cli/src/ui/key/keyBindings.ts b/packages/cli/src/ui/key/keyBindings.ts index 5b1afc0735..2e8e35648e 100644 --- a/packages/cli/src/ui/key/keyBindings.ts +++ b/packages/cli/src/ui/key/keyBindings.ts @@ -84,7 +84,6 @@ export enum Command { SHOW_IDE_CONTEXT_DETAIL = 'app.showIdeContextDetail', TOGGLE_MARKDOWN = 'app.toggleMarkdown', TOGGLE_COPY_MODE = 'app.toggleCopyMode', - TOGGLE_YOLO = 'app.toggleYolo', CYCLE_APPROVAL_MODE = 'app.cycleApprovalMode', SHOW_MORE_LINES = 'app.showMoreLines', EXPAND_PASTE = 'app.expandPaste', @@ -379,7 +378,6 @@ export const defaultKeyBindingConfig: KeyBindingConfig = new Map([ [Command.SHOW_IDE_CONTEXT_DETAIL, [new KeyBinding('ctrl+g')]], [Command.TOGGLE_MARKDOWN, [new KeyBinding('alt+m')]], [Command.TOGGLE_COPY_MODE, [new KeyBinding('ctrl+s')]], - [Command.TOGGLE_YOLO, [new KeyBinding('ctrl+y')]], [Command.CYCLE_APPROVAL_MODE, [new KeyBinding('shift+tab')]], [Command.SHOW_MORE_LINES, [new KeyBinding('ctrl+o')]], [Command.EXPAND_PASTE, [new KeyBinding('ctrl+o')]], @@ -500,7 +498,6 @@ export const commandCategories: readonly CommandCategory[] = [ Command.SHOW_IDE_CONTEXT_DETAIL, Command.TOGGLE_MARKDOWN, Command.TOGGLE_COPY_MODE, - Command.TOGGLE_YOLO, Command.CYCLE_APPROVAL_MODE, Command.SHOW_MORE_LINES, Command.EXPAND_PASTE, @@ -603,7 +600,7 @@ export const commandDescriptions: Readonly> = { [Command.SHOW_IDE_CONTEXT_DETAIL]: 'Show IDE context details.', [Command.TOGGLE_MARKDOWN]: 'Toggle Markdown rendering.', [Command.TOGGLE_COPY_MODE]: 'Toggle copy mode when in alternate buffer mode.', - [Command.TOGGLE_YOLO]: 'Toggle YOLO (auto-approval) mode for tool calls.', + [Command.CYCLE_APPROVAL_MODE]: 'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy.', [Command.SHOW_MORE_LINES]: diff --git a/packages/cli/src/ui/key/keyMatchers.test.ts b/packages/cli/src/ui/key/keyMatchers.test.ts index ab12ca1ddf..fb8167fe11 100644 --- a/packages/cli/src/ui/key/keyMatchers.test.ts +++ b/packages/cli/src/ui/key/keyMatchers.test.ts @@ -403,11 +403,7 @@ describe('keyMatchers', () => { positive: [createKey('tab')], negative: [createKey('f6'), createKey('f', { ctrl: true })], }, - { - command: Command.TOGGLE_YOLO, - positive: [createKey('y', { ctrl: true })], - negative: [createKey('y'), createKey('y', { alt: true })], - }, + { command: Command.CYCLE_APPROVAL_MODE, positive: [createKey('tab', { shift: true })], diff --git a/packages/cli/src/utils/handleAutoUpdate.test.ts b/packages/cli/src/utils/handleAutoUpdate.test.ts index 94795bf94e..b10204834b 100644 --- a/packages/cli/src/utils/handleAutoUpdate.test.ts +++ b/packages/cli/src/utils/handleAutoUpdate.test.ts @@ -202,12 +202,7 @@ describe('handleAutoUpdate', () => { expect(mockSpawn).not.toHaveBeenCalled(); }); - it.each([ - PackageManager.NPX, - PackageManager.PNPX, - PackageManager.BUNX, - PackageManager.BINARY, - ])( + it.each([PackageManager.NPX, PackageManager.PNPX, PackageManager.BUNX])( 'should suppress update notifications when running via %s', (packageManager) => { mockGetInstallationInfo.mockReturnValue({ diff --git a/packages/cli/src/utils/handleAutoUpdate.ts b/packages/cli/src/utils/handleAutoUpdate.ts index bd0effa53b..348acd33b0 100644 --- a/packages/cli/src/utils/handleAutoUpdate.ts +++ b/packages/cli/src/utils/handleAutoUpdate.ts @@ -87,12 +87,9 @@ export function handleAutoUpdate( ); if ( - [ - PackageManager.NPX, - PackageManager.PNPX, - PackageManager.BUNX, - PackageManager.BINARY, - ].includes(installationInfo.packageManager) + [PackageManager.NPX, PackageManager.PNPX, PackageManager.BUNX].includes( + installationInfo.packageManager, + ) ) { return; } diff --git a/packages/cli/src/utils/installationInfo.test.ts b/packages/cli/src/utils/installationInfo.test.ts index fbebec8bf7..ca1120c0e3 100644 --- a/packages/cli/src/utils/installationInfo.test.ts +++ b/packages/cli/src/utils/installationInfo.test.ts @@ -58,19 +58,6 @@ describe('getInstallationInfo', () => { process.argv = originalArgv; }); - it('should detect running as a standalone binary', () => { - vi.stubEnv('IS_BINARY', 'true'); - process.argv[1] = '/path/to/binary'; - const info = getInstallationInfo(projectRoot, true); - expect(info.packageManager).toBe(PackageManager.BINARY); - expect(info.isGlobal).toBe(true); - expect(info.updateMessage).toBe( - 'Running as a standalone binary. Please update by downloading the latest version from GitHub.', - ); - expect(info.updateCommand).toBeUndefined(); - vi.unstubAllEnvs(); - }); - it('should return UNKNOWN when cliPath is not available', () => { process.argv[1] = ''; const info = getInstallationInfo(projectRoot, true); diff --git a/packages/cli/src/utils/installationInfo.ts b/packages/cli/src/utils/installationInfo.ts index 39d77ba640..a682cc75e1 100644 --- a/packages/cli/src/utils/installationInfo.ts +++ b/packages/cli/src/utils/installationInfo.ts @@ -21,7 +21,6 @@ export enum PackageManager { BUNX = 'bunx', HOMEBREW = 'homebrew', NPX = 'npx', - BINARY = 'binary', UNKNOWN = 'unknown', } @@ -42,16 +41,6 @@ export function getInstallationInfo( } try { - // Check for standalone binary first - if (process.env['IS_BINARY'] === 'true') { - return { - packageManager: PackageManager.BINARY, - isGlobal: true, - updateMessage: - 'Running as a standalone binary. Please update by downloading the latest version from GitHub.', - }; - } - // Normalize path separators to forward slashes for consistent matching. const realPath = fs.realpathSync(cliPath).replace(/\\/g, '/'); const normalizedProjectRoot = projectRoot?.replace(/\\/g, '/'); diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index eff489dcd6..f033e288fe 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -1751,7 +1751,7 @@ describe('setApprovalMode with folder trust', () => { it('should throw an error when setting YOLO mode in an untrusted folder', () => { const config = new Config(baseParams); vi.spyOn(config, 'isTrustedFolder').mockReturnValue(false); - expect(() => config.setApprovalMode(ApprovalMode.YOLO)).toThrow( + expect(() => config.setApprovalMode(ApprovalMode.PLAN)).toThrow( 'Cannot enable privileged approval modes in an untrusted folder.', ); }); @@ -1773,7 +1773,7 @@ describe('setApprovalMode with folder trust', () => { it('should NOT throw an error when setting any mode in a trusted folder', () => { const config = new Config(baseParams); vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true); - expect(() => config.setApprovalMode(ApprovalMode.YOLO)).not.toThrow(); + expect(() => config.setApprovalMode(ApprovalMode.PLAN)).not.toThrow(); expect(() => config.setApprovalMode(ApprovalMode.AUTO_EDIT)).not.toThrow(); expect(() => config.setApprovalMode(ApprovalMode.DEFAULT)).not.toThrow(); }); @@ -1781,7 +1781,7 @@ describe('setApprovalMode with folder trust', () => { it('should NOT throw an error when setting any mode if trustedFolder is undefined', () => { const config = new Config(baseParams); vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true); // isTrustedFolder defaults to true - expect(() => config.setApprovalMode(ApprovalMode.YOLO)).not.toThrow(); + expect(() => config.setApprovalMode(ApprovalMode.PLAN)).not.toThrow(); expect(() => config.setApprovalMode(ApprovalMode.AUTO_EDIT)).not.toThrow(); expect(() => config.setApprovalMode(ApprovalMode.DEFAULT)).not.toThrow(); }); @@ -1829,7 +1829,7 @@ describe('setApprovalMode with folder trust', () => { } as Partial as ToolRegistry); const updateSpy = vi.spyOn(config, 'updateSystemInstructionIfInitialized'); - config.setApprovalMode(ApprovalMode.YOLO); + config.setApprovalMode(ApprovalMode.PLAN); expect(updateSpy).toHaveBeenCalled(); }); @@ -1910,7 +1910,7 @@ describe('setApprovalMode with folder trust', () => { vi.mocked(logApprovalModeDuration).mockClear(); performanceSpy.mockReturnValueOnce(time3); - config.setApprovalMode(ApprovalMode.YOLO); + config.setApprovalMode(ApprovalMode.AUTO_EDIT); expect(logApprovalModeDuration).toHaveBeenCalledWith( config, expect.objectContaining({ diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index aa3e9aa5b6..d67b03ef7e 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -2290,11 +2290,8 @@ export class Config implements McpContext, AgentLoopContext { const isPlanModeTransition = currentMode !== mode && (currentMode === ApprovalMode.PLAN || mode === ApprovalMode.PLAN); - const isYoloModeTransition = - currentMode !== mode && - (currentMode === ApprovalMode.YOLO || mode === ApprovalMode.YOLO); - if (isPlanModeTransition || isYoloModeTransition) { + if (isPlanModeTransition) { if (this._geminiClient?.isInitialized()) { this._geminiClient.setTools().catch((err) => { debugLogger.error('Failed to update tools', err); diff --git a/packages/core/src/core/coreToolScheduler.test.ts b/packages/core/src/core/coreToolScheduler.test.ts index 3a9d0e2e92..a8f3245383 100644 --- a/packages/core/src/core/coreToolScheduler.test.ts +++ b/packages/core/src/core/coreToolScheduler.test.ts @@ -303,11 +303,10 @@ function createMockConfig(overrides: Partial = {}): Config { _serverName?: string, ) => { // Mock simple policy logic for tests - const mode = finalConfig.getApprovalMode(); - if (mode === ApprovalMode.YOLO) { + const allowed = finalConfig.getAllowedTools(); + if (allowed?.includes('*')) { return { decision: PolicyDecision.ALLOW }; } - const allowed = finalConfig.getAllowedTools(); if ( allowed && (allowed.includes(toolCall.name) || @@ -992,7 +991,7 @@ describe('CoreToolScheduler YOLO mode', () => { // Configure the scheduler for YOLO mode. const mockConfig = createMockConfig({ getToolRegistry: () => mockToolRegistry, - getApprovalMode: () => ApprovalMode.YOLO, + getAllowedTools: () => ['*'], isInteractive: () => false, }); const mockMessageBus = createMockMessageBus(); @@ -1084,7 +1083,7 @@ describe('CoreToolScheduler request queueing', () => { const mockConfig = createMockConfig({ getToolRegistry: () => mockToolRegistry, - getApprovalMode: () => ApprovalMode.YOLO, // Use YOLO to avoid confirmation prompts + getAllowedTools: () => ['*'], // Use YOLO to avoid confirmation prompts isInteractive: () => false, }); const mockMessageBus = createMockMessageBus(); @@ -1385,7 +1384,7 @@ describe('CoreToolScheduler request queueing', () => { const mockConfig = createMockConfig({ getToolRegistry: () => mockToolRegistry, - getApprovalMode: () => ApprovalMode.YOLO, + getAllowedTools: () => ['*'], }); const mockMessageBus = createMockMessageBus(); mockConfig.getMessageBus = vi.fn().mockReturnValue(mockMessageBus); @@ -1623,7 +1622,7 @@ describe('CoreToolScheduler Sequential Execution', () => { const mockConfig = createMockConfig({ getToolRegistry: () => mockToolRegistry, - getApprovalMode: () => ApprovalMode.YOLO, // Use YOLO to avoid confirmation prompts + getAllowedTools: () => ['*'], // Use YOLO to avoid confirmation prompts isInteractive: () => false, }); const mockMessageBus = createMockMessageBus(); @@ -1728,7 +1727,7 @@ describe('CoreToolScheduler Sequential Execution', () => { const mockConfig = createMockConfig({ getToolRegistry: () => mockToolRegistry, - getApprovalMode: () => ApprovalMode.YOLO, + getAllowedTools: () => ['*'], isInteractive: () => false, }); const mockMessageBus = createMockMessageBus(); @@ -2072,7 +2071,7 @@ describe('CoreToolScheduler Sequential Execution', () => { const mockConfig = createMockConfig({ getToolRegistry: () => mockToolRegistry, - getApprovalMode: () => ApprovalMode.YOLO, + getAllowedTools: () => ['*'], isInteractive: () => false, }); const mockMessageBus = createMockMessageBus(); @@ -2146,7 +2145,7 @@ describe('CoreToolScheduler Sequential Execution', () => { const mockConfig = createMockConfig({ getToolRegistry: () => mockToolRegistry, - getApprovalMode: () => ApprovalMode.YOLO, + getAllowedTools: () => ['*'], isInteractive: () => false, }); mockConfig.getHookSystem = vi.fn().mockReturnValue(undefined); diff --git a/packages/core/src/core/prompts-substitution.test.ts b/packages/core/src/core/prompts-substitution.test.ts index 9bad6a066d..47e20859ea 100644 --- a/packages/core/src/core/prompts-substitution.test.ts +++ b/packages/core/src/core/prompts-substitution.test.ts @@ -58,6 +58,8 @@ describe('Core System Prompt Substitution', () => { getSkillManager: vi.fn().mockReturnValue({ getSkills: vi.fn().mockReturnValue([]), }), + getAllowedTools: vi.fn().mockReturnValue([]), + getApprovalMode: vi.fn().mockReturnValue('default'), getApprovedPlanPath: vi.fn().mockReturnValue(undefined), } as unknown as Config; }); diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index 82a7943de4..7fbccdc871 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -116,6 +116,7 @@ describe('Core System Prompt (prompts.ts)', () => { getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), getApprovedPlanPath: vi.fn().mockReturnValue(undefined), isTrackerEnabled: vi.fn().mockReturnValue(false), + getAllowedTools: vi.fn().mockReturnValue([]), get config() { return this; }, @@ -436,6 +437,7 @@ describe('Core System Prompt (prompts.ts)', () => { }), getApprovedPlanPath: vi.fn().mockReturnValue(undefined), isTrackerEnabled: vi.fn().mockReturnValue(false), + getAllowedTools: vi.fn().mockReturnValue([]), get config() { return this; }, @@ -591,7 +593,7 @@ describe('Core System Prompt (prompts.ts)', () => { }); it('should include YOLO mode instructions in interactive mode', () => { - vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.YOLO); + vi.mocked(mockConfig.getAllowedTools).mockReturnValue(['*']); vi.mocked(mockConfig.isInteractive).mockReturnValue(true); const prompt = getCoreSystemPrompt(mockConfig); expect(prompt).toContain('# Autonomous Mode (YOLO)'); @@ -599,7 +601,7 @@ describe('Core System Prompt (prompts.ts)', () => { }); it('should NOT include YOLO mode instructions in non-interactive mode', () => { - vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.YOLO); + vi.mocked(mockConfig.getAllowedTools).mockReturnValue(['*']); vi.mocked(mockConfig.isInteractive).mockReturnValue(false); const prompt = getCoreSystemPrompt(mockConfig); expect(prompt).not.toContain('# Autonomous Mode (YOLO)'); diff --git a/packages/core/src/hooks/hookAggregator.ts b/packages/core/src/hooks/hookAggregator.ts index 73e814702e..523bc823fd 100644 --- a/packages/core/src/hooks/hookAggregator.ts +++ b/packages/core/src/hooks/hookAggregator.ts @@ -355,7 +355,6 @@ export class HookAggregator { // Extract additionalContext from various hook types if ( 'additionalContext' in specific && - // eslint-disable-next-line no-restricted-syntax typeof specific['additionalContext'] === 'string' ) { contexts.push(specific['additionalContext']); diff --git a/packages/core/src/policy/config.test.ts b/packages/core/src/policy/config.test.ts index 0e2301c1c8..1a1ceff066 100644 --- a/packages/core/src/policy/config.test.ts +++ b/packages/core/src/policy/config.test.ts @@ -311,13 +311,16 @@ describe('createPolicyEngineConfig', () => { expect(excludedRule?.priority).toBe(4.9); // MCP excluded server }); - it('should allow all tools in YOLO mode', async () => { - const config = await createPolicyEngineConfig({}, ApprovalMode.YOLO); + it('should allow all tools with wildcard allowedTools', async () => { + const config = await createPolicyEngineConfig( + { tools: { allowed: ['*'] } }, + ApprovalMode.DEFAULT, + ); const rule = config.rules?.find( - (r) => r.decision === PolicyDecision.ALLOW && !r.toolName, + (r) => r.decision === PolicyDecision.ALLOW && r.toolName === '*', ); expect(rule).toBeDefined(); - expect(rule?.priority).toBeCloseTo(1.998, 5); + expect(rule?.priority).toBeCloseTo(4.3, 5); }); it('should allow edit tool in AUTO_EDIT mode', async () => { @@ -506,14 +509,14 @@ describe('createPolicyEngineConfig', () => { expect(explicitFalseRule).toBeUndefined(); }); - it('should have YOLO allow-all rule beat write tool rules in YOLO mode', async () => { + it('should have wildcard allow rule beat write tool rules', async () => { const config = await createPolicyEngineConfig( - { tools: { exclude: ['dangerous-tool'] } }, - ApprovalMode.YOLO, + { tools: { allowed: ['*'], exclude: ['dangerous-tool'] } }, + ApprovalMode.DEFAULT, ); const wildcardRule = config.rules?.find( - (r) => !r.toolName && r.decision === PolicyDecision.ALLOW, + (r) => r.toolName === '*' && r.decision === PolicyDecision.ALLOW, ); const writeToolRules = config.rules?.filter( (r) => diff --git a/packages/core/src/policy/config.ts b/packages/core/src/policy/config.ts index 392ab15c0c..7734c00465 100644 --- a/packages/core/src/policy/config.ts +++ b/packages/core/src/policy/config.ts @@ -9,15 +9,14 @@ import * as path from 'node:path'; import * as crypto from 'node:crypto'; import { fileURLToPath } from 'node:url'; import { Storage } from '../config/storage.js'; -import { +import type { ApprovalMode, - type PolicyEngineConfig, - PolicyDecision, - type PolicyRule, - type PolicySettings, - type SafetyCheckerRule, - ALWAYS_ALLOW_PRIORITY_OFFSET, + PolicyEngineConfig, + PolicyRule, + PolicySettings, + SafetyCheckerRule, } from './types.js'; +import { PolicyDecision, ALWAYS_ALLOW_PRIORITY_OFFSET } from './types.js'; import type { PolicyEngine } from './policy-engine.js'; import { loadPoliciesFromToml, type PolicyFileError } from './toml-loader.js'; import { buildArgsPatterns, isSafeRegExp } from './utils.js'; @@ -217,7 +216,7 @@ async function filterSecurePolicyDirectories( /** * Loads and sanitizes policies from an extension's policies directory. - * Security: Filters out 'ALLOW' rules and YOLO mode configurations. + * Security: Filters out 'ALLOW' rules and Allow-all configurations. */ export async function loadExtensionPolicies( extensionName: string, @@ -241,14 +240,6 @@ export async function loadExtensionPolicies( return false; } - // Security: Extensions are not allowed to contribute YOLO mode rules. - if (rule.modes?.includes(ApprovalMode.YOLO)) { - debugLogger.warn( - `[PolicyConfig] Extension "${extensionName}" attempted to contribute a rule for YOLO mode. Ignoring this rule for security.`, - ); - return false; - } - // Prefix source with extension name to avoid collisions and double prefixing. // toml-loader.ts adds "Extension: file.toml", we transform it to "Extension (name): file.toml". rule.source = rule.source?.replace( @@ -259,14 +250,6 @@ export async function loadExtensionPolicies( }); const checkers = result.checkers.filter((checker) => { - // Security: Extensions are not allowed to contribute YOLO mode checkers. - if (checker.modes?.includes(ApprovalMode.YOLO)) { - debugLogger.warn( - `[PolicyConfig] Extension "${extensionName}" attempted to contribute a safety checker for YOLO mode. Ignoring this checker for security.`, - ); - return false; - } - // Prefix source with extension name. checker.source = checker.source?.replace( /^Extension: /, @@ -397,7 +380,7 @@ export async function createPolicyEngineConfig( // 50: Read-only tools (becomes 1.050 in default tier) // 60: Plan mode catch-all DENY override (becomes 1.060 in default tier) // 70: Plan mode explicit ALLOW override (becomes 1.070 in default tier) - // 999: YOLO mode allow-all (becomes 1.999 in default tier) + // 999: Allow-all (becomes 1.999 in default tier) // MCP servers that are explicitly excluded in settings.mcp.excluded // Priority: MCP_EXCLUDED_PRIORITY (highest in user tier for security - persistent server blocks) @@ -433,6 +416,17 @@ export async function createPolicyEngineConfig( // Priority: ALLOWED_TOOLS_FLAG_PRIORITY (user tier - explicit temporary allows) if (settings.tools?.allowed) { for (const tool of settings.tools.allowed) { + if (tool === '*') { + rules.push({ + toolName: '*', + decision: PolicyDecision.ALLOW, + priority: ALLOWED_TOOLS_FLAG_PRIORITY, + source: 'Settings (Tools Allowed)', + allowRedirection: true, + }); + continue; + } + // Check for legacy format: toolName(args) const match = tool.match(/^([a-zA-Z0-9_-]+)\((.*)\)$/); if (match) { diff --git a/packages/core/src/policy/policies/plan.toml b/packages/core/src/policy/policies/plan.toml index 5a7ee6e59f..93b0e580cf 100644 --- a/packages/core/src/policy/policies/plan.toml +++ b/packages/core/src/policy/policies/plan.toml @@ -25,7 +25,7 @@ # 10: Write tools default to ASK_USER (becomes 1.010 in default tier) # 60: Plan mode catch-all DENY override (becomes 1.060 in default tier) # 70: Plan mode explicit ALLOW override (becomes 1.070 in default tier) -# 999: YOLO mode allow-all (becomes 1.999 in default tier) +# 999: Allow-all allow-all (becomes 1.999 in default tier) # Mode Transitions (into/out of Plan Mode) diff --git a/packages/core/src/policy/policies/read-only.toml b/packages/core/src/policy/policies/read-only.toml index 8435e49d0b..9109cdd16b 100644 --- a/packages/core/src/policy/policies/read-only.toml +++ b/packages/core/src/policy/policies/read-only.toml @@ -25,7 +25,7 @@ # 10: Write tools default to ASK_USER (becomes 1.010 in default tier) # 15: Auto-edit tool override (becomes 1.015 in default tier) # 50: Read-only tools (becomes 1.050 in default tier) -# 999: YOLO mode allow-all (becomes 1.999 in default tier) +# 999: Allow-all allow-all (becomes 1.999 in default tier) [[rule]] toolName = "glob" diff --git a/packages/core/src/policy/policies/write.toml b/packages/core/src/policy/policies/write.toml index c24f6dfee3..5cf396c310 100644 --- a/packages/core/src/policy/policies/write.toml +++ b/packages/core/src/policy/policies/write.toml @@ -25,7 +25,7 @@ # 10: Write tools default to ASK_USER (becomes 1.010 in default tier) # 15: Auto-edit tool override (becomes 1.015 in default tier) # 50: Read-only tools (becomes 1.050 in default tier) -# 999: YOLO mode allow-all (becomes 1.999 in default tier) +# 999: Allow-all allow-all (becomes 1.999 in default tier) [[rule]] toolName = "replace" diff --git a/packages/core/src/policy/policies/yolo.toml b/packages/core/src/policy/policies/yolo.toml deleted file mode 100644 index 230b4c2670..0000000000 --- a/packages/core/src/policy/policies/yolo.toml +++ /dev/null @@ -1,55 +0,0 @@ -# Priority system for policy rules: -# - Higher priority numbers win over lower priority numbers -# - When multiple rules match, the highest priority rule is applied -# - Rules are evaluated in order of priority (highest first) -# -# Priority bands (tiers): -# - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100) -# - Extension policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) -# - Workspace policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) -# - User policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100) -# - Admin policies (TOML): 5 + priority/1000 (e.g., priority 100 → 5.100) -# -# This ensures Admin > User > Workspace > Extension > Default hierarchy is always preserved, -# while allowing user-specified priorities to work within each tier. -# -# Settings-based and dynamic rules (all in user tier 4.x): -# 4.95: Tools that the user has selected as "Always Allow" in the interactive UI -# 4.9: MCP servers excluded list (security: persistent server blocks) -# 4.4: Command line flag --exclude-tools (explicit temporary blocks) -# 4.3: Command line flag --allowed-tools (explicit temporary allows) -# 4.2: MCP servers with trust=true (persistent trusted servers) -# 4.1: MCP servers allowed list (persistent general server allows) -# -# TOML policy priorities (before transformation): -# 10: Write tools default to ASK_USER (becomes 1.010 in default tier) -# 15: Auto-edit tool override (becomes 1.015 in default tier) -# 50: Read-only tools (becomes 1.050 in default tier) -# 998: YOLO mode allow-all (becomes 1.998 in default tier) -# 999: Ask-user tool (becomes 1.999 in default tier) - -# Ask-user tool always requires user interaction, even in YOLO mode. -# This ensures the model can gather user preferences/decisions when needed. -# Note: In non-interactive mode, this decision is converted to DENY by the policy engine. -[[rule]] -toolName = "ask_user" -decision = "ask_user" -priority = 999 -modes = ["yolo"] - -# Plan mode transitions are blocked in YOLO mode to maintain state consistency -# and because planning currently requires human interaction (plan approval), -# which conflicts with YOLO's autonomous nature. -[[rule]] -toolName = ["enter_plan_mode", "exit_plan_mode"] -decision = "deny" -priority = 999 -modes = ["yolo"] -interactive = true - -# Allow everything else in YOLO mode -[[rule]] -decision = "allow" -priority = 998 -modes = ["yolo"] -allow_redirection = true diff --git a/packages/core/src/policy/policy-engine.test.ts b/packages/core/src/policy/policy-engine.test.ts index 5e03443722..6c78a9efcc 100644 --- a/packages/core/src/policy/policy-engine.test.ts +++ b/packages/core/src/policy/policy-engine.test.ts @@ -15,7 +15,6 @@ import { ApprovalMode, PRIORITY_SUBAGENT_TOOL, ALWAYS_ALLOW_PRIORITY_FRACTION, - PRIORITY_YOLO_ALLOW_ALL, } from './types.js'; import type { FunctionCall } from '@google/genai'; import { SafetyCheckDecision } from '../safety/protocol.js'; @@ -336,19 +335,25 @@ describe('PolicyEngine', () => { ); }); - it('should return ALLOW by default in YOLO mode when no rules match', async () => { - engine = new PolicyEngine({ approvalMode: ApprovalMode.YOLO }); + it('should return ALLOW by default when a wildcard ALLOW rule exists', async () => { + engine = new PolicyEngine({ + rules: [{ toolName: '*', decision: PolicyDecision.ALLOW, priority: 1 }], + }); - // No rules defined, should return ALLOW in YOLO mode const { decision } = await engine.check({ name: 'any-tool' }, undefined); expect(decision).toBe(PolicyDecision.ALLOW); }); - it('should NOT override explicit DENY rules in YOLO mode', async () => { + it('should NOT override explicit DENY rules when a wildcard rule exists', async () => { const rules: PolicyRule[] = [ - { toolName: 'dangerous-tool', decision: PolicyDecision.DENY }, + { toolName: '*', decision: PolicyDecision.ALLOW, priority: 1 }, + { + toolName: 'dangerous-tool', + decision: PolicyDecision.DENY, + priority: 10, + }, ]; - engine = new PolicyEngine({ rules, approvalMode: ApprovalMode.YOLO }); + engine = new PolicyEngine({ rules }); const { decision } = await engine.check( { name: 'dangerous-tool' }, @@ -362,18 +367,18 @@ describe('PolicyEngine', () => { ).toBe(PolicyDecision.ALLOW); }); - it('should respect rule priority in YOLO mode when a match exists', async () => { + it('should respect rule priority when a wildcard match exists', async () => { const rules: PolicyRule[] = [ { - toolName: 'test-tool', - decision: PolicyDecision.ASK_USER, + toolName: '*', + decision: PolicyDecision.ALLOW, priority: 10, }, { toolName: 'test-tool', decision: PolicyDecision.DENY, priority: 20 }, ]; - engine = new PolicyEngine({ rules, approvalMode: ApprovalMode.YOLO }); + engine = new PolicyEngine({ rules }); - // Priority 20 (DENY) should win over priority 10 (ASK_USER) + // Priority 20 (DENY) should win over priority 10 (ALLOW) const { decision } = await engine.check({ name: 'test-tool' }, undefined); expect(decision).toBe(PolicyDecision.DENY); }); @@ -1638,13 +1643,13 @@ describe('PolicyEngine', () => { }); describe('shell command parsing failure', () => { - it('should return ALLOW in YOLO mode even if shell command parsing fails', async () => { + it('should return ALLOW when using wildcard policy even if shell command parsing fails', async () => { const { splitCommands } = await import('../utils/shell-utils.js'); const rules: PolicyRule[] = [ { + toolName: '*', decision: PolicyDecision.ALLOW, priority: 999, - modes: [ApprovalMode.YOLO], }, { toolName: 'run_shell_command', @@ -1653,10 +1658,7 @@ describe('PolicyEngine', () => { }, ]; - engine = new PolicyEngine({ - rules, - approvalMode: ApprovalMode.YOLO, - }); + engine = new PolicyEngine({ rules }); // Simulate parsing failure (splitCommands returning empty array) vi.mocked(splitCommands).mockReturnValueOnce([]); @@ -1671,7 +1673,7 @@ describe('PolicyEngine', () => { expect(result.rule?.priority).toBe(999); }); - it('should return DENY in YOLO mode if shell command parsing fails and a higher priority rule says DENY', async () => { + it('should return DENY when using wildcard policy if shell command parsing fails and a higher priority rule says DENY', async () => { const { splitCommands } = await import('../utils/shell-utils.js'); const rules: PolicyRule[] = [ { @@ -1680,16 +1682,13 @@ describe('PolicyEngine', () => { priority: 2000, // Very high priority DENY (e.g. Admin) }, { + toolName: '*', decision: PolicyDecision.ALLOW, priority: 999, - modes: [ApprovalMode.YOLO], }, ]; - engine = new PolicyEngine({ - rules, - approvalMode: ApprovalMode.YOLO, - }); + engine = new PolicyEngine({ rules }); // Simulate parsing failure vi.mocked(splitCommands).mockReturnValueOnce([]); @@ -2317,16 +2316,16 @@ describe('PolicyEngine', () => { { decision: PolicyDecision.ALLOW, priority: 999, - modes: [ApprovalMode.YOLO], + modes: [ApprovalMode.AUTO_EDIT], }, { toolName: 'dangerous-tool', decision: PolicyDecision.DENY, priority: 10, - modes: [ApprovalMode.YOLO], + modes: [ApprovalMode.AUTO_EDIT], }, ], - approvalMode: ApprovalMode.YOLO, + approvalMode: ApprovalMode.AUTO_EDIT, allToolNames: ['dangerous-tool', 'safe-tool'], expected: [], }, @@ -2842,25 +2841,25 @@ describe('PolicyEngine', () => { }); }); - describe('YOLO mode with ask_user tool', () => { - it('should return ASK_USER for ask_user tool even in YOLO mode', async () => { + describe('AUTO_EDIT mode with ask_user tool', () => { + it('should return ASK_USER for ask_user tool even in AUTO_EDIT mode', async () => { const rules: PolicyRule[] = [ { toolName: 'ask_user', decision: PolicyDecision.ASK_USER, priority: 999, - modes: [ApprovalMode.YOLO], + modes: [ApprovalMode.AUTO_EDIT], }, { decision: PolicyDecision.ALLOW, - priority: PRIORITY_YOLO_ALLOW_ALL, - modes: [ApprovalMode.YOLO], + priority: 998, + modes: [ApprovalMode.AUTO_EDIT], }, ]; engine = new PolicyEngine({ rules, - approvalMode: ApprovalMode.YOLO, + approvalMode: ApprovalMode.AUTO_EDIT, }); const result = await engine.check( @@ -2870,24 +2869,24 @@ describe('PolicyEngine', () => { expect(result.decision).toBe(PolicyDecision.ASK_USER); }); - it('should return ALLOW for other tools in YOLO mode', async () => { + it('should return ALLOW for other tools in AUTO_EDIT mode', async () => { const rules: PolicyRule[] = [ { toolName: 'ask_user', decision: PolicyDecision.ASK_USER, priority: 999, - modes: [ApprovalMode.YOLO], + modes: [ApprovalMode.AUTO_EDIT], }, { decision: PolicyDecision.ALLOW, - priority: PRIORITY_YOLO_ALLOW_ALL, - modes: [ApprovalMode.YOLO], + priority: 998, + modes: [ApprovalMode.AUTO_EDIT], }, ]; engine = new PolicyEngine({ rules, - approvalMode: ApprovalMode.YOLO, + approvalMode: ApprovalMode.AUTO_EDIT, }); const result = await engine.check( @@ -2985,19 +2984,19 @@ describe('PolicyEngine', () => { toolName: 'enter_plan_mode', decision: PolicyDecision.DENY, priority: 999, - modes: [ApprovalMode.YOLO], + modes: [ApprovalMode.AUTO_EDIT], }, { toolName: 'exit_plan_mode', decision: PolicyDecision.DENY, priority: 999, - modes: [ApprovalMode.YOLO], + modes: [ApprovalMode.AUTO_EDIT], }, ]; engine = new PolicyEngine({ rules, - approvalMode: ApprovalMode.YOLO, + approvalMode: ApprovalMode.AUTO_EDIT, }); const resultEnter = await engine.check( diff --git a/packages/core/src/policy/policy-engine.ts b/packages/core/src/policy/policy-engine.ts index 53bca3f531..2d8aa95a55 100644 --- a/packages/core/src/policy/policy-engine.ts +++ b/packages/core/src/policy/policy-engine.ts @@ -215,8 +215,7 @@ export class PolicyEngine { return ( !allowRedirection && hasRedirection(command) && - this.approvalMode !== ApprovalMode.AUTO_EDIT && - this.approvalMode !== ApprovalMode.YOLO + this.approvalMode !== ApprovalMode.AUTO_EDIT ); } @@ -250,12 +249,8 @@ export class PolicyEngine { return { decision: PolicyDecision.DENY, rule }; } - // In YOLO mode, we should proceed anyway even if we can't parse the command. - if (this.approvalMode === ApprovalMode.YOLO) { - return { - decision: PolicyDecision.ALLOW, - rule, - }; + if (rule?.toolName === '*') { + return { decision: PolicyDecision.ALLOW, rule }; } debugLogger.debug( @@ -492,15 +487,6 @@ export class PolicyEngine { // Default if no rule matched if (decision === undefined) { - if (this.approvalMode === ApprovalMode.YOLO) { - debugLogger.debug( - `[PolicyEngine.check] NO MATCH in YOLO mode - using ALLOW`, - ); - return { - decision: PolicyDecision.ALLOW, - }; - } - debugLogger.debug( `[PolicyEngine.check] NO MATCH - using default decision: ${this.defaultDecision}`, ); diff --git a/packages/core/src/policy/toml-loader.test.ts b/packages/core/src/policy/toml-loader.test.ts index 959f09ba80..1abe9e949c 100644 --- a/packages/core/src/policy/toml-loader.test.ts +++ b/packages/core/src/policy/toml-loader.test.ts @@ -230,21 +230,21 @@ priority = 100 toolName = "glob" decision = "allow" priority = 100 -modes = ["default", "yolo"] +modes = ["default", "autoEdit"] [[rule]] toolName = "grep" decision = "allow" priority = 100 -modes = ["yolo"] +modes = ["autoEdit"] `); // Both rules should be included expect(result.rules).toHaveLength(2); expect(result.rules[0].toolName).toBe('glob'); - expect(result.rules[0].modes).toEqual(['default', 'yolo']); + expect(result.rules[0].modes).toEqual(['default', 'autoEdit']); expect(result.rules[1].toolName).toBe('grep'); - expect(result.rules[1].modes).toEqual(['yolo']); + expect(result.rules[1].modes).toEqual(['autoEdit']); expect(getErrors(result)).toHaveLength(0); }); diff --git a/packages/core/src/policy/types.ts b/packages/core/src/policy/types.ts index 5cd668ef4e..d01d8fc926 100644 --- a/packages/core/src/policy/types.ts +++ b/packages/core/src/policy/types.ts @@ -47,7 +47,6 @@ export function getHookSource(input: Record): HookSource { export enum ApprovalMode { DEFAULT = 'default', AUTO_EDIT = 'autoEdit', - YOLO = 'yolo', PLAN = 'plan', } diff --git a/packages/core/src/prompts/promptProvider.test.ts b/packages/core/src/prompts/promptProvider.test.ts index c2253a9b57..143599d68d 100644 --- a/packages/core/src/prompts/promptProvider.test.ts +++ b/packages/core/src/prompts/promptProvider.test.ts @@ -70,6 +70,7 @@ describe('PromptProvider', () => { }), getApprovedPlanPath: vi.fn().mockReturnValue(undefined), getApprovalMode: vi.fn(), + getAllowedTools: vi.fn().mockReturnValue([]), isTrackerEnabled: vi.fn().mockReturnValue(false), } as unknown as Config; }); diff --git a/packages/core/src/prompts/promptProvider.ts b/packages/core/src/prompts/promptProvider.ts index a2e1333895..9ccd181612 100644 --- a/packages/core/src/prompts/promptProvider.ts +++ b/packages/core/src/prompts/promptProvider.ts @@ -53,7 +53,7 @@ export class PromptProvider { const approvalMode = context.config.getApprovalMode?.() ?? ApprovalMode.DEFAULT; const isPlanMode = approvalMode === ApprovalMode.PLAN; - const isYoloMode = approvalMode === ApprovalMode.YOLO; + const isYoloMode = context.config.getAllowedTools()?.includes('*') ?? false; const skills = context.config.getSkillManager().getSkills(); const toolNames = context.toolRegistry.getAllToolNames(); const enabledToolNames = new Set(toolNames); diff --git a/packages/core/src/services/chatCompressionService.ts b/packages/core/src/services/chatCompressionService.ts index a1f9c12f2c..663284bb32 100644 --- a/packages/core/src/services/chatCompressionService.ts +++ b/packages/core/src/services/chatCompressionService.ts @@ -156,13 +156,11 @@ async function truncateHistoryToBudget( } else if (responseObj && typeof responseObj === 'object') { if ( 'output' in responseObj && - // eslint-disable-next-line no-restricted-syntax typeof responseObj['output'] === 'string' ) { contentStr = responseObj['output']; } else if ( 'content' in responseObj && - // eslint-disable-next-line no-restricted-syntax typeof responseObj['content'] === 'string' ) { contentStr = responseObj['content']; diff --git a/packages/core/src/services/loopDetectionService.ts b/packages/core/src/services/loopDetectionService.ts index 53030911b0..d8b72233be 100644 --- a/packages/core/src/services/loopDetectionService.ts +++ b/packages/core/src/services/loopDetectionService.ts @@ -584,12 +584,10 @@ export class LoopDetectionService { } const flashConfidence = - // eslint-disable-next-line no-restricted-syntax typeof flashResult['unproductive_state_confidence'] === 'number' ? flashResult['unproductive_state_confidence'] : 0; const flashAnalysis = - // eslint-disable-next-line no-restricted-syntax typeof flashResult['unproductive_state_analysis'] === 'string' ? flashResult['unproductive_state_analysis'] : ''; @@ -636,13 +634,11 @@ export class LoopDetectionService { const mainModelConfidence = mainModelResult && - // eslint-disable-next-line no-restricted-syntax typeof mainModelResult['unproductive_state_confidence'] === 'number' ? mainModelResult['unproductive_state_confidence'] : 0; const mainModelAnalysis = mainModelResult && - // eslint-disable-next-line no-restricted-syntax typeof mainModelResult['unproductive_state_analysis'] === 'string' ? mainModelResult['unproductive_state_analysis'] : undefined; @@ -691,7 +687,6 @@ export class LoopDetectionService { if ( result && - // eslint-disable-next-line no-restricted-syntax typeof result['unproductive_state_confidence'] === 'number' ) { return result; diff --git a/packages/core/src/skills/builtin/skill-creator/SKILL.md b/packages/core/src/skills/builtin/skill-creator/SKILL.md index 57996a25cd..b4af4c071c 100644 --- a/packages/core/src/skills/builtin/skill-creator/SKILL.md +++ b/packages/core/src/skills/builtin/skill-creator/SKILL.md @@ -1,6 +1,9 @@ --- name: skill-creator -description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Gemini CLI's capabilities with specialized knowledge, workflows, or tool integrations. +description: + Guide for creating effective skills. This skill should be used when users want + to create a new skill (or update an existing skill) that extends Gemini CLI's + capabilities with specialized knowledge, workflows, or tool integrations. --- # Skill Creator @@ -9,22 +12,33 @@ This skill provides guidance for creating effective skills. ## About Skills -Skills are modular, self-contained packages that extend Gemini CLI's capabilities by providing specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific domains or tasks—they transform Gemini CLI from a general-purpose agent into a specialized agent equipped with procedural knowledge that no model can fully possess. +Skills are modular, self-contained packages that extend Gemini CLI's +capabilities by providing specialized knowledge, workflows, and tools. Think of +them as "onboarding guides" for specific domains or tasks—they transform Gemini +CLI from a general-purpose agent into a specialized agent equipped with +procedural knowledge that no model can fully possess. ### What Skills Provide 1. Specialized workflows - Multi-step procedures for specific domains -2. Tool integrations - Instructions for working with specific file formats or APIs +2. Tool integrations - Instructions for working with specific file formats or + APIs 3. Domain expertise - Company-specific knowledge, schemas, business logic -4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks +4. Bundled resources - Scripts, references, and assets for complex and + repetitive tasks ## Core Principles ### Concise is Key -The context window is a public good. Skills share the context window with everything else Gemini CLI needs: system prompt, conversation history, other Skills' metadata, and the actual user request. +The context window is a public good. Skills share the context window with +everything else Gemini CLI needs: system prompt, conversation history, other +Skills' metadata, and the actual user request. -**Default assumption: Gemini CLI is already very smart.** Only add context Gemini CLI doesn't already have. Challenge each piece of information: "Does Gemini CLI really need this explanation?" and "Does this paragraph justify its token cost?" +**Default assumption: Gemini CLI is already very smart.** Only add context +Gemini CLI doesn't already have. Challenge each piece of information: "Does +Gemini CLI really need this explanation?" and "Does this paragraph justify its +token cost?" Prefer concise examples over verbose explanations. @@ -32,13 +46,19 @@ Prefer concise examples over verbose explanations. Match the level of specificity to the task's fragility and variability: -**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach. +**High freedom (text-based instructions)**: Use when multiple approaches are +valid, decisions depend on context, or heuristics guide the approach. -**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior. +**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred +pattern exists, some variation is acceptable, or configuration affects behavior. -**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed. +**Low freedom (specific scripts, few parameters)**: Use when operations are +fragile and error-prone, consistency is critical, or a specific sequence must be +followed. -Think of Gemini CLI as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom). +Think of Gemini CLI as exploring a path: a narrow bridge with cliffs needs +specific guardrails (low freedom), while an open field allows many routes (high +freedom). ### Anatomy of a Skill @@ -61,45 +81,75 @@ skill-name/ Every SKILL.md consists of: -- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Gemini CLI reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used. -- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all). +- **Frontmatter** (YAML): Contains `name` and `description` fields. These are + the only fields that Gemini CLI reads to determine when the skill gets used, + thus it is very important to be clear and comprehensive in describing what the + skill is, and when it should be used. +- **Body** (Markdown): Instructions and guidance for using the skill. Only + loaded AFTER the skill triggers (if at all). #### Bundled Resources (optional) ##### Scripts (`scripts/`) -Executable code (Node.js/Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten. +Executable code (Node.js/Python/Bash/etc.) for tasks that require deterministic +reliability or are repeatedly rewritten. -- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed +- **When to include**: When the same code is being rewritten repeatedly or + deterministic reliability is needed - **Example**: `scripts/rotate_pdf.cjs` for PDF rotation tasks -- **Benefits**: Token efficient, deterministic, may be executed without loading into context -- **Agentic Ergonomics**: Scripts must output LLM-friendly stdout. Suppress standard tracebacks. Output clear, concise success/failure messages, and paginate or truncate outputs (e.g., "Success: First 50 lines of processed file...") to prevent context window overflow. -- **Note**: Scripts may still need to be read by Gemini CLI for patching or environment-specific adjustments +- **Benefits**: Token efficient, deterministic, may be executed without loading + into context +- **Agentic Ergonomics**: Scripts must output LLM-friendly stdout. Suppress + standard tracebacks. Output clear, concise success/failure messages, and + paginate or truncate outputs (e.g., "Success: First 50 lines of processed + file...") to prevent context window overflow. +- **Note**: Scripts may still need to be read by Gemini CLI for patching or + environment-specific adjustments ##### References (`references/`) -Documentation and reference material intended to be loaded as needed into context to inform Gemini CLI's process and thinking. +Documentation and reference material intended to be loaded as needed into +context to inform Gemini CLI's process and thinking. -- **When to include**: For documentation that Gemini CLI should reference while working -- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications -- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides -- **Benefits**: Keeps SKILL.md lean, loaded only when Gemini CLI determines it's needed -- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md +- **When to include**: For documentation that Gemini CLI should reference while + working +- **Examples**: `references/finance.md` for financial schemas, + `references/mnda.md` for company NDA template, `references/policies.md` for + company policies, `references/api_docs.md` for API specifications +- **Use cases**: Database schemas, API documentation, domain knowledge, company + policies, detailed workflow guides +- **Benefits**: Keeps SKILL.md lean, loaded only when Gemini CLI determines it's + needed +- **Best practice**: If files are large (>10k words), include grep search + patterns in SKILL.md - **Avoid duplication**: Information should live in either SKILL.md or - references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files. + references files, not both. Prefer references files for detailed information + unless it's truly core to the skill—this keeps SKILL.md lean while making + information discoverable without hogging the context window. Keep only + essential procedural instructions and workflow guidance in SKILL.md; move + detailed reference material, schemas, and examples to references files. ##### Assets (`assets/`) -Files not intended to be loaded into context, but rather used within the output Gemini CLI produces. +Files not intended to be loaded into context, but rather used within the output +Gemini CLI produces. -- **When to include**: When the skill needs files that will be used in the final output -- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography -- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified -- **Benefits**: Separates output resources from documentation, enables Gemini CLI to use files without loading them into context +- **When to include**: When the skill needs files that will be used in the final + output +- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for + PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, + `assets/font.ttf` for typography +- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample + documents that get copied or modified +- **Benefits**: Separates output resources from documentation, enables Gemini + CLI to use files without loading them into context #### What to Not Include in a Skill -A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including: +A skill should only contain essential files that directly support its +functionality. Do NOT create extraneous documentation or auxiliary files, +including: - README.md - INSTALLATION_GUIDE.md @@ -107,7 +157,10 @@ A skill should only contain essential files that directly support its functional - CHANGELOG.md - etc. -The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxiliary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion. +The skill should only contain the information needed for an AI agent to do the +job at hand. It should not contain auxiliary context about the process that went +into creating it, setup and testing procedures, user-facing documentation, etc. +Creating additional documentation files just adds clutter and confusion. ### Progressive Disclosure Design Principle @@ -115,13 +168,21 @@ Skills use a three-level loading system to manage context efficiently: 1. **Metadata (name + description)** - Always in context (~100 words) 2. **SKILL.md body** - When skill triggers (<5k words) -3. **Bundled resources** - As needed by Gemini CLI (Unlimited because scripts can be executed without reading into context window) +3. **Bundled resources** - As needed by Gemini CLI (Unlimited because scripts + can be executed without reading into context window) #### Progressive Disclosure Patterns -Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them. +Keep SKILL.md body to the essentials and under 500 lines to minimize context +bloat. Split content into separate files when approaching this limit. When +splitting out content into other files, it is very important to reference them +from SKILL.md and describe clearly when to read them, to ensure the reader of +the skill knows they exist and when to use them. -**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files. +**Key principle:** When a skill supports multiple variations, frameworks, or +options, keep only the core workflow and selection guidance in SKILL.md. Move +variant-specific details (patterns, examples, configuration) into separate +reference files. **Pattern 1: High-level guide with references** @@ -143,7 +204,8 @@ Gemini CLI loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed. **Pattern 2: Domain-specific organization** -For Skills with multiple domains, organize content by domain to avoid loading irrelevant context: +For Skills with multiple domains, organize content by domain to avoid loading +irrelevant context: ``` bigquery-skill/ @@ -157,7 +219,8 @@ bigquery-skill/ When a user asks about sales metrics, Gemini CLI only reads sales.md. -Similarly, for skills supporting multiple frameworks or variants, organize by variant: +Similarly, for skills supporting multiple frameworks or variants, organize by +variant: ``` cloud-deploy/ @@ -183,15 +246,20 @@ Use pandas for loading and basic queries. See [PANDAS.md](PANDAS.md). ## Advanced Operations -For massive files that exceed memory, see [STREAMING.md](STREAMING.md). For timestamp normalization, see [TIMESTAMPS.md](TIMESTAMPS.md). +For massive files that exceed memory, see [STREAMING.md](STREAMING.md). For +timestamp normalization, see [TIMESTAMPS.md](TIMESTAMPS.md). -Gemini CLI reads REDLINING.md or OOXML.md only when the user needs those features. +Gemini CLI reads REDLINING.md or OOXML.md only when the user needs those +features. ``` **Important guidelines:** -- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md. -- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Gemini CLI can see the full scope when previewing. +- **Avoid deeply nested references** - Keep references one level deep from + SKILL.md. All reference files should link directly from SKILL.md. +- **Structure longer reference files** - For files longer than 100 lines, + include a table of contents at the top so Gemini CLI can see the full scope + when previewing. ## Skill Creation Process @@ -205,66 +273,93 @@ Skill creation involves these steps: 6. Install and reload the skill 7. Iterate based on real usage -Follow these steps in order, skipping only if there is a clear reason why they are not applicable. +Follow these steps in order, skipping only if there is a clear reason why they +are not applicable. ### Skill Naming -- Use lowercase letters, digits, and hyphens only; normalize user-provided titles to hyphen-case (e.g., "Plan Mode" -> `plan-mode`). -- When generating names, generate a name under 64 characters (letters, digits, hyphens). +- Use lowercase letters, digits, and hyphens only; normalize user-provided + titles to hyphen-case (e.g., "Plan Mode" -> `plan-mode`). +- When generating names, generate a name under 64 characters (letters, digits, + hyphens). - Prefer short, verb-led phrases that describe the action. -- Namespace by tool when it improves clarity or triggering (e.g., `gh-address-comments`, `linear-address-issue`). +- Namespace by tool when it improves clarity or triggering (e.g., + `gh-address-comments`, `linear-address-issue`). - Name the skill folder exactly after the skill name. ### Step 1: Understanding the Skill with Concrete Examples -Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill. +Skip this step only when the skill's usage patterns are already clearly +understood. It remains valuable even when working with an existing skill. -To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback. +To create an effective skill, clearly understand concrete examples of how the +skill will be used. This understanding can come from either direct user examples +or generated examples that are validated with user feedback. For example, when building an image-editor skill, relevant questions include: -- "What functionality should the image-editor skill support? Editing, rotating, anything else?" +- "What functionality should the image-editor skill support? Editing, rotating, + anything else?" - "Can you give some examples of how this skill would be used?" -- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?" +- "I can imagine users asking for things like 'Remove the red-eye from this + image' or 'Rotate this image'. Are there other ways you imagine this skill + being used?" - "What would a user say that should trigger this skill?" -**Avoid interrogation loops:** Do not ask more than one or two clarifying questions at a time. Bias toward action: propose a concrete list of features or examples based on your initial understanding, and ask the user to refine them. +**Avoid interrogation loops:** Do not ask more than one or two clarifying +questions at a time. Bias toward action: propose a concrete list of features or +examples based on your initial understanding, and ask the user to refine them. -Conclude this step when there is a clear sense of the functionality the skill should support. +Conclude this step when there is a clear sense of the functionality the skill +should support. ### Step 2: Planning the Reusable Skill Contents To turn concrete examples into an effective skill, analyze each example by: 1. Considering how to execute on the example from scratch -2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly +2. Identifying what scripts, references, and assets would be helpful when + executing these workflows repeatedly -Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows: +Example: When building a `pdf-editor` skill to handle queries like "Help me +rotate this PDF," the analysis shows: 1. Rotating a PDF requires re-writing the same code each time 2. A `scripts/rotate_pdf.cjs` script would be helpful to store in the skill -Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows: +Example: When designing a `frontend-webapp-builder` skill for queries like +"Build me a todo app" or "Build me a dashboard to track my steps," the analysis +shows: 1. Writing a frontend webapp requires the same boilerplate HTML/React each time -2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill +2. An `assets/hello-world/` template containing the boilerplate HTML/React + project files would be helpful to store in the skill -Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows: +Example: When building a `big-query` skill to handle queries like "How many +users have logged in today?" the analysis shows: -1. Querying BigQuery requires re-discovering the table schemas and relationships each time -2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill +1. Querying BigQuery requires re-discovering the table schemas and relationships + each time +2. A `references/schema.md` file documenting the table schemas would be helpful + to store in the skill -To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets. +To establish the skill's contents, analyze each concrete example to create a +list of the reusable resources to include: scripts, references, and assets. ### Step 3: Initializing the Skill At this point, it is time to actually create the skill. -Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step. +Skip this step only if the skill being developed already exists, and iteration +or packaging is needed. In this case, continue to the next step. -When creating a new skill from scratch, always run the `init_skill.cjs` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable. +When creating a new skill from scratch, always run the `init_skill.cjs` script. +The script conveniently generates a new template skill directory that +automatically includes everything a skill requires, making the skill creation +process much more efficient and reliable. -**Note:** Use the absolute path to the script as provided in the `available_resources` section. +**Note:** Use the absolute path to the script as provided in the +`available_resources` section. Usage: @@ -277,30 +372,48 @@ The script: - Creates the skill directory at the specified path - Generates a SKILL.md template with proper frontmatter and TODO placeholders - Creates example resource directories: `scripts/`, `references/`, and `assets/` -- Adds example files (`scripts/example_script.cjs`, `references/example_reference.md`, `assets/example_asset.txt`) that can be customized or deleted +- Adds example files (`scripts/example_script.cjs`, + `references/example_reference.md`, `assets/example_asset.txt`) that can be + customized or deleted -After initialization, customize or remove the generated SKILL.md and example files as needed. +After initialization, customize or remove the generated SKILL.md and example +files as needed. ### Step 4: Edit the Skill -When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Gemini CLI to use. Include information that would be beneficial and non-obvious to Gemini CLI. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Gemini CLI instance execute these tasks more effectively. +When editing the (newly-generated or existing) skill, remember that the skill is +being created for another instance of Gemini CLI to use. Include information +that would be beneficial and non-obvious to Gemini CLI. Consider what procedural +knowledge, domain-specific details, or reusable assets would help another Gemini +CLI instance execute these tasks more effectively. #### Learn Proven Design Patterns Consult these helpful guides based on your skill's needs: -- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic -- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns +- **Multi-step processes**: See references/workflows.md for sequential workflows + and conditional logic +- **Specific output formats or quality standards**: See + references/output-patterns.md for template and example patterns These files contain established best practices for effective skill design. #### Start with Reusable Skill Contents -To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`. +To begin implementation, start with the reusable resources identified above: +`scripts/`, `references/`, and `assets/` files. Note that this step may require +user input. For example, when implementing a `brand-guidelines` skill, the user +may need to provide brand assets or templates to store in `assets/`, or +documentation to store in `references/`. -Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion. +Added scripts must be tested by actually running them to ensure there are no +bugs and that the output matches what is expected. If there are many similar +scripts, only a representative sample needs to be tested to ensure confidence +that they all work while balancing time to completion. -Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them. +Any example files and directories not needed for the skill should be deleted. +The initialization script creates example files in `scripts/`, `references/`, +and `assets/` to demonstrate structure, but most skills won't need all of them. #### Update SKILL.md @@ -311,11 +424,17 @@ Any example files and directories not needed for the skill should be deleted. Th Write the YAML frontmatter with `name` and `description`: - `name`: The skill name -- `description`: This is the primary triggering mechanism for your skill, and helps Gemini CLI understand when to use the skill. - - Include both what the Skill does and specific triggers/contexts for when to use it. - - **Must be a single-line string** (e.g., `description: Data ingestion...`). Quotes are optional. - - Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Gemini CLI. - - Example: `description: Data ingestion, cleaning, and transformation for tabular data. Use when Gemini CLI needs to work with CSV/TSV files to analyze large datasets, normalize schemas, or merge sources.` +- `description`: This is the primary triggering mechanism for your skill, and + helps Gemini CLI understand when to use the skill. + - Include both what the Skill does and specific triggers/contexts for when to + use it. + - **Must be a single-line string** (e.g., `description: Data ingestion...`). + Quotes are optional. + - Include all "when to use" information here - Not in the body. The body is + only loaded after triggering, so "When to Use This Skill" sections in the + body are not helpful to Gemini CLI. + - Example: + `description: Data ingestion, cleaning, and transformation for tabular data. Use when Gemini CLI needs to work with CSV/TSV files to analyze large datasets, normalize schemas, or merge sources.` Do not include any other fields in YAML frontmatter. @@ -325,9 +444,13 @@ Write instructions for using the skill and its bundled resources. ### Step 5: Packaging a Skill -Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first (checking YAML and ensuring no TODOs remain) to ensure it meets all requirements: +Once development of the skill is complete, it must be packaged into a +distributable .skill file that gets shared with the user. The packaging process +automatically validates the skill first (checking YAML and ensuring no TODOs +remain) to ensure it meets all requirements: -**Note:** Use the absolute path to the script as provided in the `available_resources` section. +**Note:** Use the absolute path to the script as provided in the +`available_resources` section. ```bash node /scripts/package_skill.cjs @@ -347,15 +470,22 @@ The packaging script will: - Description completeness and quality - File organization and resource references -2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension. +2. **Package** the skill if validation passes, creating a .skill file named + after the skill (e.g., `my-skill.skill`) that includes all files and + maintains the proper directory structure for distribution. The .skill file is + a zip file with a .skill extension. -If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again. +If validation fails, the script will report the errors and exit without creating +a package. Fix any validation errors and run the packaging command again. ### Step 6: Installing and Reloading a Skill -Once the skill is packaged into a `.skill` file, offer to install it for the user. Ask whether they would like to install it locally in the current folder (workspace scope) or at the user level (user scope). +Once the skill is packaged into a `.skill` file, offer to install it for the +user. Ask whether they would like to install it locally in the current folder +(workspace scope) or at the user level (user scope). -If the user agrees to an installation, perform it immediately using the `run_shell_command` tool: +If the user agrees to an installation, perform it immediately using the +`run_shell_command` tool: - **Locally (workspace scope)**: ```bash @@ -366,13 +496,19 @@ If the user agrees to an installation, perform it immediately using the `run_she gemini skills install --scope user ``` -**Important:** After the installation is complete, notify the user that they MUST manually execute the `/skills reload` command in their interactive Gemini CLI session to enable the new skill. They can then verify the installation by running `/skills list`. +**Important:** After the installation is complete, notify the user that they +MUST manually execute the `/skills reload` command in their interactive Gemini +CLI session to enable the new skill. They can then verify the installation by +running `/skills list`. -Note: You (the agent) cannot execute the `/skills reload` command yourself; it must be done by the user in an interactive instance of Gemini CLI. Do not attempt to run it on their behalf. +Note: You (the agent) cannot execute the `/skills reload` command yourself; it +must be done by the user in an interactive instance of Gemini CLI. Do not +attempt to run it on their behalf. ### Step 7: Iterate -After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed. +After testing the skill, users may request improvements. Often this happens +right after using the skill, with fresh context of how the skill performed. **Iteration workflow:** diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 2f059030ca..1aac34c83b 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -21,7 +21,6 @@ import type { RewindEvent, MalformedJsonResponseEvent, IdeConnectionEvent, - ConversationFinishedEvent, ChatCompressionEvent, FileOperationEvent, InvalidChunkEvent, @@ -1141,28 +1140,6 @@ export class ClearcutLogger { }); } - logConversationFinishedEvent(event: ConversationFinishedEvent): void { - const data: EventValue[] = [ - { - gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID, - value: this.config?.getSessionId() ?? '', - }, - { - gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONVERSATION_TURN_COUNT, - value: JSON.stringify(event.turnCount), - }, - { - gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE, - value: event.approvalMode, - }, - ]; - - this.enqueueLogEvent( - this.createLogEvent(EventNames.CONVERSATION_FINISHED, data), - ); - this.flushIfNeeded(); - } - logEndSessionEvent(): void { // Flush immediately on session end. this.enqueueLogEvent(this.createLogEvent(EventNames.END_SESSION, [])); diff --git a/packages/core/src/telemetry/index.ts b/packages/core/src/telemetry/index.ts index 0d264695d8..7fcf549433 100644 --- a/packages/core/src/telemetry/index.ts +++ b/packages/core/src/telemetry/index.ts @@ -38,7 +38,6 @@ export { logApiResponse, logFlashFallback, logSlashCommand, - logConversationFinishedEvent, logChatCompression, logToolOutputTruncated, logExtensionEnable, @@ -64,7 +63,6 @@ export { FlashFallbackEvent, StartSessionEvent, ToolCallEvent, - ConversationFinishedEvent, ToolOutputTruncatedEvent, WebFetchFallbackAttemptEvent, NetworkRetryAttemptEvent, diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index d5cc605e65..578f0eee19 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -26,7 +26,6 @@ import { type LoopDetectionDisabledEvent, type SlashCommandEvent, type RewindEvent, - type ConversationFinishedEvent, type ChatCompressionEvent, type MalformedJsonResponseEvent, type InvalidChunkEvent, @@ -428,21 +427,6 @@ export function logIdeConnection( }); } -export function logConversationFinishedEvent( - config: Config, - event: ConversationFinishedEvent, -): void { - ClearcutLogger.getInstance(config)?.logConversationFinishedEvent(event); - bufferTelemetryEvent(() => { - const logger = logs.getLogger(SERVICE_NAME); - const logRecord: LogRecord = { - body: event.toLogBody(), - attributes: event.toOpenTelemetryAttributes(config), - }; - logger.emit(logRecord); - }); -} - export function logChatCompression( config: Config, event: ChatCompressionEvent, diff --git a/packages/core/src/telemetry/semantic.ts b/packages/core/src/telemetry/semantic.ts index 6dae06d381..cb38502c91 100644 --- a/packages/core/src/telemetry/semantic.ts +++ b/packages/core/src/telemetry/semantic.ts @@ -63,7 +63,6 @@ function getStringReferences(parts: AnyPart[]): StringReference[] { }); } } else if (part instanceof GenericPart) { - // eslint-disable-next-line no-restricted-syntax if (part.type === 'executableCode' && typeof part['code'] === 'string') { refs.push({ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion @@ -74,7 +73,6 @@ function getStringReferences(parts: AnyPart[]): StringReference[] { }); } else if ( part.type === 'codeExecutionResult' && - // eslint-disable-next-line no-restricted-syntax typeof part['output'] === 'string' ) { refs.push({ diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 0ee6e63503..dace2b4bab 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -1181,35 +1181,6 @@ export class IdeConnectionEvent { } } -export const EVENT_CONVERSATION_FINISHED = 'gemini_cli.conversation_finished'; -export class ConversationFinishedEvent { - 'event_name': 'conversation_finished'; - 'event.timestamp': string; // ISO 8601; - approvalMode: ApprovalMode; - turnCount: number; - - constructor(approvalMode: ApprovalMode, turnCount: number) { - this['event_name'] = 'conversation_finished'; - this['event.timestamp'] = new Date().toISOString(); - this.approvalMode = approvalMode; - this.turnCount = turnCount; - } - - toOpenTelemetryAttributes(config: Config): LogAttributes { - return { - ...getCommonAttributes(config), - 'event.name': EVENT_CONVERSATION_FINISHED, - 'event.timestamp': this['event.timestamp'], - approvalMode: this.approvalMode, - turnCount: this.turnCount, - }; - } - - toLogBody(): string { - return `Conversation finished.`; - } -} - export const EVENT_FILE_OPERATION = 'gemini_cli.file_operation'; export class FileOperationEvent implements BaseTelemetryEvent { 'event.name': 'file_operation'; @@ -1843,7 +1814,6 @@ export type TelemetryEvent = | NextSpeakerCheckEvent | MalformedJsonResponseEvent | IdeConnectionEvent - | ConversationFinishedEvent | SlashCommandEvent | FileOperationEvent | InvalidChunkEvent diff --git a/packages/core/src/test-utils/mock-message-bus.ts b/packages/core/src/test-utils/mock-message-bus.ts index 05ed8cb32d..659081e152 100644 --- a/packages/core/src/test-utils/mock-message-bus.ts +++ b/packages/core/src/test-utils/mock-message-bus.ts @@ -62,7 +62,7 @@ export class MockMessageBus { if (!this.subscriptions.has(type)) { this.subscriptions.set(type, new Set()); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + this.subscriptions.get(type)!.add(listener as (message: Message) => void); }, ); @@ -74,7 +74,6 @@ export class MockMessageBus { (type: T['type'], listener: (message: T) => void) => { const listeners = this.subscriptions.get(type); if (listeners) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion listeners.delete(listener as (message: Message) => void); } }, @@ -103,7 +102,6 @@ export class MockMessageBus { * Create a mock MessageBus for testing */ export function createMockMessageBus(): MessageBus { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return new MockMessageBus() as unknown as MessageBus; } @@ -113,6 +111,5 @@ export function createMockMessageBus(): MessageBus { export function getMockMessageBusInstance( messageBus: MessageBus, ): MockMessageBus { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return messageBus as unknown as MockMessageBus; } diff --git a/packages/core/src/test-utils/mockWorkspaceContext.ts b/packages/core/src/test-utils/mockWorkspaceContext.ts index 640b51f616..67c614e9f5 100644 --- a/packages/core/src/test-utils/mockWorkspaceContext.ts +++ b/packages/core/src/test-utils/mockWorkspaceContext.ts @@ -19,7 +19,6 @@ export function createMockWorkspaceContext( ): WorkspaceContext { const allDirs = [rootDir, ...additionalDirs]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const mockWorkspaceContext = { addDirectory: vi.fn(), getDirectories: vi.fn().mockReturnValue(allDirs), diff --git a/packages/core/src/tools/exit-plan-mode.test.ts b/packages/core/src/tools/exit-plan-mode.test.ts index 88e327ab34..bc57cf9a89 100644 --- a/packages/core/src/tools/exit-plan-mode.test.ts +++ b/packages/core/src/tools/exit-plan-mode.test.ts @@ -361,7 +361,7 @@ Ask the user for specific feedback on how to improve the plan.`, }); describe('getAllowApprovalMode (internal)', () => { - it('should return YOLO when config.isInteractive() is false', async () => { + it('should return AUTO_EDIT when config.isInteractive() is false', async () => { mockConfig.isInteractive = vi.fn().mockReturnValue(false); const planRelativePath = createPlanFile('test.md', '# Content'); const invocation = tool.build({ plan_path: planRelativePath }); @@ -369,9 +369,9 @@ Ask the user for specific feedback on how to improve the plan.`, // Directly call execute to trigger the internal getAllowApprovalMode const result = await invocation.execute(new AbortController().signal); - expect(result.llmContent).toContain('YOLO mode'); + expect(result.llmContent).toContain('Auto-Edit mode'); expect(mockConfig.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.YOLO, + ApprovalMode.AUTO_EDIT, ); }); @@ -418,10 +418,6 @@ Ask the user for specific feedback on how to improve the plan.`, ApprovalMode.DEFAULT, 'Default mode (edits will require confirmation)', ); - await testMode( - ApprovalMode.YOLO, - 'YOLO mode (all tool calls auto-approved)', - ); }); it('should throw for invalid post-planning modes', async () => { diff --git a/packages/core/src/tools/exit-plan-mode.ts b/packages/core/src/tools/exit-plan-mode.ts index aad95492c2..d5ad93dae1 100644 --- a/packages/core/src/tools/exit-plan-mode.ts +++ b/packages/core/src/tools/exit-plan-mode.ts @@ -255,12 +255,12 @@ Ask the user for specific feedback on how to improve the plan.`, /** * Determines the approval mode to switch to when plan mode is exited via a policy ALLOW. - * In non-interactive environments, this defaults to YOLO to allow automated execution. + * In non-interactive environments, this defaults to AUTO_EDIT to allow automated execution. */ private getAllowApprovalMode(): ApprovalMode { if (!this.config.isInteractive()) { - // For non-interactive environment requires minimal user action, exit as YOLO mode for plan implementation. - return ApprovalMode.YOLO; + // For non-interactive environment requires minimal user action, exit as AUTO_EDIT mode for plan implementation. + return ApprovalMode.AUTO_EDIT; } // By default, YOLO mode in interactive environment cannot enter/exit plan mode. // Always exit plan mode and move to default approval mode if exit_plan_mode tool is configured with allow decision. diff --git a/packages/core/src/tools/mcp-tool.ts b/packages/core/src/tools/mcp-tool.ts index 195a78ec61..4f7be451b2 100644 --- a/packages/core/src/tools/mcp-tool.ts +++ b/packages/core/src/tools/mcp-tool.ts @@ -108,7 +108,7 @@ export function isMcpToolAnnotation( return ( typeof annotation === 'object' && annotation !== null && - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, no-restricted-syntax + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion typeof (annotation as Record)['_serverName'] === 'string' ); } diff --git a/packages/core/src/tools/trackerTools.test.ts b/packages/core/src/tools/trackerTools.test.ts index 6513a71dd5..bf6248bbea 100644 --- a/packages/core/src/tools/trackerTools.test.ts +++ b/packages/core/src/tools/trackerTools.test.ts @@ -37,6 +37,7 @@ describe('Tracker Tools Integration', () => { model: 'gemini-3-flash', debugMode: false, }); + await config.storage.initialize(); messageBus = new MessageBus(null as unknown as PolicyEngine, false); }); diff --git a/packages/core/src/utils/approvalModeUtils.test.ts b/packages/core/src/utils/approvalModeUtils.test.ts index 6cf36bf858..8c2b015ebe 100644 --- a/packages/core/src/utils/approvalModeUtils.test.ts +++ b/packages/core/src/utils/approvalModeUtils.test.ts @@ -30,12 +30,6 @@ describe('approvalModeUtils', () => { 'Plan mode (read-only planning)', ); }); - - it('should return correct description for YOLO mode', () => { - expect(getApprovalModeDescription(ApprovalMode.YOLO)).toBe( - 'YOLO mode (all tool calls auto-approved)', - ); - }); }); describe('getPlanModeExitMessage', () => { @@ -50,11 +44,5 @@ describe('approvalModeUtils', () => { 'User has manually exited Plan Mode. Switching to Auto-Edit mode (edits will be applied automatically).', ); }); - - it('should default to non-manual message', () => { - expect(getPlanModeExitMessage(ApprovalMode.YOLO)).toBe( - 'Plan approved. Switching to YOLO mode (all tool calls auto-approved).', - ); - }); }); }); diff --git a/packages/core/src/utils/approvalModeUtils.ts b/packages/core/src/utils/approvalModeUtils.ts index bb855d2303..89b1bfd0cb 100644 --- a/packages/core/src/utils/approvalModeUtils.ts +++ b/packages/core/src/utils/approvalModeUtils.ts @@ -18,8 +18,7 @@ export function getApprovalModeDescription(mode: ApprovalMode): string { return 'Default mode (edits will require confirmation)'; case ApprovalMode.PLAN: return 'Plan mode (read-only planning)'; - case ApprovalMode.YOLO: - return 'YOLO mode (all tool calls auto-approved)'; + default: return checkExhaustive(mode); } diff --git a/packages/core/src/utils/editCorrector.ts b/packages/core/src/utils/editCorrector.ts index 2c58bad98f..f8ff81b97e 100644 --- a/packages/core/src/utils/editCorrector.ts +++ b/packages/core/src/utils/editCorrector.ts @@ -112,7 +112,6 @@ Return ONLY the corrected string in the specified JSON format with the key 'corr if ( result && - // eslint-disable-next-line no-restricted-syntax typeof result['corrected_string_escaping'] === 'string' && result['corrected_string_escaping'].length > 0 ) { diff --git a/packages/core/src/utils/googleErrors.ts b/packages/core/src/utils/googleErrors.ts index 4439d55de5..994c17b697 100644 --- a/packages/core/src/utils/googleErrors.ts +++ b/packages/core/src/utils/googleErrors.ts @@ -231,7 +231,7 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null { } // Basic structural check before casting. // Since the proto definitions are loose, we primarily rely on @type presence. - // eslint-disable-next-line no-restricted-syntax + if (typeof detailObj['@type'] === 'string') { // We can just cast it; the consumer will have to switch on @type // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion diff --git a/packages/core/src/utils/oauth-flow.ts b/packages/core/src/utils/oauth-flow.ts index e13fd37837..f3c19089e2 100644 --- a/packages/core/src/utils/oauth-flow.ts +++ b/packages/core/src/utils/oauth-flow.ts @@ -361,24 +361,20 @@ async function parseTokenEndpointResponse( data && typeof data === 'object' && 'access_token' in data && - // eslint-disable-next-line no-restricted-syntax typeof (data as Record)['access_token'] === 'string' ) { const obj = data as Record; const result: OAuthTokenResponse = { access_token: String(obj['access_token']), token_type: - // eslint-disable-next-line no-restricted-syntax typeof obj['token_type'] === 'string' ? obj['token_type'] : 'Bearer', expires_in: - // eslint-disable-next-line no-restricted-syntax typeof obj['expires_in'] === 'number' ? obj['expires_in'] : undefined, refresh_token: - // eslint-disable-next-line no-restricted-syntax typeof obj['refresh_token'] === 'string' ? obj['refresh_token'] : undefined, - // eslint-disable-next-line no-restricted-syntax + scope: typeof obj['scope'] === 'string' ? obj['scope'] : undefined, }; return result;