diff --git a/GEMINI.md b/GEMINI.md index c08e486b22..f47b9f08df 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,89 +1,55 @@ -# Gemini CLI Project Context +# Workspace: Base Folder Strategy & Custom Skills -Gemini CLI is an open-source AI agent that brings the power of Gemini directly -into the terminal. It is designed to be a terminal-first, extensible, and -powerful tool for developers. +This branch introduces the **\_ux_git-worktree** and **\_ux_finish-pr** skills +and enforces the **Base Folder Strategy** for repository management. It is +maintained by the AI DevTools UX team. -## Project Overview +## 🚨 Directive for AI Agents (Read First) -- **Purpose:** Provide a seamless terminal interface for Gemini models, - supporting code understanding, generation, automation, and integration via MCP - (Model Context Protocol). -- **Main Technologies:** - - **Runtime:** Node.js (>=20.0.0, recommended ~20.19.0 for development) - - **Language:** TypeScript - - **UI Framework:** React (using [Ink](https://github.com/vadimdemedes/ink) - for CLI rendering) - - **Testing:** Vitest - - **Bundling:** esbuild - - **Linting/Formatting:** ESLint, Prettier -- **Architecture:** Monorepo structure using npm workspaces. - - `packages/cli`: User-facing terminal UI, input processing, and display - rendering. - - `packages/core`: Backend logic, Gemini API orchestration, prompt - construction, and tool execution. - - `packages/a2a-server`: Experimental Agent-to-Agent server. - - `packages/sdk`: Programmatic SDK for embedding Gemini CLI capabilities. - - `packages/devtools`: Integrated developer tools (Network/Console inspector). - - `packages/test-utils`: Shared test utilities and test rig. - - `packages/vscode-ide-companion`: VS Code extension pairing with the CLI. +**If you are an AI agent operating in this branch, your first task is to ensure +the custom skills are active.** -## Building and Running +1. Check if `~/.gemini/skills/_ux_git-worktree/` and + `~/.gemini/skills/_ux_finish-pr/` exist. +2. If any are missing, immediately execute `./scripts/sync-skills.sh`. +3. Prompt the user to run `/skills reload` if you are in an interactive session. -- **Install Dependencies:** `npm install` -- **Build All:** `npm run build:all` (Builds packages, sandbox, and VS Code - companion) -- **Build Packages:** `npm run build` -- **Run in Development:** `npm run start` -- **Run in Debug Mode:** `npm run debug` (Enables Node.js inspector) -- **Bundle Project:** `npm run bundle` -- **Clean Artifacts:** `npm run clean` +## 🤝 Team Contributions -## Testing and Quality +This is a **Draft PR branch**. Your team is encouraged to: -- **Test Commands:** - - **Unit (All):** `npm run test` - - **Integration (E2E):** `npm run test:e2e` - - **Workspace-Specific:** `npm test -w -- ` (Note: `` must - be relative to the workspace root, e.g., - `-w @google/gemini-cli-core -- src/routing/modelRouterService.test.ts`) -- **Full Validation:** `npm run preflight` (Heaviest check; runs clean, install, - build, lint, type check, and tests. Recommended before submitting PRs. Due to - its long runtime, only run this at the very end of a code implementation task. - If it fails, use faster, targeted commands (e.g., `npm run test`, - `npm run lint`, or workspace-specific tests) to iterate on fixes before - re-running `preflight`. For simple, non-code changes like documentation or - prompting updates, skip `preflight` at the end of the task and wait for PR - validation.) -- **Individual Checks:** `npm run lint` / `npm run format` / `npm run typecheck` +- Refine the `_ux_git-worktree` skill instructions in + `packages/core/src/skills/builtin/_ux_git-worktree/SKILL.md`. +- Refine the `_ux_finish-pr` skill instructions in + `packages/core/src/skills/builtin/_ux_finish-pr/SKILL.md`. +- Improve the automation in `scripts/worktree-manager.sh`. +- All changes should be committed directly to this branch + (`feature/gemini-cli-ux-team-skills`). -## Development Conventions +## 1. Architectural Strategy -- **Contributions:** Follow the process outlined in `CONTRIBUTING.md`. Requires - signing the Google CLA. -- **Pull Requests:** Keep PRs small, focused, and linked to an existing issue. - Always activate the `pr-creator` skill for PR generation, even when using the - `gh` CLI. -- **Commit Messages:** Follow the - [Conventional Commits](https://www.conventionalcommits.org/) standard. -- **Imports:** Use specific imports and avoid restricted relative imports - between packages (enforced by ESLint). -- **License Headers:** For all new source code files (`.ts`, `.tsx`, `.js`), - include the Apache-2.0 license header with the current year. (e.g., - `Copyright 2026 Google LLC`). This is enforced by ESLint. +Functional work happens in sibling directories (e.g., `main/`, `feature-name/`). +The root directory acts as a container. -## Testing Conventions +## 2. Setting Up Custom Skills -- **Environment Variables:** When testing code that depends on environment - variables, use `vi.stubEnv('NAME', 'value')` in `beforeEach` and - `vi.unstubAllEnvs()` in `afterEach`. Avoid modifying `process.env` directly as - it can lead to test leakage and is less reliable. To "unset" a variable, use - an empty string `vi.stubEnv('NAME', '')`. +To ensure this agent has the necessary procedural knowledge to manage worktrees +and PR reviews on your behalf, you must sync the custom skills to your local +user directory. -## Documentation +### Sync Script -- Always use the `docs-writer` skill when you are asked to write, edit, or - review any documentation. -- Documentation is located in the `docs/` directory. -- Suggest documentation updates when code changes render existing documentation - obsolete or incomplete. +Run the following from the root of this worktree: + +```bash +./scripts/sync-skills.sh +``` + +## 3. Mandatory Workflow + +- **ALWAYS** use the `_ux_git-worktree` skill for branch management. +- **ALWAYS** use the `_ux_finish-pr` skill for pull request maintenance. +- Never use standard `git checkout -b`. +- Use `worktree-manager.sh pr ` for semantic PR checkouts. +- When operating in a worktree, ensure the primary `main/.git` path is included + in your trusted directories to bypass macOS sandbox restrictions. diff --git a/README.md b/README.md index 93485498ed..cfabda0ca7 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,12 @@ npm install -g @google/gemini-cli@nightly ### Automation & Integration +- **Custom Development Skills**: This branch introduces specialized skills for + the AI DevTools UX team: + - **\_ux_git-worktree**: Manage Git Worktrees using the "Base Folder + Strategy". + - **\_ux_finish-pr**: Co-author assistant for authors to cross the finish line + with UX polish and CI fixes. - Automate operational tasks like querying pull requests or handling complex rebases - Use MCP servers to connect new capabilities, including diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index a76054263f..b6654abb72 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -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/cli/src/commands/hooks/migrate.ts b/packages/cli/src/commands/hooks/migrate.ts index 36bb2cf9aa..5522221b90 100644 --- a/packages/cli/src/commands/hooks/migrate.ts +++ b/packages/cli/src/commands/hooks/migrate.ts @@ -79,7 +79,7 @@ function migrateClaudeHook(claudeHook: unknown): unknown { migrated['command'] = hook['command']; // Replace CLAUDE_PROJECT_DIR with GEMINI_PROJECT_DIR in command - // eslint-disable-next-line no-restricted-syntax + if (typeof migrated['command'] === 'string') { migrated['command'] = migrated['command'].replace( /\$CLAUDE_PROJECT_DIR/g, @@ -94,7 +94,7 @@ function migrateClaudeHook(claudeHook: unknown): unknown { } // Map timeout field (Claude uses seconds, Gemini uses seconds) - // eslint-disable-next-line no-restricted-syntax + if ('timeout' in hook && typeof hook['timeout'] === 'number') { migrated['timeout'] = hook['timeout']; } @@ -142,7 +142,6 @@ function migrateClaudeHooks(claudeConfig: unknown): Record { // Transform matcher if ( 'matcher' in definition && - // eslint-disable-next-line no-restricted-syntax typeof definition['matcher'] === 'string' ) { migratedDef['matcher'] = transformMatcher(definition['matcher']); diff --git a/packages/cli/src/test-utils/AppRig.tsx b/packages/cli/src/test-utils/AppRig.tsx index 8c62592bc6..6043c7f8cc 100644 --- a/packages/cli/src/test-utils/AppRig.tsx +++ b/packages/cli/src/test-utils/AppRig.tsx @@ -280,14 +280,14 @@ export class AppRig { } private stubRefreshAuth() { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + // eslint-disable-next-line @typescript-eslint/no-explicit-any const gcConfig = this.config as any; gcConfig.refreshAuth = async (authMethod: AuthType) => { gcConfig.modelAvailabilityService.reset(); const newContentGeneratorConfig = { authType: authMethod, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + proxy: gcConfig.getProxy(), apiKey: process.env['GEMINI_API_KEY'] || 'test-api-key', }; @@ -456,7 +456,7 @@ export class AppRig { const actualToolName = toolName === '*' ? undefined : toolName; this.config .getPolicyEngine() - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + .removeRulesForTool(actualToolName as string, source); this.breakpointTools.delete(toolName); } @@ -729,7 +729,7 @@ export class AppRig { .getGeminiClient() ?.getChatRecordingService(); if (recordingService) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-explicit-any (recordingService as any).conversationFile = null; } } @@ -749,7 +749,7 @@ export class AppRig { MockShellExecutionService.reset(); ideContextStore.clear(); // Forcefully clear IdeClient singleton promise - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-explicit-any (IdeClient as any).instancePromise = null; vi.clearAllMocks(); diff --git a/packages/cli/src/test-utils/customMatchers.ts b/packages/cli/src/test-utils/customMatchers.ts index ae9b44ee44..d34576cf3f 100644 --- a/packages/cli/src/test-utils/customMatchers.ts +++ b/packages/cli/src/test-utils/customMatchers.ts @@ -79,7 +79,7 @@ export async function toMatchSvgSnapshot( } function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment + // eslint-disable-next-line @typescript-eslint/no-explicit-any const { isNot } = this as any; let pass = true; const invalidLines: Array<{ line: number; content: string }> = []; @@ -108,7 +108,6 @@ function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) { }; } -// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion expect.extend({ toHaveOnlyValidCharacters, toMatchSvgSnapshot, diff --git a/packages/cli/src/test-utils/mockCommandContext.ts b/packages/cli/src/test-utils/mockCommandContext.ts index 47e56e1a44..b153aaf85e 100644 --- a/packages/cli/src/test-utils/mockCommandContext.ts +++ b/packages/cli/src/test-utils/mockCommandContext.ts @@ -37,14 +37,14 @@ export const createMockCommandContext = ( }, services: { config: null, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + settings: { merged: defaultMergedSettings, setValue: vi.fn(), forScope: vi.fn().mockReturnValue({ settings: {} }), } as unknown as LoadedSettings, git: undefined as GitService | undefined, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment + logger: { log: vi.fn(), logMessage: vi.fn(), @@ -53,7 +53,7 @@ export const createMockCommandContext = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, // Cast because Logger is a class. }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment + ui: { addItem: vi.fn(), clear: vi.fn(), @@ -72,7 +72,7 @@ export const createMockCommandContext = ( } as any, session: { sessionShellAllowlist: new Set(), - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + stats: { sessionStartTime: new Date(), lastPromptTokenCount: 0, @@ -93,14 +93,12 @@ export const createMockCommandContext = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any const merge = (target: any, source: any): any => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const output = { ...target }; for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const sourceValue = source[key]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const targetValue = output[key]; if ( @@ -108,11 +106,10 @@ export const createMockCommandContext = ( Object.prototype.toString.call(sourceValue) === '[object Object]' && Object.prototype.toString.call(targetValue) === '[object Object]' ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment output[key] = merge(targetValue, sourceValue); } else { // If not, we do a direct assignment. This preserves Date objects and others. - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + output[key] = sourceValue; } } @@ -120,6 +117,5 @@ export const createMockCommandContext = ( return output; }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return merge(defaultMocks, overrides); }; diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 74bac044c4..f3f692ced7 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -416,11 +416,10 @@ export const render = ( stdout.clear(); act(() => { instance = inkRenderDirect(tree, { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion stdout: stdout as unknown as NodeJS.WriteStream, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + stderr: stderr as unknown as NodeJS.WriteStream, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + stdin: stdin as unknown as NodeJS.ReadStream, debug: false, exitOnCtrlC: false, @@ -499,7 +498,6 @@ const getMockConfigInternal = (): Config => { return mockConfigInternal; }; -// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const configProxy = new Proxy({} as Config, { get(_target, prop) { if (prop === 'getTargetDir') { @@ -526,7 +524,6 @@ const configProxy = new Proxy({} as Config, { } const internal = getMockConfigInternal(); if (prop in internal) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return internal[prop as keyof typeof internal]; } throw new Error(`mockConfig does not have property ${String(prop)}`); @@ -657,7 +654,7 @@ export const renderWithProviders = ( uiState: providedUiState, width, mouseEventsEnabled = false, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + config = configProxy as unknown as Config, useAlternateBuffer = true, uiActions, @@ -685,20 +682,17 @@ export const renderWithProviders = ( button?: 0 | 1 | 2, ) => Promise; } => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const baseState: UIState = new Proxy( { ...baseMockUiState, ...providedUiState }, { get(target, prop) { if (prop in target) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return target[prop as keyof typeof target]; } // For properties not in the base mock or provided state, // we'll check the original proxy to see if it's a defined but // unprovided property, and if not, throw. if (prop in baseMockUiState) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return baseMockUiState[prop as keyof typeof baseMockUiState]; } throw new Error(`mockUiState does not have property ${String(prop)}`); @@ -736,7 +730,7 @@ export const renderWithProviders = ( if (prop === 'getUseAlternateBuffer') { return () => useAlternateBuffer; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return Reflect.get(target, prop, receiver); }, }); @@ -847,9 +841,8 @@ export function renderHook( waitUntilReady: () => Promise; generateSvg: () => string; } { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const result = { current: undefined as unknown as Result }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + let currentProps = options?.initialProps as Props; function TestComponent({ @@ -884,7 +877,6 @@ export function renderHook( function rerender(props?: Props) { if (arguments.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion currentProps = props as Props; } act(() => { @@ -920,7 +912,6 @@ export function renderHookWithProviders( waitUntilReady: () => Promise; generateSvg: () => string; } { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const result = { current: undefined as unknown as Result }; let setPropsFn: ((props: Props) => void) | undefined; @@ -942,7 +933,7 @@ export function renderHookWithProviders( act(() => { renderResult = renderWithProviders( - {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {} , options, @@ -952,7 +943,6 @@ export function renderHookWithProviders( function rerender(newProps?: Props) { act(() => { if (arguments.length > 0 && setPropsFn) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion setPropsFn(newProps as Props); } else if (forceUpdateFn) { forceUpdateFn(); diff --git a/packages/cli/src/test-utils/settings.ts b/packages/cli/src/test-utils/settings.ts index dd498b6625..ab2420849d 100644 --- a/packages/cli/src/test-utils/settings.ts +++ b/packages/cli/src/test-utils/settings.ts @@ -46,23 +46,22 @@ export const createMockSettings = ( workspace, isTrusted, errors, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + merged: mergedOverride, ...settingsOverrides } = overrides; const loaded = new LoadedSettings( - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion (system as any) || { path: '', settings: {}, originalSettings: {} }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (systemDefaults as any) || { path: '', settings: {}, originalSettings: {} }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (user as any) || { path: '', settings: settingsOverrides, originalSettings: settingsOverrides, }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (workspace as any) || { path: '', settings: {}, originalSettings: {} }, isTrusted ?? true, errors || [], @@ -76,7 +75,6 @@ export const createMockSettings = ( // Assign any function overrides (e.g., vi.fn() for methods) for (const key in overrides) { if (typeof overrides[key] === 'function') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment (loaded as any)[key] = overrides[key]; } } diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index d070840f2d..9a7ec0b932 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -505,9 +505,7 @@ export const useSlashCommandProcessor = ( const props = result.props as Record; if ( !props || - // eslint-disable-next-line no-restricted-syntax typeof props['name'] !== 'string' || - // eslint-disable-next-line no-restricted-syntax typeof props['displayName'] !== 'string' || !props['definition'] ) { 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/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/_ux_finish-pr/SKILL.md b/packages/core/src/skills/builtin/_ux_finish-pr/SKILL.md new file mode 100644 index 0000000000..a582e55a1e --- /dev/null +++ b/packages/core/src/skills/builtin/_ux_finish-pr/SKILL.md @@ -0,0 +1,37 @@ +--- +name: _ux_finish-pr +description: Expert PR maintenance with a focus on UX and functional polish. Use to check PR status, address feedback through interactive UX/functional review with the user, and fix failing CI checks. +--- + +# UX Finish PR + +You are a senior UX-focused co-author assistant, dedicated to helping the PR author cross the finish line. Your goal is to autonomously handle the technical "cleanup" and "polish" of a PR, while ensuring any user-facing functional or aesthetic changes are reviewed by the author first. + +## Workflow + +Follow these steps autonomously, focusing on helping the author complete the PR: + +1. **Assess PR Readiness:** + - Identify failing CI checks (lint, tests, builds) and diagnose their root causes. + - Gather unresolved comments from reviewers. + +2. **Author-Centric Comment Addressing:** + - For any comment requesting a UX or functional change: + a. Analyze the feedback and propose a specific technical solution. + b. **Pause and share your proposal with the author.** Explain how it addresses the feedback and what the resulting UX will be. + c. Wait for the author's directive to proceed. + - Autonomously handle minor technical or non-user-facing feedback. + +3. **Autonomous CI Fixes:** + - Propose and apply fixes for linting or test failures. + - **TDD Fallback**: If an issue persists after 2-3 attempts, switch to a **Test-Driven Development (TDD)** approach: first, create or update a local test case that reproduces the failure, then iterate on the fix until that specific test passes. + - Verify fixes locally using project standards (e.g., `npm run lint`, `npm test -u` to update all snapshots). + +4. **Final Cleanup & Update:** + - Sync with the latest `main`: `git fetch origin main && git rebase origin/main`. + - **Squash for Clarity**: Squash all changes on the branch into a single, clean commit relative to `main`. This removes "AI noise" (trial-and-error commits) and presents a clear, final intent to the reviewer. + - **Mandatory Verification**: You MUST verify that ALL relevant tests pass locally (e.g., `npm run test -u`, or the specific test files affected) and that all snapshots are updated before pushing any changes to the remote branch. + - Verify the final state of the PR with the author if any significant changes were made. + - Force-push with lease: `git push origin HEAD --force-with-lease`. + +Always provide a direct link to the PR after each major update. Prioritize brevity and technical rationale in your communication. diff --git a/packages/core/src/skills/builtin/_ux_git-worktree/SKILL.md b/packages/core/src/skills/builtin/_ux_git-worktree/SKILL.md new file mode 100644 index 0000000000..4bc291cb74 --- /dev/null +++ b/packages/core/src/skills/builtin/_ux_git-worktree/SKILL.md @@ -0,0 +1,56 @@ +--- +name: _ux_git-worktree +description: Manage Git Worktrees according to the "Base Folder Strategy". Use when the user wants to create branches, switch tasks, check out PRs, or manage parallel development environments. +--- + +# Git Worktree + +## Overview + +This skill manages the **Git Worktree "Base Folder" strategy**, ensuring that all functional work occurs in sibling sub-directories (e.g., `main/`, `feature-name/`) rather than nested branches. It prevents sandbox interference and enables parallel development. + +## Core Rules + +1. **Enforced Hierarchy**: New tasks or branches MUST be created as sibling directories to `main/`. +2. **No Nesting**: Branches should never be created inside existing sub-folders. +3. **Metadata Pathing**: When operating in a worktree, always include the primary `main/.git` path in the trusted environment to bypass macOS sandbox restrictions. + +## Workflows + +### 1. Creating a New Task (Branch) + +When the user asks to "start a new task" or "create a branch": +1. Identify the base directory (the parent of `main/`). +2. Use `git worktree add ../ -b ` from within `main/`. +3. **Mandatory Prep**: Run `npm install` inside the new worktree directory to ensure all dependencies are resolved. +4. Instruct the user to move into the new directory and reload their session. + +### 2. Checking out a PR (Semantic Naming) + +When the user asks to "check out PR #123": +1. **NEVER** use standard `gh pr checkout` without a directory. +2. **ALWAYS** use the automation script: `./packages/core/src/skills/builtin/_ux_git-worktree/scripts/worktree-manager.sh pr 123`. +3. **Mandatory Prep**: Run `npm install` inside the new worktree directory to ensure all dependencies are resolved. +4. This script will automatically fetch the PR title and create a semantic directory name (e.g., `pr-123-fix-core-bug`). + +### 3. Committing Changes in a Worktree + +If operating in a sibling worktree (e.g., `feature-xyz/`): +1. Check for sandbox access to `../main/.git`. +2. If access is denied, use `/directory add ../main/.git` (if interactive) or suggest the `--include-directories` flag for the next launch. + +## Task-Based Guide + +### Managing Worktrees +- **List Worktrees**: Run `git worktree list`. +- **Semantic PR Checkout**: `worktree-manager.sh pr `. +- **Add Manual Worktree**: `git worktree add ../ `. +- **Remove Worktree**: `git worktree remove `. + +## Resources + +### references/architecture.md +Technical details of the "Base Folder" standard. + +### scripts/worktree-manager.sh +Automated wrapper for Git Worktree operations that handles sibling pathing, semantic PR naming, and metadata links. \ No newline at end of file diff --git a/packages/core/src/skills/builtin/_ux_git-worktree/references/architecture.md b/packages/core/src/skills/builtin/_ux_git-worktree/references/architecture.md new file mode 100644 index 0000000000..2668d0d472 --- /dev/null +++ b/packages/core/src/skills/builtin/_ux_git-worktree/references/architecture.md @@ -0,0 +1,24 @@ +# Base Folder Strategy Architecture + +## Directory Layout + +```text +/project-root/ <-- Container directory (Base Folder) +├── main/ # Primary repository checkout (contains .git/) +├── feature-alpha/ # Isolated worktree for feature 'alpha' +├── bugfix-beta/ # Isolated worktree for bugfix 'beta' +└── ... +``` + +## Shared Metadata + +All worktrees (`feature-alpha/`, `bugfix-beta/`, etc.) share the Git database +located in `main/.git`. Git worktrees use a `.git` file (not a directory) that +contains a pointer to the original metadata: +`gitdir: /path/to/main/.git/worktrees/feature-alpha` + +## Sandbox Constraints (macOS) + +On macOS, the Seatbelt sandbox restricts write access to the worktree directory +only. To perform Git operations (which modify `main/.git/worktrees/`), the agent +requires explicit access to the `main/.git` path. diff --git a/packages/core/src/skills/builtin/_ux_git-worktree/scripts/worktree-manager.sh b/packages/core/src/skills/builtin/_ux_git-worktree/scripts/worktree-manager.sh new file mode 100755 index 0000000000..265f8d3b32 --- /dev/null +++ b/packages/core/src/skills/builtin/_ux_git-worktree/scripts/worktree-manager.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# worktree-manager.sh - Manage sibling worktrees for Gemini CLI + +set -e + +ACTION="${1}" +NAME="${2}" +BRANCH="${3}" + +BASE_DIR="$(pwd)" +PARENT_DIR="$(dirname "${BASE_DIR}")" + +slugify() { + local input="${1}" + local slug + slug=$(echo "${input}" | iconv -t ascii//TRANSLIT) + slug=$(echo "${slug}" | tr -cd "[:alnum:] ") + slug=$(echo "${slug}" | tr "[:upper:]" "[:lower:]") + slug=$(echo "${slug}" | tr " " "-") + slug="${slug//--/-}" + slug=$(echo "${slug}" | cut -c 1-50) + echo "${slug}" +} + +case "${ACTION}" in + "add") + if [[ -z "${NAME}" ]] || [[ -z "${BRANCH}" ]]; then + echo "Error: Usage: worktree-manager.sh add " + exit 1 + fi + git worktree add "${PARENT_DIR}/${NAME}" "${BRANCH}" + echo "Success: Added worktree at ${PARENT_DIR}/${NAME} tracking branch ${BRANCH}" + ;; + "pr") + if [[ -z "${NAME}" ]]; then + echo "Error: Usage: worktree-manager.sh pr " + exit 1 + fi + PR_NUMBER="${NAME}" + echo "Fetching PR details for #${PR_NUMBER}..." + + PR_DATA=$(gh pr view "${PR_NUMBER}" --json title,headRefName) + + PR_TITLE=$(echo "${PR_DATA}" | jq -r .title) + PR_BRANCH=$(echo "${PR_DATA}" | jq -r .headRefName) + + SLUG=$(slugify "${PR_TITLE}") + DIR_NAME="pr-${PR_NUMBER}-${SLUG}" + + echo "Creating semantic worktree: ${DIR_NAME}" + git worktree add "${PARENT_DIR}/${DIR_NAME}" "${PR_BRANCH}" + echo "Success: Added PR worktree at ${PARENT_DIR}/${DIR_NAME}" + ;; + "list") + git worktree list + ;; + "remove") + if [[ -z "${NAME}" ]]; then + echo "Error: Usage: worktree-manager.sh remove " + exit 1 + fi + git worktree remove "${PARENT_DIR}/${NAME}" + echo "Success: Removed worktree ${PARENT_DIR}/${NAME}" + ;; + *) + echo "Error: Unknown action ${ACTION}" + exit 1 + ;; +esac 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/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/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/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; diff --git a/scripts/sync-skills.sh b/scripts/sync-skills.sh new file mode 100755 index 0000000000..8bfe0a6e83 --- /dev/null +++ b/scripts/sync-skills.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# sync-skills.sh - Syncs custom skills from the repo to the user's global ~/.gemini/skills folder. +# It also creates a slash command for each skill to make them easily accessible. + +SKILLS_DIR="${HOME}/.gemini/skills" +COMMANDS_DIR="${HOME}/.gemini/commands" +REPO_SKILLS_PATH="packages/core/src/skills/builtin" + +mkdir -p "${SKILLS_DIR}" +mkdir -p "${COMMANDS_DIR}" + +echo "Syncing skills and commands..." + +# List of skills to sync +CUSTOM_SKILLS=("_ux_git-worktree" "_ux_finish-pr") + +for SKILL in "${CUSTOM_SKILLS[@]}"; do + if [[ -d "${REPO_SKILLS_PATH}/${SKILL}" ]]; then + # Sync Skill + cp -r "${REPO_SKILLS_PATH}/${SKILL}" "${SKILLS_DIR}/" + echo "✅ Synced: ${SKILL}" + + # Create Slash Command + COMMAND_FILE="${COMMANDS_DIR}/${SKILL}.toml" + cat < "${COMMAND_FILE}" +description = "Invoke the ${SKILL} skill" +prompt = "Activate the ${SKILL} skill and follow its instructions to: {{args}}" +EOF + echo "✅ Created Command: /${SKILL}" + else + echo "❌ Error: Skill ${SKILL} not found in ${REPO_SKILLS_PATH}" + fi +done + +echo "Done. Run '/skills reload' and '/commands reload' in your Gemini session to apply changes."