mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-11 18:40:57 -07:00
feat: introduce _ux_git-worktree and _ux_finish-pr skills
This commit is contained in:
@@ -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']);
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
@@ -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 ../<branch-name> -b <branch-name>` 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 <number>`.
|
||||
- **Add Manual Worktree**: `git worktree add ../<dir> <branch>`.
|
||||
- **Remove Worktree**: `git worktree remove <dir>`.
|
||||
|
||||
## 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.
|
||||
@@ -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.
|
||||
+69
@@ -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 <dir-name> <branch-name>"
|
||||
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 <pr-number>"
|
||||
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 <dir-name>"
|
||||
exit 1
|
||||
fi
|
||||
git worktree remove "${PARENT_DIR}/${NAME}"
|
||||
echo "Success: Removed worktree ${PARENT_DIR}/${NAME}"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown action ${ACTION}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
<T extends Message>(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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<string, unknown>)['_serverName'] === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, unknown>)['access_token'] === 'string'
|
||||
) {
|
||||
const obj = data as Record<string, unknown>;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user