Compare commits

..

2 Commits

Author SHA1 Message Date
gemini-cli[bot] 0b19986533 🤖 Gemini Bot Productivity Optimizations 2026-05-07 19:11:21 +00:00
gemini-cli[bot] 294fd9b75a # 📊 Implement Triage Accuracy Metric
This PR implements the "Triage Accuracy (Overrides)" metric as requested in #26660 (Option 1).

## Changes
- Created `tools/gemini-cli-bot/metrics/scripts/triage_accuracy.ts`.
- The script uses GraphQL to analyze the last 100 issues.
- It detects the first `area/*` label added by a bot (`gemini-cli-robot` or any `*[bot]`).
- It flags an "override" if a human later removes that label or replaces it with a different `area/*` label.

## Expected Impact
This metric provides a feedback loop for our automated triage system, allowing us to measure how often maintainers need to correct the bot's classification. This will help in fine-tuning the triage prompts and logic.

## Metrics Added
- `triage_accuracy_overrides`: Total number of human overrides in the sample.
- `triage_accuracy_total_bot_labeled`: Total number of issues labeled by the bot in the sample.
- `triage_accuracy_rate`: The ratio of correct (non-overridden) triage actions.
2026-05-07 18:59:27 +00:00
390 changed files with 7716 additions and 16197 deletions
+2 -4
View File
@@ -3,11 +3,9 @@
"extensionReloading": true, "extensionReloading": true,
"modelSteering": true, "modelSteering": true,
"autoMemory": true, "autoMemory": true,
"memoryManager": true,
"topicUpdateNarration": true, "topicUpdateNarration": true,
"voiceMode": true, "voiceMode": true
"adk": {
"agentSessionNoninteractiveEnabled": true
}
}, },
"general": { "general": {
"devtools": true "devtools": true
+8 -10
View File
@@ -114,14 +114,13 @@ runs:
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}' BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
DRY_RUN: '${{ inputs.dry-run }}' DRY_RUN: '${{ inputs.dry-run }}'
RELEASE_TAG: '${{ inputs.release-tag }}' RELEASE_TAG: '${{ inputs.release-tag }}'
GIT_PUSH_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}'
run: |- run: |-
set -e set -e
git add package.json package-lock.json packages/*/package.json git add package.json package-lock.json packages/*/package.json
git commit -m "chore(release): ${RELEASE_TAG}" git commit -m "chore(release): ${RELEASE_TAG}"
if [[ "${DRY_RUN}" == "false" ]]; then if [[ "${DRY_RUN}" == "false" ]]; then
echo "Pushing release branch to remote..." echo "Pushing release branch to remote..."
git push "https://x-access-token:${GIT_PUSH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:${BRANCH_NAME}" --follow-tags git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
else else
echo "Dry run enabled. Skipping push." echo "Dry run enabled. Skipping push."
fi fi
@@ -175,9 +174,9 @@ runs:
npm publish \ npm publish \
--dry-run="${INPUTS_DRY_RUN}" \ --dry-run="${INPUTS_DRY_RUN}" \
--workspace="${INPUTS_CORE_PACKAGE_NAME}" \ --workspace="${INPUTS_CORE_PACKAGE_NAME}" \
--tag staging-tmp --no-tag
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} staging-tmp npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} false
fi fi
- name: '🔗 Install latest core package' - name: '🔗 Install latest core package'
@@ -223,9 +222,9 @@ runs:
npm publish \ npm publish \
--dry-run="${INPUTS_DRY_RUN}" \ --dry-run="${INPUTS_DRY_RUN}" \
--workspace="${INPUTS_CLI_PACKAGE_NAME}" \ --workspace="${INPUTS_CLI_PACKAGE_NAME}" \
--tag staging-tmp --no-tag
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
npm dist-tag rm ${INPUTS_CLI_PACKAGE_NAME} staging-tmp npm dist-tag rm ${INPUTS_CLI_PACKAGE_NAME} false
fi fi
- name: 'Get a2a-server Token' - name: 'Get a2a-server Token'
@@ -250,9 +249,9 @@ runs:
npm publish \ npm publish \
--dry-run="${INPUTS_DRY_RUN}" \ --dry-run="${INPUTS_DRY_RUN}" \
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \ --workspace="${INPUTS_A2A_PACKAGE_NAME}" \
--tag staging-tmp --no-tag
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} staging-tmp npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} false
fi fi
- name: '🔬 Verify NPM release by version' - name: '🔬 Verify NPM release by version'
@@ -337,8 +336,7 @@ runs:
shell: 'bash' shell: 'bash'
run: | run: |
echo "Cleaning up release branch ${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}..." echo "Cleaning up release branch ${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}..."
git push "https://x-access-token:${GIT_PUSH_TOKEN}@github.com/${{ github.repository }}.git" --delete "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}" git push origin --delete "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}"
env: env:
GIT_PUSH_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}'
STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}' STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
-244
View File
@@ -1,244 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
module.exports = async ({ github, context, core }) => {
const rawLabels = process.env.LABELS_OUTPUT;
core.info(`Raw labels JSON: ${rawLabels}`);
let parsedLabels;
try {
// First, try to parse the raw output as JSON.
parsedLabels = JSON.parse(rawLabels);
} catch (jsonError) {
// If that fails, check for a markdown code block.
core.warning(
`Direct JSON parsing failed: ${jsonError.message}. Trying to extract from a markdown block.`,
);
const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
if (jsonMatch && jsonMatch[1]) {
try {
parsedLabels = JSON.parse(jsonMatch[1].trim());
} catch (markdownError) {
core.setFailed(
`Failed to parse JSON even after extracting from markdown block: ${markdownError.message}\nRaw output: ${rawLabels}`,
);
return;
}
} else {
// If no markdown block, try to find a raw JSON array in the output.
// The CLI may include debug/log lines (e.g. telemetry init, YOLO mode)
// before the actual JSON response.
const jsonArrayMatch = rawLabels.match(
/\[\s*\{\s*"issue_number"[\s\S]*\}\s*\]/,
);
if (jsonArrayMatch) {
try {
parsedLabels = JSON.parse(jsonArrayMatch[0]);
} catch (extractError) {
// It's possible the regex matched from a `[STARTUP]` log all the way to the end
// of the JSON array. We need to be more aggressive and find the FIRST `[ { "issue_number"`
core.warning(
`Strict array match failed: ${extractError.message}. Attempting to clean leading noisy brackets.`,
);
const fallbackMatch = rawLabels.match(
/(\[\s*\{\s*"issue_number"[\s\S]*)/,
);
if (fallbackMatch) {
try {
// We might have grabbed trailing noise too, so we find the last closing bracket
const cleaned = fallbackMatch[0].substring(
0,
fallbackMatch[0].lastIndexOf(']') + 1,
);
parsedLabels = JSON.parse(cleaned);
} catch (fallbackError) {
core.setFailed(
`Found JSON-like content but failed to parse: ${fallbackError.message}\nRaw output: ${rawLabels}`,
);
return;
}
} else {
core.setFailed(
`Found JSON-like content but failed to parse: ${extractError.message}\nRaw output: ${rawLabels}`,
);
return;
}
}
} else {
core.setFailed(
`Output is not valid JSON and does not contain extractable JSON.\nRaw output: ${rawLabels}`,
);
return;
}
}
}
core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
for (const entry of parsedLabels) {
const issueNumber = entry.issue_number;
if (!issueNumber) {
core.info(
`Skipping entry with no issue number: ${JSON.stringify(entry)}`,
);
continue;
}
let labelsToAdd = entry.labels_to_add || [];
let labelsToRemove = entry.labels_to_remove || [];
labelsToRemove.push('status/need-triage');
if (labelsToAdd.includes('status/manual-triage')) {
// If the AI flagged it for manual triage, remove bot-triaged if it exists
labelsToRemove.push('status/bot-triaged');
// Ensure we don't accidentally try to add bot-triaged if the AI returned it
labelsToAdd = labelsToAdd.filter((l) => l !== 'status/bot-triaged');
} else {
// Standard successful bot triage
labelsToAdd.push('status/bot-triaged');
}
// Deduplicate arrays
labelsToAdd = [...new Set(labelsToAdd)];
labelsToRemove = [...new Set(labelsToRemove)];
// Fetch existing labels to auto-resolve conflicts
try {
const { data: issueData } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
const existingLabels = issueData.labels.map((l) =>
typeof l === 'string' ? l : l.name,
);
const hasNewArea = labelsToAdd.some((l) => l.startsWith('area/'));
if (hasNewArea) {
const existingAreas = existingLabels.filter((l) =>
l.startsWith('area/'),
);
labelsToRemove.push(...existingAreas);
}
const hasNewPriority = labelsToAdd.some((l) => l.startsWith('priority/'));
if (hasNewPriority) {
const existingPriorities = existingLabels.filter((l) =>
l.startsWith('priority/'),
);
labelsToRemove.push(...existingPriorities);
}
const hasNewKind = labelsToAdd.some((l) => l.startsWith('kind/'));
if (hasNewKind) {
const existingKinds = existingLabels.filter((l) =>
l.startsWith('kind/'),
);
labelsToRemove.push(...existingKinds);
}
// Re-deduplicate and filter out labels we are trying to add
labelsToRemove = [...new Set(labelsToRemove)].filter(
(l) => !labelsToAdd.includes(l),
);
} catch (e) {
core.warning(
`Failed to fetch existing labels for #${issueNumber}: ${e.message}`,
);
}
// Enforce mutually exclusive area labels
const areaLabelsToAdd = labelsToAdd.filter((l) => l.startsWith('area/'));
if (areaLabelsToAdd.length > 1) {
core.warning(
`Issue #${issueNumber} has multiple area labels to add: ${areaLabelsToAdd.join(', ')}. Keeping only the first one.`,
);
const firstArea = areaLabelsToAdd[0];
labelsToAdd = labelsToAdd.filter(
(l) => !l.startsWith('area/') || l === firstArea,
);
}
// Enforce mutually exclusive priority labels
const priorityLabelsToAdd = labelsToAdd.filter((l) =>
l.startsWith('priority/'),
);
if (priorityLabelsToAdd.length > 1) {
core.warning(
`Issue #${issueNumber} has multiple priority labels to add: ${priorityLabelsToAdd.join(', ')}. Keeping only the first one.`,
);
const firstPriority = priorityLabelsToAdd[0];
labelsToAdd = labelsToAdd.filter(
(l) => !l.startsWith('priority/') || l === firstPriority,
);
}
if (labelsToAdd.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: labelsToAdd,
});
const explanation = entry.explanation ? ` - ${entry.explanation}` : '';
core.info(
`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}${explanation}`,
);
}
if (labelsToRemove.length > 0) {
for (const label of labelsToRemove) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: label,
});
} catch (e) {
if (e.status !== 404) {
core.warning(
`Failed to remove label ${label} from #${issueNumber}: ${e.message}`,
);
}
}
}
core.info(
`Successfully removed labels for #${issueNumber}: ${labelsToRemove.join(', ')}`,
);
}
if (
(entry.explanation && process.env.SUPPRESS_COMMENT !== 'true') ||
entry.effort_analysis
) {
let commentBody = '';
if (entry.explanation && process.env.SUPPRESS_COMMENT !== 'true') {
commentBody += entry.explanation;
}
if (entry.effort_analysis) {
if (commentBody) commentBody += '\n\n';
commentBody += `**Effort Analysis:**\n${entry.effort_analysis}`;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: commentBody,
});
}
if (
(!entry.labels_to_add || entry.labels_to_add.length === 0) &&
(!entry.labels_to_remove || entry.labels_to_remove.length === 0)
) {
core.info(
`No labels to add or remove for #${issueNumber}, leaving as is`,
);
}
}
};
-50
View File
@@ -1,50 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const fs = require('node:fs');
module.exports = async ({ github, context, core }) => {
let issuesToCleanup = [];
try {
const fileContent = fs.readFileSync('issues_to_cleanup.json', 'utf8');
issuesToCleanup = JSON.parse(fileContent);
} catch (error) {
if (error.code === 'ENOENT') {
core.info('No issues found to clean up.');
return;
}
core.setFailed(`Failed to read issues_to_cleanup.json: ${error.message}`);
return;
}
for (const issue of issuesToCleanup) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: 'status/need-triage',
});
core.info(
`Successfully removed status/need-triage from #${issue.number}`,
);
} catch (error) {
if (error.status === 404) {
core.info(
`Label status/need-triage not found on #${issue.number}, skipping.`,
);
} else {
core.warning(
`Failed to remove label from #${issue.number}: ${error.message}`,
);
}
}
}
core.info(
`Cleaned up status/need-triage from ${issuesToCleanup.length} issues.`,
);
};
@@ -1,60 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const fs = require('node:fs');
module.exports = async ({ github, context, core }) => {
core.info('Fetching open issues to check for conflicting labels...');
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100,
});
const conflictingLabelIssues = [];
for (const issue of issues) {
if (issue.pull_request) continue;
const areaLabels = issue.labels
.filter((l) => l.name && l.name.startsWith('area/'))
.map((l) => l.name);
const priorityLabels = issue.labels
.filter((l) => l.name && l.name.startsWith('priority/'))
.map((l) => l.name);
if (areaLabels.length > 1 || priorityLabels.length > 1) {
let message = `Issue #${issue.number} has conflicting labels:`;
if (areaLabels.length > 1)
message += ` multiple areas (${areaLabels.join(', ')}).`;
if (priorityLabels.length > 1)
message += ` multiple priorities (${priorityLabels.join(', ')}).`;
core.info(message);
conflictingLabelIssues.push({
number: issue.number,
title: issue.title,
body: issue.body || '',
});
}
}
// Limit to 50 to avoid overwhelming the AI in a single run
const issuesToProcess = conflictingLabelIssues.slice(0, 50);
fs.writeFileSync(
'conflicting_labels_issues.json',
JSON.stringify(issuesToProcess, null, 2),
);
core.info(
`Found ${conflictingLabelIssues.length} issues with conflicting labels. Wrote ${issuesToProcess.length} to conflicting_labels_issues.json`,
);
};
+14 -38
View File
@@ -41,41 +41,6 @@ module.exports = async ({ github, context, core }) => {
now.getTime() - NO_RESPONSE_DAYS * 24 * 60 * 60 * 1000, now.getTime() - NO_RESPONSE_DAYS * 24 * 60 * 60 * 1000,
); );
const maintainerCache = new Map();
async function isMaintainer(user, association) {
if (user?.type === 'Bot') return true;
if (['OWNER', 'MEMBER', 'COLLABORATOR'].includes(association)) return true;
const username = user?.login;
if (!username) return false;
if (maintainerCache.has(username)) {
return maintainerCache.get(username);
}
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username,
});
// Permission can be admin, write, read, none.
// Roles like 'maintain' or 'triage' often map to 'write' or 'read' in the top-level field.
const isM =
['admin', 'write'].includes(data.permission) ||
['admin', 'maintain', 'write'].includes(data.role_name);
maintainerCache.set(username, isM);
return isM;
} catch (err) {
core.warning(
`Could not check permissions for ${username}: ${err.message}`,
);
maintainerCache.set(username, false);
return false;
}
}
async function processItems(query, callback) { async function processItems(query, callback) {
core.info(`Searching: ${query}`); core.info(`Searching: ${query}`);
try { try {
@@ -118,7 +83,10 @@ module.exports = async ({ github, context, core }) => {
const lastComment = comments[0]; const lastComment = comments[0];
if ( if (
lastComment && lastComment &&
!(await isMaintainer(lastComment.user, lastComment.author_association)) !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(
lastComment.author_association,
) &&
lastComment.user?.type !== 'Bot'
) { ) {
core.info( core.info(
`Removing ${NEED_INFO_LABEL} from #${item.number} due to contributor response.`, `Removing ${NEED_INFO_LABEL} from #${item.number} due to contributor response.`,
@@ -220,7 +188,11 @@ module.exports = async ({ github, context, core }) => {
await processItems( await processItems(
`repo:${owner}/${repo} is:open is:pr -label:"help wanted" -label:"🔒 maintainer only" -label:"status/pr-nudge-sent" created:${prCloseThreshold.toISOString()}..${nudgeThreshold.toISOString()}`, `repo:${owner}/${repo} is:open is:pr -label:"help wanted" -label:"🔒 maintainer only" -label:"status/pr-nudge-sent" created:${prCloseThreshold.toISOString()}..${nudgeThreshold.toISOString()}`,
async (pr) => { async (pr) => {
if (await isMaintainer(pr.user, pr.author_association)) return; if (
['OWNER', 'MEMBER', 'COLLABORATOR'].includes(pr.author_association) ||
pr.user?.type === 'Bot'
)
return;
core.info(`Nudging PR #${pr.number} for contribution policy.`); core.info(`Nudging PR #${pr.number} for contribution policy.`);
if (!dryRun) { if (!dryRun) {
@@ -244,7 +216,11 @@ module.exports = async ({ github, context, core }) => {
await processItems( await processItems(
`repo:${owner}/${repo} is:open is:pr -label:"help wanted" -label:"🔒 maintainer only" created:<${prCloseThreshold.toISOString()}`, `repo:${owner}/${repo} is:open is:pr -label:"help wanted" -label:"🔒 maintainer only" created:<${prCloseThreshold.toISOString()}`,
async (pr) => { async (pr) => {
if (await isMaintainer(pr.user, pr.author_association)) return; if (
['OWNER', 'MEMBER', 'COLLABORATOR'].includes(pr.author_association) ||
pr.user?.type === 'Bot'
)
return;
core.info( core.info(
`Closing PR #${pr.number} per contribution policy (no 'help wanted').`, `Closing PR #${pr.number} per contribution policy (no 'help wanted').`,
-99
View File
@@ -1,99 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const fs = require('node:fs');
module.exports = async ({ github, context, core }) => {
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
issues(first: 50, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
id
number
title
body
issueType {
name
}
labels(first: 20) {
nodes {
name
}
}
}
}
}
}
`;
try {
const result = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
});
const issues = result.repository.issues.nodes;
const issuesNeedingAnalysis = [];
let syncedCount = 0;
for (const issue of issues) {
if (issue.issueType === null) {
const labelNames = issue.labels.nodes.map((l) => l.name);
const hasBug = labelNames.includes('kind/bug');
const hasFeature =
labelNames.includes('kind/feature') ||
labelNames.includes('kind/enhancement');
let issueTypeId = null;
if (hasBug) {
issueTypeId = 'IT_kwDOCaSVvs4BR7vP'; // Bug
} else if (hasFeature) {
issueTypeId = 'IT_kwDOCaSVvs4BR7vQ'; // Feature
}
if (issueTypeId) {
await github.graphql(
`
mutation($issueId: ID!, $issueTypeId: ID!) {
updateIssue(input: {id: $issueId, issueTypeId: $issueTypeId}) {
issue {
id
}
}
}
`,
{
issueId: issue.id,
issueTypeId: issueTypeId,
},
);
core.info(`Successfully synced Issue Type for #${issue.number}`);
syncedCount++;
} else {
// Needs analysis to determine kind/type
issuesNeedingAnalysis.push({
number: issue.number,
title: issue.title,
body: issue.body,
});
}
}
}
// Write issues needing analysis to a file so the AI can process them
fs.writeFileSync(
'no_type_issues.json',
JSON.stringify(issuesNeedingAnalysis),
);
core.info(`Synced ${syncedCount} issues from labels.`);
core.info(
`Found ${issuesNeedingAnalysis.length} issues missing both type and kind label to be analyzed.`,
);
} catch (error) {
core.setFailed(`Failed to sync issue types: ${error.message}`);
}
};
@@ -30,7 +30,6 @@ jobs:
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4 uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
with: with:
ref: '${{ inputs.ref || github.ref }}' ref: '${{ inputs.ref || github.ref }}'
persist-credentials: false
- name: 'Set up Node.js' - name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
-4
View File
@@ -148,7 +148,6 @@ jobs:
with: with:
ref: '${{ needs.parse_run_context.outputs.sha }}' ref: '${{ needs.parse_run_context.outputs.sha }}'
repository: '${{ needs.parse_run_context.outputs.repository }}' repository: '${{ needs.parse_run_context.outputs.repository }}'
persist-credentials: false
- name: 'Set up Node.js ${{ matrix.node-version }}' - name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
@@ -194,7 +193,6 @@ jobs:
with: with:
ref: '${{ needs.parse_run_context.outputs.sha }}' ref: '${{ needs.parse_run_context.outputs.sha }}'
repository: '${{ needs.parse_run_context.outputs.repository }}' repository: '${{ needs.parse_run_context.outputs.repository }}'
persist-credentials: false
- name: 'Set up Node.js 20.x' - name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
@@ -235,7 +233,6 @@ jobs:
with: with:
ref: '${{ needs.parse_run_context.outputs.sha }}' ref: '${{ needs.parse_run_context.outputs.sha }}'
repository: '${{ needs.parse_run_context.outputs.repository }}' repository: '${{ needs.parse_run_context.outputs.repository }}'
persist-credentials: false
- name: 'Set up Node.js 20.x' - name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
@@ -317,7 +314,6 @@ jobs:
with: with:
ref: '${{ needs.parse_run_context.outputs.sha }}' ref: '${{ needs.parse_run_context.outputs.sha }}'
repository: '${{ needs.parse_run_context.outputs.repository }}' repository: '${{ needs.parse_run_context.outputs.repository }}'
persist-credentials: false
fetch-depth: 0 fetch-depth: 0
- name: 'Set up Node.js 20.x' - name: 'Set up Node.js 20.x'
-10
View File
@@ -57,7 +57,6 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.branch_ref || github.ref }}' ref: '${{ github.event.inputs.branch_ref || github.ref }}'
fetch-depth: 0 fetch-depth: 0
@@ -131,8 +130,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Link Checker' - name: 'Link Checker'
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1 uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
with: with:
@@ -160,8 +157,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Set up Node.js ${{ matrix.node-version }}' - name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
@@ -257,8 +252,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Set up Node.js ${{ matrix.node-version }}' - name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
@@ -346,7 +339,6 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.branch_ref || github.ref }}' ref: '${{ github.event.inputs.branch_ref || github.ref }}'
- name: 'Initialize CodeQL' - name: 'Initialize CodeQL'
@@ -371,7 +363,6 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.branch_ref || github.ref }}' ref: '${{ github.event.inputs.branch_ref || github.ref }}'
fetch-depth: 1 fetch-depth: 1
@@ -399,7 +390,6 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.branch_ref || github.ref }}' ref: '${{ github.event.inputs.branch_ref || github.ref }}'
- name: 'Set up Node.js 20.x' - name: 'Set up Node.js 20.x'
-3
View File
@@ -43,7 +43,6 @@ jobs:
with: with:
ref: '${{ github.event.pull_request.head.sha }}' ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.repository }}' repository: '${{ github.repository }}'
persist-credentials: false
- name: 'Set up Node.js ${{ matrix.node-version }}' - name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
@@ -87,7 +86,6 @@ jobs:
with: with:
ref: '${{ github.event.pull_request.head.sha }}' ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.repository }}' repository: '${{ github.repository }}'
persist-credentials: false
- name: 'Set up Node.js 20.x' - name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
@@ -127,7 +125,6 @@ jobs:
with: with:
ref: '${{ github.event.pull_request.head.sha }}' ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.repository }}' repository: '${{ github.repository }}'
persist-credentials: false
- name: 'Set up Node.js 20.x' - name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
-1
View File
@@ -19,7 +19,6 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
ref: 'main' ref: 'main'
persist-credentials: false
- name: 'Set up Node.js' - name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
-2
View File
@@ -24,8 +24,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Setup Pages' - name: 'Setup Pages'
uses: 'actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b' # ratchet:actions/configure-pages@v5 uses: 'actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b' # ratchet:actions/configure-pages@v5
-2
View File
@@ -38,7 +38,6 @@ jobs:
with: with:
# Check out the trusted code from main for detection # Check out the trusted code from main for detection
fetch-depth: 0 fetch-depth: 0
persist-credentials: false
- name: 'Detect Steering Changes' - name: 'Detect Steering Changes'
id: 'detect' id: 'detect'
@@ -103,7 +102,6 @@ jobs:
# This only runs AFTER manual approval # This only runs AFTER manual approval
ref: '${{ github.event.pull_request.head.sha }}' ref: '${{ github.event.pull_request.head.sha }}'
fetch-depth: 0 fetch-depth: 0
persist-credentials: false
- name: 'Remove Approval Notification' - name: 'Remove Approval Notification'
# Run even if other steps fail, to ensure we clean up the "Action Required" message # Run even if other steps fail, to ensure we clean up the "Action Required" message
-4
View File
@@ -46,8 +46,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Set up Node.js' - name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
@@ -107,8 +105,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Download Logs' - name: 'Download Logs'
uses: 'actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806' # ratchet:actions/download-artifact@v4 uses: 'actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806' # ratchet:actions/download-artifact@v4
@@ -48,8 +48,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Log in to GitHub Container Registry' - name: 'Log in to GitHub Container Registry'
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3 uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
@@ -90,8 +90,6 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Generate GitHub App Token' - name: 'Generate GitHub App Token'
id: 'generate_token' id: 'generate_token'
+14 -21
View File
@@ -29,7 +29,7 @@ on:
default: false default: false
concurrency: concurrency:
group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number || github.ref }}' group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.inputs.issue_number || github.ref }}'
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
@@ -41,12 +41,14 @@ jobs:
github.event_name == 'schedule' || github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_interactive != 'true') || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_interactive != 'true') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_interactive == 'true') || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_interactive == 'true') ||
(github.event_name == 'issue_comment' && github.event.comment.user.login != 'gemini-cli[bot]' && contains(github.event.comment.body, '@gemini-cli') && contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.comment.author_association)) (github.event_name == 'issue_comment' && github.event.comment.user.login != 'gemini-cli[bot]' && contains(github.event.comment.body, '@gemini-cli') && contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_review_comment' && github.event.comment.user.login != 'gemini-cli[bot]' && contains(github.event.comment.body, '@gemini-cli') && contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.comment.author_association))
) )
# The reasoning phase is strictly readonly. # The reasoning phase is strictly readonly.
permissions: permissions:
contents: 'read' contents: 'read'
issues: 'read' issues: 'read'
pull-requests: 'read'
actions: 'read' actions: 'read'
env: env:
GEMINI_CLI_TRUST_WORKSPACE: 'true' GEMINI_CLI_TRUST_WORKSPACE: 'true'
@@ -55,7 +57,7 @@ jobs:
id: 'determine_ref' id: 'determine_ref'
env: env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}' ISSUE_NUMBER: '${{ github.event.issue.number || github.event.pull_request.number || github.event.inputs.issue_number }}'
run: | run: |
REF="${{ github.ref }}" REF="${{ github.ref }}"
if [ -n "$ISSUE_NUMBER" ]; then if [ -n "$ISSUE_NUMBER" ]; then
@@ -123,12 +125,11 @@ jobs:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}' GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
GEMINI_MODEL: 'gemini-3-flash-preview' GEMINI_MODEL: 'gemini-3-flash-preview'
GEMINI_CLI_HOME: 'tools/gemini-cli-bot'
ENABLE_PRS: "${{ github.event.inputs.enable_prs || 'false' }}" ENABLE_PRS: "${{ github.event.inputs.enable_prs || 'false' }}"
TRIGGER_ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}' TRIGGER_ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
TRIGGER_COMMENT_ID: '${{ github.event.comment.id || github.event.inputs.comment_id }}' TRIGGER_COMMENT_ID: '${{ github.event.comment.id || github.event.inputs.comment_id }}'
run: | run: |
PROMPT_PATH="tools/gemini-cli-bot/brain/scheduled.md" PROMPT_PATH="tools/gemini-cli-bot/brain/metrics.md"
if [ "${{ github.event_name }}" = "issue_comment" ] || [ "${{ github.event.inputs.run_interactive }}" = "true" ]; then if [ "${{ github.event_name }}" = "issue_comment" ] || [ "${{ github.event.inputs.run_interactive }}" = "true" ]; then
PROMPT_PATH="tools/gemini-cli-bot/brain/interactive.md" PROMPT_PATH="tools/gemini-cli-bot/brain/interactive.md"
export ENABLE_PRS="true" export ENABLE_PRS="true"
@@ -151,16 +152,9 @@ jobs:
echo "</untrusted_context>" >> trigger_context.md echo "</untrusted_context>" >> trigger_context.md
fi fi
if [ "$ENABLE_PRS" = "true" ]; then cat trigger_context.md "$PROMPT_PATH" tools/gemini-cli-bot/brain/common.md > combined_prompt.md
echo "**System Directive**: PR creation is ENABLED for this run. You MUST activate the **'prs' skill** to stage your changes and generate a \`pr-description.md\` file if you are proposing fixes." >> trigger_context.md
echo "**CRITICAL System Directive**: You MUST ONLY propose and implement a **SINGLE** improvement or fix per run. Bundling unrelated changes (e.g., a documentation update and a script fix, or a metrics update and a logic fix) into a single PR is STRICTLY FORBIDDEN and will result in immediate rejection during the critique phase. If you identify multiple issues, pick the most impactful one and ignore the others for now." >> trigger_context.md
else
echo "**System Directive**: PR creation is DISABLED for this run. You MUST NOT stage files or attempt to create a PR description." >> trigger_context.md
fi
echo "" >> trigger_context.md
cat trigger_context.md "$PROMPT_PATH" > combined_prompt.md node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml -p "$(cat combined_prompt.md)"
node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml --prompt="$(cat combined_prompt.md)"
if [ -n "$TRIGGER_ISSUE_NUMBER" ] && [ ! -s "issue-comment.md" ] && [ ! -s "pr-comment.md" ]; then if [ -n "$TRIGGER_ISSUE_NUMBER" ] && [ ! -s "issue-comment.md" ] && [ ! -s "pr-comment.md" ]; then
echo "Agent failed to respond. Generating fallback error message." echo "Agent failed to respond. Generating fallback error message."
@@ -170,18 +164,17 @@ jobs:
fi fi
- name: 'Run Critique Phase' - name: 'Run Critique Phase'
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}" if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event.inputs.run_interactive == 'true' }}"
env: env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}' GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
GEMINI_MODEL: 'gemini-3-flash-preview' GEMINI_MODEL: 'gemini-3-flash-preview'
GEMINI_CLI_HOME: 'tools/gemini-cli-bot'
run: | run: |
if git diff --staged --quiet; then if git diff --staged --quiet; then
echo "No changes staged. Skipping critique." echo "No changes staged. Skipping critique."
echo "[APPROVED]" > critique_result.txt echo "[APPROVED]" > critique_result.txt
else else
node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml --prompt="$(cat tools/gemini-cli-bot/.gemini/skills/critique/SKILL.md)" 2>&1 | tee critique_output.log node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml -p "$(cat tools/gemini-cli-bot/brain/critique.md)" 2>&1 | tee critique_output.log
if [ "${PIPESTATUS[0]}" -eq 0 ] && grep -q "\[APPROVED\]" critique_output.log && ! grep -q "\[REJECTED\]" critique_output.log; then if [ "${PIPESTATUS[0]}" -eq 0 ] && grep -q "\[APPROVED\]" critique_output.log && ! grep -q "\[REJECTED\]" critique_output.log; then
echo "[APPROVED]" > critique_result.txt echo "[APPROVED]" > critique_result.txt
@@ -192,7 +185,7 @@ jobs:
fi fi
- name: 'Generate Patch' - name: 'Generate Patch'
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}" if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event.inputs.run_interactive == 'true' }}"
run: | run: |
touch bot-changes.patch touch bot-changes.patch
touch pr-description.md touch pr-description.md
@@ -230,7 +223,7 @@ jobs:
steps: steps:
- name: 'Generate GitHub App Token 🔑' - name: 'Generate GitHub App Token 🔑'
id: 'generate_token' id: 'generate_token'
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}" if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event.inputs.run_interactive == 'true' }}"
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2 uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
with: with:
app-id: '${{ secrets.APP_ID }}' app-id: '${{ secrets.APP_ID }}'
@@ -245,7 +238,7 @@ jobs:
id: 'determine_ref' id: 'determine_ref'
env: env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}' ISSUE_NUMBER: '${{ github.event.issue.number || github.event.pull_request.number || github.event.inputs.issue_number }}'
run: | run: |
REF="main" REF="main"
if [ -n "$ISSUE_NUMBER" ]; then if [ -n "$ISSUE_NUMBER" ]; then
@@ -270,7 +263,7 @@ jobs:
path: '${{ runner.temp }}/brain-data/' path: '${{ runner.temp }}/brain-data/'
- name: 'Create or Update PR' - name: 'Create or Update PR'
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}" if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event.inputs.run_interactive == 'true' }}"
env: env:
GH_TOKEN: '${{ steps.generate_token.outputs.token }}' GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
FALLBACK_PAT: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' FALLBACK_PAT: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
@@ -23,7 +23,6 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with: with:
persist-credentials: false
fetch-depth: 0 fetch-depth: 0
- name: 'Setup Node.js' - name: 'Setup Node.js'
@@ -33,8 +33,6 @@ jobs:
- name: 'Checkout repository' - name: 'Checkout repository'
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4 uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
with:
persist-credentials: false
- name: 'Lifecycle Management' - name: 'Lifecycle Management'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
@@ -28,8 +28,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Log in to GitHub Container Registry' - name: 'Log in to GitHub Container Registry'
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3 uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
@@ -1,6 +1,10 @@
name: '📋 Gemini Scheduled Issue Triage' name: '📋 Gemini Scheduled Issue Triage'
on: on:
issues:
types:
- 'opened'
- 'reopened'
schedule: schedule:
- cron: '0 * * * *' # Runs every hour - cron: '0 * * * *' # Runs every hour
workflow_dispatch: workflow_dispatch:
@@ -19,15 +23,13 @@ permissions:
jobs: jobs:
triage-issues: triage-issues:
timeout-minutes: 60 timeout-minutes: 10
if: |- if: |-
${{ github.repository == 'google-gemini/gemini-cli' }} ${{ github.repository == 'google-gemini/gemini-cli' }}
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Generate GitHub App Token' - name: 'Generate GitHub App Token'
id: 'generate_token' id: 'generate_token'
@@ -49,26 +51,6 @@ jobs:
echo "has_issues=true" >> "${GITHUB_OUTPUT}" echo "has_issues=true" >> "${GITHUB_OUTPUT}"
echo "✅ Found issue #${{ github.event.issue.number }} from event to triage! 🎯" echo "✅ Found issue #${{ github.event.issue.number }} from event to triage! 🎯"
- name: 'Sync Issue Types'
if: |-
${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
script: |-
const syncIssueTypes = require('./.github/scripts/sync-issue-types.cjs');
await syncIssueTypes({ github, context, core });
- name: 'Find Issues with Conflicting Labels'
if: |-
${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
script: |-
const findConflictingLabels = require('./.github/scripts/find-conflicting-labels.cjs');
await findConflictingLabels({ github, context, core });
- name: 'Find untriaged issues' - name: 'Find untriaged issues'
if: |- if: |-
${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
@@ -81,55 +63,26 @@ jobs:
echo '🔍 Finding issues missing area labels...' echo '🔍 Finding issues missing area labels...'
gh issue list --repo "${GITHUB_REPOSITORY}" \ gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue -label:status/bot-triaged -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/documentation -label:area/unknown' --limit 50 --json number,title,body > no_area_issues.json --search 'is:open is:issue -label:status/bot-triaged -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/documentation -label:area/unknown' --limit 100 --json number,title,body > no_area_issues.json
echo '🔍 Finding issues missing kind labels...' echo '🔍 Finding issues missing kind labels...'
gh issue list --repo "${GITHUB_REPOSITORY}" \ gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue -label:status/bot-triaged -label:kind/bug -label:kind/enhancement -label:kind/customer-issue -label:kind/question' --limit 50 --json number,title,body > no_kind_issues.json --search 'is:open is:issue -label:status/bot-triaged -label:kind/bug -label:kind/enhancement -label:kind/customer-issue -label:kind/question' --limit 100 --json number,title,body > no_kind_issues.json
echo '🏷️ Finding issues missing priority labels...' echo '🏷️ Finding issues missing priority labels...'
gh issue list --repo "${GITHUB_REPOSITORY}" \ gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue -label:priority/p0 -label:priority/p1 -label:priority/p2 -label:priority/p3 -label:priority/unknown' --limit 50 --json number,title,body > no_priority_issues.json --search 'is:open is:issue -label:status/bot-triaged -label:priority/p0 -label:priority/p1 -label:priority/p2 -label:priority/p3 -label:priority/unknown' --limit 100 --json number,title,body > no_priority_issues.json
echo '📏 Finding issues missing effort labels...' echo '🔄 Merging and deduplicating issues...'
gh issue list --repo "${GITHUB_REPOSITORY}" \ jq -c -s 'add | unique_by(.number)' no_area_issues.json no_kind_issues.json no_priority_issues.json > issues_to_triage.json
--search 'is:open is:issue -label:effort/small -label:effort/medium -label:effort/large label:area/core,area/extensions,area/site,area/non-interactive' --limit 5 --json number,title,body > no_effort_issues.json
echo '🔄 Merging and deduplicating standard triage issues...' ISSUE_COUNT="$(jq 'length' issues_to_triage.json)"
if [ ! -f conflicting_labels_issues.json ]; then echo "[]" > conflicting_labels_issues.json; fi if [ "$ISSUE_COUNT" -gt 0 ]; then
jq -c -s 'add | unique_by(.number)' no_area_issues.json no_kind_issues.json no_priority_issues.json conflicting_labels_issues.json > standard_issues_to_triage.json
echo '📏 Deduplicating effort issues...'
jq -c -s 'add | unique_by(.number)' no_effort_issues.json > effort_issues_to_triage.json
STANDARD_COUNT="$(jq 'length' standard_issues_to_triage.json)"
EFFORT_COUNT="$(jq 'length' effort_issues_to_triage.json)"
if [ "$STANDARD_COUNT" -gt 0 ] || [ "$EFFORT_COUNT" -gt 0 ]; then
echo "has_issues=true" >> "${GITHUB_OUTPUT}" echo "has_issues=true" >> "${GITHUB_OUTPUT}"
echo "has_standard_issues=$([ "$STANDARD_COUNT" -gt 0 ] && echo 'true' || echo 'false')" >> "${GITHUB_OUTPUT}"
echo "has_effort_issues=$([ "$EFFORT_COUNT" -gt 0 ] && echo 'true' || echo 'false')" >> "${GITHUB_OUTPUT}"
else else
echo "has_issues=false" >> "${GITHUB_OUTPUT}" echo "has_issues=false" >> "${GITHUB_OUTPUT}"
echo "has_standard_issues=false" >> "${GITHUB_OUTPUT}"
echo "has_effort_issues=false" >> "${GITHUB_OUTPUT}"
fi fi
echo "✅ Found ${STANDARD_COUNT} standard issues and ${EFFORT_COUNT} effort issues to triage! 🎯" echo "✅ Found ${ISSUE_COUNT} unique issues to triage! 🎯"
- name: 'Create Gemini CLI Experiments Override'
if: |-
steps.get_issue_from_event.outputs.has_issues == 'true' || steps.find_issues.outputs.has_issues == 'true'
run: |
cat << 'EOF' > gemini_exp.json
{
"flags": [
{
"flagId": 45750526,
"boolValue": false
}
],
"experimentIds": []
}
EOF
- name: 'Get Repository Labels' - name: 'Get Repository Labels'
id: 'get_labels' id: 'get_labels'
@@ -146,18 +99,16 @@ jobs:
core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`); core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
return labelNames; return labelNames;
- name: 'Run Standard Triage Analysis' - name: 'Run Gemini Issue Analysis'
if: |- if: |-
steps.get_issue_from_event.outputs.has_issues == 'true' || steps.find_issues.outputs.has_standard_issues == 'true' steps.get_issue_from_event.outputs.has_issues == 'true' || steps.find_issues.outputs.has_issues == 'true'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0 uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
id: 'gemini_standard_issue_analysis' id: 'gemini_issue_analysis'
env: env:
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
REPOSITORY: '${{ github.repository }}' REPOSITORY: '${{ github.repository }}'
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}' AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
GEMINI_CLI_TRUST_WORKSPACE: 'true' GEMINI_CLI_TRUST_WORKSPACE: 'true'
GEMINI_EXP: 'gemini_exp.json'
GEMINI_STRICT_TELEMETRY_LIMITS: 'true'
with: with:
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}' gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}' gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
@@ -188,15 +139,13 @@ jobs:
## Steps ## Steps
1. You are only able to use the echo and read_file commands. Review the available labels in the environment variable: "${AVAILABLE_LABELS}". 1. You are only able to use the echo and read_file commands. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
2. Use the read_file tool to read the file "standard_issues_to_triage.json" which contains the JSON array of issues to triage. 2. Use the read_file tool to read the file "issues_to_triage.json" which contains the JSON array of issues to triage.
3. Review the issue title, body and any comments provided in the JSON file. 3. Review the issue title, body and any comments provided in the JSON file.
4. Identify the most relevant labels from the existing labels, specifically focusing on area/*, kind/*, and priority/*. 4. Identify the most relevant labels from the existing labels, specifically focusing on area/*, kind/* and priority/*.
5. Label Policy: 5. Label Policy:
- If the issue already has a kind/ label, do not change it. - If the issue already has a kind/ label, do not change it.
- If the issue has exactly ONE priority/ label, do not change it. - If the issue already has a priority/ label, do not change it.
- If the issue is missing a priority/ label, OR if the issue currently has MULTIPLE priority/ labels, you must evaluate the issue's impact to determine exactly ONE priority level (priority/p0, priority/p1, priority/p2, priority/p3, or priority/unknown) based the guidelines. If you are fixing an issue with multiple priority/ labels, put the correct one in `labels_to_add` and put all the incorrect ones in `labels_to_remove`. - If the issue already has an area/ label, do not change it.
- If the issue has exactly ONE area/ label, do not change it.
- If the issue is missing an area/ label, OR if the issue currently has MULTIPLE area/ labels, select exactly ONE area/ label that best fits the issue. Issues MUST NOT have multiple area/ labels. If you are fixing an issue with multiple area/ labels, put the correct one in `labels_to_add` and put all the incorrect ones in `labels_to_remove`.
- If any of these are missing, select exactly ONE appropriate label for the missing category. - If any of these are missing, select exactly ONE appropriate label for the missing category.
6. Identify other applicable labels based on the issue content, such as status/*, help wanted, good first issue, etc. 6. Identify other applicable labels based on the issue content, such as status/*, help wanted, good first issue, etc.
7. Give me a single short explanation about why you are selecting each label in the process. 7. Give me a single short explanation about why you are selecting each label in the process.
@@ -227,121 +176,11 @@ jobs:
- Do not add comments or modify the issue content. - Do not add comments or modify the issue content.
- Do not remove the following labels maintainer, help wanted or good first issue. - Do not remove the following labels maintainer, help wanted or good first issue.
- Triage only the current issue. - Triage only the current issue.
- Identify exactly ONE area/ label. Do NOT assign multiple area/ labels to a single issue. - Identify only one area/ label.
- Identify only one kind/ label (Do not apply kind/duplicate or kind/parent-issue) - Identify only one kind/ label (Do not apply kind/duplicate or kind/parent-issue)
- Identify exactly ONE priority/ label. Do NOT assign multiple priority/ labels to a single issue. - Identify only one priority/ label.
- Once you categorize the issue if it needs information bump down the priority by 1 eg.. a p0 would become a p1 a p1 would become a p2. P2 and P3 can stay as is in this scenario. - Once you categorize the issue if it needs information bump down the priority by 1 eg.. a p0 would become a p1 a p1 would become a p2. P2 and P3 can stay as is in this scenario.
Categorization Guidelines (Priority):
P0 - Urgent Blocking Issues:
- Definition: Critical failures breaking core functionality for a large portion of users. Examples: CLI fails to launch globally, core commands (gemini run) crash on valid input, unhandled promise rejections on boot, critical security vulnerability.
- Note: You must apply status/manual-triage instead of priority/p0.
P1 - Critical but Workable:
- Definition: Severe issues without a reasonable workaround, significantly degrading the developer experience but not globally blocking. Examples: Specific tools failing consistently (e.g., `web_search` returns 500s), persistent PTY streaming hangs, memory leaks leading to OOM after short use.
P2 - Significant Issues:
- Definition: Affect some workflows but a clear workaround exists, or non-critical bugs. Examples: Theme flickering, confusing error messages, minor UI misalignment, failing to read deeply nested config files correctly.
P3 - Minor/Enhancements:
- Definition: Trivial bugs, typos, documentation requests, or feature requests.
Categorization Guidelines (Kind):
kind/bug: The issue is describing an unexpected behavior or failure in the application.
kind/enhancement: The issue is describing a feature request or an improvement to an existing feature.
kind/question: The issue is asking a question about how to use the CLI or about a specific feature.
Categorization Guidelines (Area):
area/agent: The "brain" of the CLI. Core agent logic, model quality, tool/function calling, memory, web search, generated code quality, sub-agents.
area/core: The fundamental CLI app. UI/UX, installation, OS compatibility, performance, command parsing, theming, flickering.
area/documentation: Website docs, READMEs, inline help text.
area/enterprise: Telemetry, Policy, Quota / Licensing
area/extensions: Gemini CLI extensions capability
area/non-interactive: GitHub Actions, SDK, 3P Integrations, Shell Scripting, Command line automation
area/platform: Platform specific behavior
area/security: Authentication, authorization, privacy, data leaks, credential storage.
- name: 'Stop Telemetry Collector'
if: |-
steps.find_issues.outputs.has_effort_issues == 'true'
run: 'docker rm -f gemini-telemetry-collector || true'
- name: 'Run Effort Triage Analysis'
if: |-
steps.find_issues.outputs.has_effort_issues == 'true'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
id: 'gemini_effort_issue_analysis'
env:
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
REPOSITORY: '${{ github.repository }}'
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
GEMINI_CLI_TRUST_WORKSPACE: 'true'
GEMINI_EXP: 'gemini_exp.json'
GEMINI_STRICT_TELEMETRY_LIMITS: 'true'
with:
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |-
{
"maxSessionTurns": 25,
"coreTools": [
"run_shell_command(echo)",
"grep_search",
"glob",
"read_file"
],
"telemetry": {
"enabled": true,
"target": "gcp"
}
}
prompt: |-
## Role
You are an expert software architect. Analyze the provided GitHub issues and assign the correct `effort/*` label based on the codebase complexity.
## Steps
1. Use the read_file tool to read "effort_issues_to_triage.json".
2. For each issue in the array:
- You must evaluate the architectural complexity to determine the effort level. You MUST NOT guess the root cause. You MUST actively use your codebase search tools (grep_search and glob) to search for keywords from the issue and explore the codebase. You must identify the specific files and components involved before deciding the effort.
3. Output a JSON array of objects, each containing the issue number and the effort label to add, along with an explanation and an effort_analysis field. This effort_analysis must be highly detailed, technical, and empirical. It MUST NOT contain vague guesses (e.g., avoid words like "likely points to" or "possibly"). You must explicitly cite the specific file paths and architectural mechanisms you discovered using your search tools, explain the root cause, and then explicitly state how that complexity maps to the chosen effort level guidelines. For example:
```
[
{
"issue_number": 123,
"labels_to_add": ["effort/small"],
"explanation": "This is a simple logic fix.",
"effort_analysis": "The `vscode-ide-companion` extension indiscriminately tracks active text editors via `vscode.window.onDidChangeActiveTextEditor` in `open-files-manager.ts`. When a user opens `.vscode/settings.json`, its content is sent to the CLI's context. The fix is highly localized to the VS Code companion extension's event listener. It involves adding a simple conditional check to exclude specific configuration files from the active editor tracking logic, which is a trivial logic adjustment with a clear root cause."
}
]
```
## Guidelines
- Output only valid JSON format
- Do not include any explanation or additional text, just the JSON
- Triage only the current issue.
Categorization Guidelines (Effort):
effort/small (1 day or less):
- Trivial Logic & Config: Schema updates (Zod), feature flag toggles, adding missing fields to package.json or settings.json.
- UI/Aesthetic Adjustments: Fixing minor layout bugs in Ink components (e.g., adding flexShrink, correcting padding in a single Box), text color changes.
- Documentation & Strings: Typos, log message updates, CLI argument descriptions.
- Localized Bug Fixes: Single-file logic errors, straightforward promise rejections (e.g., wrapping a known failure in a try/catch), simple regex or string parsing fixes.
effort/medium (2-3 days):
- React/Ink State Management: Debugging useState/useEffect/useReducer bugs, component lifecycle issues (memory leaks in the UI), terminal redraw flickering, or state synchronization between the CLI's internal input buffer and the interactive React components.
- Asynchronous Flow & Integration: Resolving complex Promise chains, ERR_STREAM_PREMATURE_CLOSE, debugging IDE companion extensions (VS Code, Android Studio) or resolving hanging HTTP requests/IPC between the CLI and external plugins, timeouts in non-interactive/ACP modes.
- Tooling & Output Parsers: Modifying how tools parse streaming stdout/stderr buffers, adding new built-in tools that don't require native bindings.
- Cross-Component Refactors: Changes that span across packages/cli and packages/core to pass new data models or telemetry state.
effort/large (3+ days):
- Platform-Specific Complexities (PTY/Signals): Any issue involving node-pty, child_process.spawn, OS-level shell behavior (Windows vs Linux vs macOS), pseudo-terminal exhaustion (ENXIO), raw mode terminal desyncs, or POSIX signal forwarding (SIGINT/SIGTERM).
- Core Architecture & Protocols: Refactoring the Scheduler, Agent-to-Agent (A2A) protocol implementation, low-level MCP (Model Context Protocol) transport mechanisms.
- Performance & Memory: Diagnosing massive disk/memory leaks, severe boot time regressions, high-throughput streaming optimizations (e.g., voice streaming pipelines).
Note: Any bug that is described as intermittent, flickering, difficult to reproduce, platform-specific, or requiring cross-environment setups (e.g., involving the VS Code IDE companion, GCA plugin, or Android Studio) MUST NOT be rated as effort/small because of the increased overhead of testing and reproducing.
Categorization Guidelines (Priority): Categorization Guidelines (Priority):
P0 - Urgent Blocking Issues: P0 - Urgent Blocking Issues:
- DO NOT APPLY THIS LABEL AUTOMATICALLY. Use status/manual-triage instead. - DO NOT APPLY THIS LABEL AUTOMATICALLY. Use status/manual-triage instead.
@@ -379,65 +218,64 @@ jobs:
- This product is designed to use different models eg.. using pro, downgrading to flash etc. - This product is designed to use different models eg.. using pro, downgrading to flash etc.
- When users report that they dont expect the model to change those would be categorized as feature requests. - When users report that they dont expect the model to change those would be categorized as feature requests.
- name: 'Apply Standard Labels to Issues' - name: 'Apply Labels to Issues'
if: |- if: |-
${{ steps.gemini_standard_issue_analysis.outcome == 'success' && ${{ steps.gemini_issue_analysis.outcome == 'success' &&
steps.gemini_standard_issue_analysis.outputs.summary != '[]' && steps.gemini_issue_analysis.outputs.summary != '[]' }}
steps.gemini_standard_issue_analysis.outputs.summary != '' }}
env: env:
REPOSITORY: '${{ github.repository }}' REPOSITORY: '${{ github.repository }}'
LABELS_OUTPUT: '${{ steps.gemini_standard_issue_analysis.outputs.summary }}' LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
SUPPRESS_COMMENT: 'true'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with: with:
github-token: '${{ steps.generate_token.outputs.token }}' github-token: '${{ steps.generate_token.outputs.token }}'
script: |- script: |-
const applyLabels = require('./.github/scripts/apply-issue-labels.cjs'); const rawLabels = process.env.LABELS_OUTPUT;
await applyLabels({ github, context, core }); core.info(`Raw labels JSON: ${rawLabels}`);
let parsedLabels;
try {
const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
if (!jsonMatch || !jsonMatch[1]) {
throw new Error("Could not find a ```json ... ``` block in the output.");
}
const jsonString = jsonMatch[1].trim();
parsedLabels = JSON.parse(jsonString);
core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
} catch (err) {
core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
return;
}
- name: 'Apply Effort Labels to Issues' for (const entry of parsedLabels) {
if: |- const issueNumber = entry.issue_number;
${{ steps.gemini_effort_issue_analysis.outcome == 'success' && if (!issueNumber) {
steps.gemini_effort_issue_analysis.outputs.summary != '[]' && core.info(`Skipping entry with no issue number: ${JSON.stringify(entry)}`);
steps.gemini_effort_issue_analysis.outputs.summary != '' }} continue;
env: }
REPOSITORY: '${{ github.repository }}'
LABELS_OUTPUT: '${{ steps.gemini_effort_issue_analysis.outputs.summary }}'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
script: |-
const applyLabels = require('./.github/scripts/apply-issue-labels.cjs');
await applyLabels({ github, context, core });
- name: 'Sync Issue Types (Post-Analysis)' const labelsToAdd = entry.labels_to_add || [];
if: |- labelsToAdd.push('status/bot-triaged');
always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
script: |-
const syncIssueTypes = require('./.github/scripts/sync-issue-types.cjs');
await syncIssueTypes({ github, context, core });
- name: 'Find Triaged Issues to Clean Up' if (labelsToAdd.length > 0) {
if: |- await github.rest.issues.addLabels({
always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') owner: context.repo.owner,
env: repo: context.repo.repo,
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}' issue_number: issueNumber,
GITHUB_REPOSITORY: '${{ github.repository }}' labels: labelsToAdd
run: |- });
set -euo pipefail const explanation = entry.explanation ? ` - ${entry.explanation}` : '';
echo '🧹 Finding issues that have both bot-triaged and need-triage labels...' core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}${explanation}`);
gh issue list --repo "${GITHUB_REPOSITORY}" \ }
--search 'is:open is:issue label:status/bot-triaged label:status/need-triage' --limit 50 --json number > issues_to_cleanup.json
- name: 'Clean Up Triage Labels' if (entry.explanation) {
if: |- await github.rest.issues.createComment({
always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') owner: context.repo.owner,
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' repo: context.repo.repo,
with: issue_number: issueNumber,
github-token: '${{ steps.generate_token.outputs.token }}' body: entry.explanation,
script: |- });
const cleanupLabels = require('./.github/scripts/cleanup-triage-labels.cjs'); }
await cleanupLabels({ github, context, core });
if ((!entry.labels_to_add || entry.labels_to_add.length === 0) && (!entry.labels_to_remove || entry.labels_to_remove.length === 0)) {
core.info(`No labels to add or remove for #${issueNumber}, leaving as is`);
}
}
@@ -21,8 +21,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Generate GitHub App Token' - name: 'Generate GitHub App Token'
id: 'generate_token' id: 'generate_token'
@@ -19,8 +19,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4 uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
with:
persist-credentials: false
- name: 'Setup Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
@@ -43,8 +41,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4 uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
with:
persist-credentials: false
- name: 'Setup Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
-2
View File
@@ -17,8 +17,6 @@ jobs:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Link Checker' - name: 'Link Checker'
id: 'lychee' id: 'lychee'
-2
View File
@@ -16,8 +16,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Set up Node.js' - name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
-2
View File
@@ -16,8 +16,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Set up Node.js' - name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
@@ -44,7 +44,6 @@ jobs:
with: with:
ref: '${{ github.ref }}' ref: '${{ github.ref }}'
fetch-depth: 0 fetch-depth: 0
persist-credentials: false
- name: 'Setup Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
-2
View File
@@ -65,13 +65,11 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
fetch-depth: 0 fetch-depth: 0
- name: 'Checkout Release Code' - name: 'Checkout Release Code'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.ref }}' ref: '${{ github.event.inputs.ref }}'
path: 'release' path: 'release'
fetch-depth: 0 fetch-depth: 0
-2
View File
@@ -50,13 +50,11 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
fetch-depth: 0 fetch-depth: 0
- name: 'Checkout Release Code' - name: 'Checkout Release Code'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.ref }}' ref: '${{ github.event.inputs.ref }}'
path: 'release' path: 'release'
fetch-depth: 0 fetch-depth: 0
-1
View File
@@ -31,7 +31,6 @@ jobs:
- name: 'Checkout repository' - name: 'Checkout repository'
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4 uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
with: with:
persist-credentials: false
# The user-level skills need to be available to the workflow # The user-level skills need to be available to the workflow
fetch-depth: 0 fetch-depth: 0
ref: 'main' ref: 'main'
@@ -17,7 +17,6 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
fetch-depth: 1 fetch-depth: 1
- name: 'Slash Command Dispatch' - name: 'Slash Command Dispatch'
@@ -54,7 +54,6 @@ jobs:
with: with:
ref: '${{ github.event.inputs.ref }}' ref: '${{ github.event.inputs.ref }}'
fetch-depth: 0 fetch-depth: 0
persist-credentials: false
- name: 'Setup Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
@@ -64,7 +64,6 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: "${{ github.event.inputs.workflow_ref || 'main' }}" ref: "${{ github.event.inputs.workflow_ref || 'main' }}"
fetch-depth: 1 fetch-depth: 1
@@ -53,14 +53,12 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
fetch-depth: 0 fetch-depth: 0
fetch-tags: true fetch-tags: true
- name: 'Checkout Release Code' - name: 'Checkout Release Code'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.release_ref }}' ref: '${{ github.event.inputs.release_ref }}'
path: 'release' path: 'release'
fetch-depth: 0 fetch-depth: 0
+1 -10
View File
@@ -55,7 +55,6 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
fetch-depth: 0 fetch-depth: 0
fetch-tags: true fetch-tags: true
@@ -172,13 +171,11 @@ jobs:
- name: 'Checkout Ref' - name: 'Checkout Ref'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.ref }}' ref: '${{ github.event.inputs.ref }}'
- name: 'Checkout correct SHA' - name: 'Checkout correct SHA'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ matrix.sha }}' ref: '${{ matrix.sha }}'
path: 'release' path: 'release'
fetch-depth: 0 fetch-depth: 0
@@ -219,13 +216,11 @@ jobs:
- name: 'Checkout Ref' - name: 'Checkout Ref'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.ref }}' ref: '${{ github.event.inputs.ref }}'
- name: 'Checkout correct SHA' - name: 'Checkout correct SHA'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ needs.calculate-versions.outputs.PREVIEW_SHA }}' ref: '${{ needs.calculate-versions.outputs.PREVIEW_SHA }}'
path: 'release' path: 'release'
fetch-depth: 0 fetch-depth: 0
@@ -293,13 +288,11 @@ jobs:
- name: 'Checkout Ref' - name: 'Checkout Ref'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.ref }}' ref: '${{ github.event.inputs.ref }}'
- name: 'Checkout correct SHA' - name: 'Checkout correct SHA'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ needs.calculate-versions.outputs.STABLE_SHA }}' ref: '${{ needs.calculate-versions.outputs.STABLE_SHA }}'
path: 'release' path: 'release'
fetch-depth: 0 fetch-depth: 0
@@ -367,7 +360,6 @@ jobs:
- name: 'Checkout Ref' - name: 'Checkout Ref'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.ref }}' ref: '${{ github.event.inputs.ref }}'
- name: 'Setup Node.js' - name: 'Setup Node.js'
@@ -403,7 +395,6 @@ jobs:
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}' BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
DRY_RUN: '${{ github.event.inputs.dry_run }}' DRY_RUN: '${{ github.event.inputs.dry_run }}'
NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}' NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
GIT_PUSH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
run: |- run: |-
git add package.json packages/*/package.json git add package.json packages/*/package.json
if [ -f package-lock.json ]; then if [ -f package-lock.json ]; then
@@ -412,7 +403,7 @@ jobs:
git commit -m "chore(release): bump version to ${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}" git commit -m "chore(release): bump version to ${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"
if [[ "${DRY_RUN}" == "false" ]]; then if [[ "${DRY_RUN}" == "false" ]]; then
echo "Pushing release branch to remote..." echo "Pushing release branch to remote..."
git push "https://x-access-token:${GIT_PUSH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:${BRANCH_NAME}" --follow-tags git push --set-upstream origin "${BRANCH_NAME}"
else else
echo "Dry run enabled. Skipping push." echo "Dry run enabled. Skipping push."
fi fi
+1 -2
View File
@@ -52,7 +52,6 @@ jobs:
- name: 'Checkout repository' - name: 'Checkout repository'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4 uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.ref }}' ref: '${{ github.event.inputs.ref }}'
fetch-depth: 0 fetch-depth: 0
@@ -193,7 +192,7 @@ jobs:
run: | run: |
echo "ROLLBACK_TAG=$ROLLBACK_TAG_NAME" >> "$GITHUB_OUTPUT" echo "ROLLBACK_TAG=$ROLLBACK_TAG_NAME" >> "$GITHUB_OUTPUT"
git tag "$ROLLBACK_TAG_NAME" "${ORIGIN_HASH}" git tag "$ROLLBACK_TAG_NAME" "${ORIGIN_HASH}"
git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" --tags git push origin --tags
- name: 'Verify Rollback Tag Added' - name: 'Verify Rollback Tag Added'
if: "${{ github.event.inputs.dry-run == 'false' }}" if: "${{ github.event.inputs.dry-run == 'false' }}"
-1
View File
@@ -26,7 +26,6 @@ jobs:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with: with:
persist-credentials: false
ref: '${{ github.event.inputs.ref || github.sha }}' ref: '${{ github.event.inputs.ref || github.sha }}'
fetch-depth: 0 fetch-depth: 0
- name: 'Push' - name: 'Push'
-1
View File
@@ -32,7 +32,6 @@ jobs:
with: with:
ref: '${{ github.event.inputs.ref || github.sha }}' ref: '${{ github.event.inputs.ref || github.sha }}'
fetch-depth: 0 fetch-depth: 0
persist-credentials: false
- name: 'Install Dependencies' - name: 'Install Dependencies'
run: 'npm ci' run: 'npm ci'
- name: 'Build bundle' - name: 'Build bundle'
-2
View File
@@ -34,8 +34,6 @@ jobs:
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4 uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
with:
persist-credentials: false
- name: 'Optimize Windows Performance' - name: 'Optimize Windows Performance'
if: "matrix.os == 'windows-latest'" if: "matrix.os == 'windows-latest'"
-2
View File
@@ -44,8 +44,6 @@ jobs:
shell: 'bash' shell: 'bash'
run: 'echo "${{ toJSON(vars) }}"' run: 'echo "${{ toJSON(vars) }}"'
- uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with:
persist-credentials: false
- name: 'Verify release' - name: 'Verify release'
uses: './.github/actions/verify-release' uses: './.github/actions/verify-release'
with: with:
+1 -5
View File
@@ -43,13 +43,9 @@
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "CLI: Run Current File", "name": "Launch Program",
"runtimeExecutable": "node",
"runtimeArgs": ["--import", "tsx"],
"skipFiles": ["<node_internals>/**"], "skipFiles": ["<node_internals>/**"],
"program": "${file}", "program": "${file}",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"outFiles": ["${workspaceFolder}/**/*.js"] "outFiles": ["${workspaceFolder}/**/*.js"]
}, },
{ {
-30
View File
@@ -18,36 +18,6 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. | | [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. | | [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.42.0 - 2026-05-12
- **Auto Memory Inbox:** Introduced a new inbox flow for Auto Memory with a
canonical-patch contract for seamless skill management
([#26338](https://github.com/google-gemini/gemini-cli/pull/26338) by
@SandyTao520).
- **Gemma 4 by Default:** Enabled Gemma 4 models by default via the Gemini API
for all users
([#26307](https://github.com/google-gemini/gemini-cli/pull/26307) by
@Abhijit-2592).
- **Voice Mode Enhancements:** Added wave animations and privacy/compliance UX
warnings for the Gemini Live backend
([#26284](https://github.com/google-gemini/gemini-cli/pull/26284) by
@devr0306, [#26454](https://github.com/google-gemini/gemini-cli/pull/26454) by
@cocosheng-g).
## Announcements: v0.41.0 - 2026-05-05
- **Real-time Voice Mode:** Implemented real-time voice mode with cloud and
local backends
([#24174](https://github.com/google-gemini/gemini-cli/pull/24174) by
@Abhijit-2592).
- **Secure Environment Loading:** Enforced workspace trust and secured .env
loading in headless mode
([#25814](https://github.com/google-gemini/gemini-cli/pull/25814) by
@ehedlund).
- **Advanced Shell Validation:** Enhanced shell command validation and added
core tools allowlist for improved security
([#25720](https://github.com/google-gemini/gemini-cli/pull/25720) by @galz10).
## Announcements: v0.40.0 - 2026-04-28 ## Announcements: v0.40.0 - 2026-04-28
- **Offline Search and Themes:** Bundled ripgrep for offline search support and - **Offline Search and Themes:** Bundled ripgrep for offline search support and
+166 -261
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.42.0 # Latest stable release: v0.40.0
Released: May 12, 2026 Released: April 28, 2026
For most users, our latest stable release is the recommended release. Install For most users, our latest stable release is the recommended release. Install
the latest stable version with: the latest stable version with:
@@ -11,272 +11,177 @@ npm install -g @google/gemini-cli
## Highlights ## Highlights
- **Auto Memory Inbox:** Introduced a new inbox flow for Auto Memory using a - **Offline Search Support:** Bundled ripgrep binaries into the Single
canonical-patch contract, enabling more robust and manageable skill Executable Application (SEA) to enable powerful codebase searching even in
extraction. environments without internet access.
- **Gemma 4 Default:** Gemma 4 models are now enabled by default via the Gemini - **Enhanced Theme Customization:** Introduced GitHub-style colorblind-friendly
API, providing improved performance and capabilities out of the box. themes to improve accessibility and provide more personalized visual options.
- **Voice Mode Polish:** Added wave animations for visual feedback and - **MCP Resource Management:** Added new tools for listing and reading Model
privacy/compliance UX warnings specifically for the Gemini Live backend. Context Protocol (MCP) resources, enhancing the agent's ability to discover
- **Session Management:** Added a `--delete` flag to the `/exit` command for and utilize external data.
instant session deletion and introduced `/bug-memory` for easier heap - **Improved Narrative Flow:** Enabled topic update narrations by default to
diagnostics. provide better session structure and a clearer understanding of the agent's
- **Improved Reliability:** Reduced default API timeouts to 60s and implemented current focus.
retries for undici and premature stream closure errors. - **Streamlined Local Model Setup:** Introduced a simplified `gemini gemma`
command for quickly setting up and running Gemma models locally.
- **Prompt-Driven Memory Management:** Replaced the legacy `MemoryManagerAgent`
with a more efficient prompt-driven memory editing system across four tiers of
context.
## What's Changed ## What's Changed
- fix(cli): prevent automatic updates from switching to less stable channels by - chore(release): bump version to 0.40.0-nightly.20260414.g5b1f7375a by
@Adib234 in [#26132](https://github.com/google-gemini/gemini-cli/pull/26132)
- chore(release): bump version to 0.42.0-nightly.20260428.g59b2dea0e by
@gemini-cli-robot in @gemini-cli-robot in
[#26142](https://github.com/google-gemini/gemini-cli/pull/26142) [#25420](https://github.com/google-gemini/gemini-cli/pull/25420)
- fix(cli): pass node arguments via NODE_OPTIONS during relaunch to support SEA - Fix(core): retry additional OpenSSL 3.x SSL errors during streaming (#16075)
by @cocosheng-g in by @rcleveng in
[#26130](https://github.com/google-gemini/gemini-cli/pull/26130) [#25187](https://github.com/google-gemini/gemini-cli/pull/25187)
- fix(cli): handle DECKPAM keypad Enter sequences in terminal by @Gitanaskhan26 - fix(core): prevent YOLO mode from being downgraded by @galz10 in
in [#26092](https://github.com/google-gemini/gemini-cli/pull/26092) [#25341](https://github.com/google-gemini/gemini-cli/pull/25341)
- docs(cli): point plan-mode session retention to actual /settings labels by - feat: bundle ripgrep binaries into SEA for offline support by @scidomino in
@ifitisit in [#25978](https://github.com/google-gemini/gemini-cli/pull/25978) [#25342](https://github.com/google-gemini/gemini-cli/pull/25342)
- fix(core): add missing oauth fields support in subagent parsing by - Changelog for v0.39.0-preview.0 by @gemini-cli-robot in
@abhipatel12 in [#25417](https://github.com/google-gemini/gemini-cli/pull/25417)
[#26141](https://github.com/google-gemini/gemini-cli/pull/26141) - feat(test): add large conversation scenario for performance test by
- fix(core): disconnect extension-backed MCP clients in stopExtension by @cynthialong0-0 in
@cocosheng-g in [#25331](https://github.com/google-gemini/gemini-cli/pull/25331)
[#26136](https://github.com/google-gemini/gemini-cli/pull/26136) - improve(core): require recurrence evidence before extracting skills by
- Update documentation workflows with workspace trust by @g-samroberts in @SandyTao520 in
[#26150](https://github.com/google-gemini/gemini-cli/pull/26150) [#25147](https://github.com/google-gemini/gemini-cli/pull/25147)
- refactor(acp): modularize monolithic acpClient into specialized files by - test(evals): add subagent delegation evaluation tests by @anj-s in
@sripasg in [#26143](https://github.com/google-gemini/gemini-cli/pull/26143) [#24619](https://github.com/google-gemini/gemini-cli/pull/24619)
- test: fix failures due to antigravity environment leakage by @adamfweidman in - feat: add github colorblind themes by @Z1xus in
[#26162](https://github.com/google-gemini/gemini-cli/pull/26162) [#15504](https://github.com/google-gemini/gemini-cli/pull/15504)
- fix(core): add explicit empty log guard in A2A pushMessage by @adamfweidman in - fix(core): honor GOOGLE_GEMINI_BASE_URL and GOOGLE_VERTEX_BASE_URL by
[#26198](https://github.com/google-gemini/gemini-cli/pull/26198) @chrisjcthomas in
- feat(cli): add --delete flag to /exit command for session deletion by [#25357](https://github.com/google-gemini/gemini-cli/pull/25357)
@AbdulTawabJuly in - fix(cli): clean up slash command IDE listeners by @jasonmatthewsuhari in
[#19332](https://github.com/google-gemini/gemini-cli/pull/19332) [#24397](https://github.com/google-gemini/gemini-cli/pull/24397)
- test(core): add regression test for issue for ToolConfirmationResponse by - Changelog for v0.38.0 by @gemini-cli-robot in
@Adib234 in [#26194](https://github.com/google-gemini/gemini-cli/pull/26194) [#25470](https://github.com/google-gemini/gemini-cli/pull/25470)
- Add the ability to @ mention the gemini robot. by @gundermanc in - fix(evals): update eval tests for invoke_agent telemetry and project-scoped
[#26207](https://github.com/google-gemini/gemini-cli/pull/26207) memory by @SandyTao520 in
- test(evals): add EvalMetadata JSDoc annotations to older tests by @akh64bit in [#25502](https://github.com/google-gemini/gemini-cli/pull/25502)
[#26147](https://github.com/google-gemini/gemini-cli/pull/26147) - Changelog for v0.38.1 by @gemini-cli-robot in
- fix(core): reduce default API timeout to 60s and enable retries for undici [#25476](https://github.com/google-gemini/gemini-cli/pull/25476)
timeouts by @Adib234 in - feat(core): integrate skill-creator into skill extraction agent by
[#26191](https://github.com/google-gemini/gemini-cli/pull/26191) @SandyTao520 in
- fix(core): distinguish fallback chains and fix maxAttempts for auto vs [#25421](https://github.com/google-gemini/gemini-cli/pull/25421)
explicit model selection by @adamfweidman in - feat(cli): provide default post-submit prompt for skill command by @ruomengz
[#26163](https://github.com/google-gemini/gemini-cli/pull/26163) in [#25327](https://github.com/google-gemini/gemini-cli/pull/25327)
- fix(cli): handle InvalidStream event gracefully without throwing by - feat(core): add tools to list and read MCP resources by @ruomengz in
@adamfweidman in [#25395](https://github.com/google-gemini/gemini-cli/pull/25395)
[#26218](https://github.com/google-gemini/gemini-cli/pull/26218) - fix(evals): add typecheck coverage for evals, integration-tests, and
- ci(github-actions): switch to github app token and fix bot self-trigger by memory-tests by @SandyTao520 in
[#25480](https://github.com/google-gemini/gemini-cli/pull/25480)
- Use OSC 777 for terminal notifications by @jackyliuxx in
[#25300](https://github.com/google-gemini/gemini-cli/pull/25300)
- fix(extensions): fix bundling for examples by @abhipatel12 in
[#25542](https://github.com/google-gemini/gemini-cli/pull/25542)
- fix(cli): reset plan session state on /clear by @jasonmatthewsuhari in
[#25515](https://github.com/google-gemini/gemini-cli/pull/25515)
- feat(core): add .mdx support to get-internal-docs tool by @g-samroberts in
[#25090](https://github.com/google-gemini/gemini-cli/pull/25090)
- docs(policy): mention that workspace policies are broken by @6112 in
[#24367](https://github.com/google-gemini/gemini-cli/pull/24367)
- fix(core): allow explicit write permissions to override governance file
protections in sandboxes by @galz10 in
[#25338](https://github.com/google-gemini/gemini-cli/pull/25338)
- feat(sandbox): resolve custom seatbelt profiles from $HOME/.gemini first by
@mvanhorn in [#25427](https://github.com/google-gemini/gemini-cli/pull/25427)
- Reduce blank lines. by @gundermanc in
[#25563](https://github.com/google-gemini/gemini-cli/pull/25563)
- fix(ui): revert preview theme on dialog unmount by @JayadityaGit in
[#22542](https://github.com/google-gemini/gemini-cli/pull/22542)
- fix(core): fix ShellExecutionConfig spread and add ProjectRegistry save
backoff by @mahimashanware in
[#25382](https://github.com/google-gemini/gemini-cli/pull/25382)
- feat(core): Disable topic updates for subagents by @gundermanc in
[#25567](https://github.com/google-gemini/gemini-cli/pull/25567)
- feat(core): enable topic update narration by default and promote to general by
@gundermanc in @gundermanc in
[#26223](https://github.com/google-gemini/gemini-cli/pull/26223) [#25586](https://github.com/google-gemini/gemini-cli/pull/25586)
- Respect logPrompts flag for logging sensitive fields by @lp-peg in - docs: migrate installation and authentication to mdx with tabbed layouts by
[#26153](https://github.com/google-gemini/gemini-cli/pull/26153) @g-samroberts in
- fix: correct API key validation logic in handleApiKeySubmit by [#25155](https://github.com/google-gemini/gemini-cli/pull/25155)
@martin-hsu-test in - feat(config): split memoryManager flag into autoMemory by @SandyTao520 in
[#25453](https://github.com/google-gemini/gemini-cli/pull/25453) [#25601](https://github.com/google-gemini/gemini-cli/pull/25601)
- fix(agent): prevent exit_plan_mode from being called via shell by - fix(core): allow Cloud Shell users to use PRO_MODEL_NO_ACCESS experiment by
@Abhijit-2592 in @sehoon38 in [#25702](https://github.com/google-gemini/gemini-cli/pull/25702)
[#26230](https://github.com/google-gemini/gemini-cli/pull/26230) - fix(cli): round slow render latency to avoid opentelemetry float warning by
- # Fix: Inconsistent Case-Sensitivity in GrepTool by @.github/workflows/gemini-cli-bot-pulse.yml[bot] in [#26235](https://github.com/google-gemini/gemini-cli/pull/26235) @scidomino in [#25709](https://github.com/google-gemini/gemini-cli/pull/25709)
- docs(core): add automated gemma setup guide by @Samee24 in - docs(tracker): introduce experimental task tracker feature by @anj-s in
[#26233](https://github.com/google-gemini/gemini-cli/pull/26233) [#24556](https://github.com/google-gemini/gemini-cli/pull/24556)
- Allow non-https proxy urls to support container environments by @stevemk14ebr - docs(cli): fix inconsistent system.md casing in system prompt docs by @Bodlux
in [#26234](https://github.com/google-gemini/gemini-cli/pull/26234) in [#25414](https://github.com/google-gemini/gemini-cli/pull/25414)
- fix(bot): productivity and backlog optimizations by @gundermanc in - feat(cli): add streamlined `gemini gemma` local model setup by @Samee24 in
[#26236](https://github.com/google-gemini/gemini-cli/pull/26236) [#25498](https://github.com/google-gemini/gemini-cli/pull/25498)
- refactor(acp): delegate prompt turn processing logic to GeminiClient by - Changelog for v0.38.2 by @gemini-cli-robot in
@sripasg in [#26222](https://github.com/google-gemini/gemini-cli/pull/26222) [#25593](https://github.com/google-gemini/gemini-cli/pull/25593)
- fix(cli): refine platform-specific undo/redo and smart bubbling for WSL by - Fix: Disallow overriding IDE stdio via workspace .env (RCE) by @M0nd0R in
[#25022](https://github.com/google-gemini/gemini-cli/pull/25022)
- feat(test): refactor the memory usage test to use metrics from CLI process
instead of test runner by @cynthialong0-0 in
[#25708](https://github.com/google-gemini/gemini-cli/pull/25708)
- feat(vertex): add settings for Vertex AI request routing by @gordonhwc in
[#25513](https://github.com/google-gemini/gemini-cli/pull/25513)
- Fix/allow for session persistence by @ahsanfarooq210 in
[#25176](https://github.com/google-gemini/gemini-cli/pull/25176)
- Allow dots on GEMINI_API_KEY by @DKbyo in
[#25497](https://github.com/google-gemini/gemini-cli/pull/25497)
- feat(telemetry): add flag for enabling traces specifically by @spencer426 in
[#25343](https://github.com/google-gemini/gemini-cli/pull/25343)
- fix(core): resolve nested plan directory duplication and relative path
policies by @mahimashanware in
[#25138](https://github.com/google-gemini/gemini-cli/pull/25138)
- feat: detect new files in @ recommendations with watcher based updates by
@prassamin in [#25256](https://github.com/google-gemini/gemini-cli/pull/25256)
- fix(cli): use newline in shell command wrapping to avoid breaking heredocs by
@cocosheng-g in @cocosheng-g in
[#26202](https://github.com/google-gemini/gemini-cli/pull/26202) [#25537](https://github.com/google-gemini/gemini-cli/pull/25537)
- fix: suppress duplicate extension warnings during startup by @cocosheng-g in - fix(cli): ensure theme dialog labels are rendered for all themes by
[#26208](https://github.com/google-gemini/gemini-cli/pull/26208) @JayadityaGit in
- fix(cli): use byte length instead of string length for readStdin size limits [#24599](https://github.com/google-gemini/gemini-cli/pull/24599)
by @Adib234 in - fix(core): disable detached mode in Bun to prevent immediate SIGHUP of child
[#26224](https://github.com/google-gemini/gemini-cli/pull/26224) processes by @euxaristia in
- fix(ui): made shell tool header wrap on Ctrl+O by @devr0306 in [#22620](https://github.com/google-gemini/gemini-cli/pull/22620)
[#26229](https://github.com/google-gemini/gemini-cli/pull/26229) - feat: add /new as alias for /clear and refine command description by @ved015
- Changelog for v0.41.0-preview.0 by @gemini-cli-robot in in [#17865](https://github.com/google-gemini/gemini-cli/pull/17865)
[#26244](https://github.com/google-gemini/gemini-cli/pull/26244) - fix(cli): start auto memory in ACP sessions by @jasonmatthewsuhari in
- Skip binary CLI relaunch by @ruomengz in [#25626](https://github.com/google-gemini/gemini-cli/pull/25626)
[#26261](https://github.com/google-gemini/gemini-cli/pull/26261) - fix(core): remove duplicate initialize call on agents refreshed by
- fix(cli): do not override GOOGLE_CLOUD_PROJECT in Cloud Shell when using @adamfweidman in
Vertex AI by @jackwotherspoon in [#25670](https://github.com/google-gemini/gemini-cli/pull/25670)
[#24455](https://github.com/google-gemini/gemini-cli/pull/24455) - test(e2e): default integration tests to Flash Preview by @SandyTao520 in
- docs(cli): add skill discovery troubleshooting checklist to tutorial by [#25753](https://github.com/google-gemini/gemini-cli/pull/25753)
@pmenic in [#26018](https://github.com/google-gemini/gemini-cli/pull/26018) - refactor(memory): replace MemoryManagerAgent with prompt-driven memory editing
- docs(policy-engine): link to tools reference for tool names and args by across four tiers by @SandyTao520 in
@Aaxhirrr in [#22081](https://github.com/google-gemini/gemini-cli/pull/22081) [#25716](https://github.com/google-gemini/gemini-cli/pull/25716)
- Fix posting invalid response to a comment by @gundermanc in - fix(cli): fix "/clear (new)" command by @mini2s in
[#26266](https://github.com/google-gemini/gemini-cli/pull/26266) [#25801](https://github.com/google-gemini/gemini-cli/pull/25801)
- fix(cli): prevent informational logs from polluting json output by - fix(core): use dynamic CLI version for IDE client instead of hardcoded '1.0.0'
@cocosheng-g in by @thekishandev in
[#26264](https://github.com/google-gemini/gemini-cli/pull/26264) [#24414](https://github.com/google-gemini/gemini-cli/pull/24414)
- feat(ui): added microphone and updated placeholder for voice mode by @devr0306 - fix(core): handle line endings in ignore file parsing by @xoma-zver in
in [#26270](https://github.com/google-gemini/gemini-cli/pull/26270) [#23895](https://github.com/google-gemini/gemini-cli/pull/23895)
- feat(cli): Add 'list' subcommand to '/commands' by @Jwhyee in - Fix/command injection shell by @Famous077 in
[#22324](https://github.com/google-gemini/gemini-cli/pull/22324) [#24170](https://github.com/google-gemini/gemini-cli/pull/24170)
- fix(core): ensure tool output cleanup on session deletion for legacy files by - fix(ui): removed background color for input by @devr0306 in
@cocosheng-g in [#25339](https://github.com/google-gemini/gemini-cli/pull/25339)
[#26263](https://github.com/google-gemini/gemini-cli/pull/26263) - fix(devtools): reduce memory usage and defer connection by @SandyTao520 in
- Docs: Update Agent Skills documentation by @jkcinouye in [#24496](https://github.com/google-gemini/gemini-cli/pull/24496)
[#22388](https://github.com/google-gemini/gemini-cli/pull/22388) - fix(core): support jsonl session logs in memory and summary services by
- test(acp): add missing coverage for extensions command error paths by
@sahilkirad in
[#25313](https://github.com/google-gemini/gemini-cli/pull/25313)
- Changelog for v0.40.0 by @gemini-cli-robot in
[#26245](https://github.com/google-gemini/gemini-cli/pull/26245)
- fix: report AgentExecutionBlocked in non-interactive programmatic modes by
@cocosheng-g in
[#26262](https://github.com/google-gemini/gemini-cli/pull/26262)
- feat(extensions): add 'delete' as an alias for /extensions uninstall by
@martin-hsu-test in
[#25660](https://github.com/google-gemini/gemini-cli/pull/25660)
- fix(core): silently skip GEMINI.md paths that are directories (EISDIR) by
@martin-hsu-test in
[#25662](https://github.com/google-gemini/gemini-cli/pull/25662)
- fix(ci): checkout PR branch instead of main in bot workflow by @gundermanc in
[#26289](https://github.com/google-gemini/gemini-cli/pull/26289)
- fix(cli): use resolved sandbox state for auto-update check by @Adib234 in
[#26285](https://github.com/google-gemini/gemini-cli/pull/26285)
- # Metrics Integrity & Standardized Reporting (BT-01) by @.github/workflows/gemini-cli-bot-pulse.yml[bot] in [#26240](https://github.com/google-gemini/gemini-cli/pull/26240)
- Add Star History section to README by @bdmorgan in
[#26290](https://github.com/google-gemini/gemini-cli/pull/26290)
- Add Star History section to README by @bdmorgan in
[#26308](https://github.com/google-gemini/gemini-cli/pull/26308)
- Remove Star History section from README by @bdmorgan in
[#26309](https://github.com/google-gemini/gemini-cli/pull/26309)
- test(evals): add behavioral eval for file creation and write_file tool
selection by @akh64bit in
[#26292](https://github.com/google-gemini/gemini-cli/pull/26292)
- feat(config): enable Gemma 4 models by default via Gemini API by @Abhijit-2592
in [#26307](https://github.com/google-gemini/gemini-cli/pull/26307)
- fix(cli): insert voice transcription at cursor position instead of ap… by
@Zheyuan-Lin in
[#26287](https://github.com/google-gemini/gemini-cli/pull/26287)
- fix(ui): fix issue with box edges by @gundermanc in
[#26148](https://github.com/google-gemini/gemini-cli/pull/26148)
- fix(cli): respect .env override for GOOGLE_CLOUD_PROJECT by @DavidAPierce in
[#26288](https://github.com/google-gemini/gemini-cli/pull/26288)
- fix(ci): robust version checking in release verification by @scidomino in
[#26337](https://github.com/google-gemini/gemini-cli/pull/26337)
- fix(cli): enable daemon relaunch in binary and bundle keytar by @ruomengz in
[#26333](https://github.com/google-gemini/gemini-cli/pull/26333)
- fix(core): discourage unprompted git add . in prompt snippets by @akh64bit in
[#26220](https://github.com/google-gemini/gemini-cli/pull/26220)
- feat(ui): added wave animation for voice mode by @devr0306 in
[#26284](https://github.com/google-gemini/gemini-cli/pull/26284)
- fix(cli): prevent Escape from clearing input buffer (#17083) by @cocosheng-g
in [#26339](https://github.com/google-gemini/gemini-cli/pull/26339)
- fix(cli): undeprecate --prompt and correct positional query docs by @Adib234
in [#26329](https://github.com/google-gemini/gemini-cli/pull/26329)
- Metrics updates by @.github/workflows/gemini-cli-bot-pulse.yml[bot] in
[#26348](https://github.com/google-gemini/gemini-cli/pull/26348)
- fix(core): remove "System: Please continue." injection on InvalidStream events
by @SandyTao520 in
[#26340](https://github.com/google-gemini/gemini-cli/pull/26340)
- docs(policy-engine): add tool argument keys reference and shell policy
cross-links by @harshpujari in
[#25292](https://github.com/google-gemini/gemini-cli/pull/25292)
- fix(cli): resolve Ghostty/raw-mode False Cancellation in oauth flow by
@Aarchi-07 in [#25026](https://github.com/google-gemini/gemini-cli/pull/25026)
- fix(core): reset session-scoped state on resumption by @cocosheng-g in
[#26342](https://github.com/google-gemini/gemini-cli/pull/26342)
- Fix bulk of remaining issues with generalist profile by @joshualitt in
[#26073](https://github.com/google-gemini/gemini-cli/pull/26073)
- fix(core): make subagents aware of active approval modes by @akh64bit in
[#23608](https://github.com/google-gemini/gemini-cli/pull/23608)
- fix(acp): resolve agent mode disconnect and improve mode awareness by @sripasg
in [#26332](https://github.com/google-gemini/gemini-cli/pull/26332)
- docs(sdk): add JSDoc to exported interfaces in packages/sdk/src/types.ts by
@cocosheng-g in
[#26441](https://github.com/google-gemini/gemini-cli/pull/26441)
- perf: skip redundant GEMINI.md loading in partialConfig by @cocosheng-g in
[#26443](https://github.com/google-gemini/gemini-cli/pull/26443)
- Enhance React guidelines by @psinha40898 in
[#22667](https://github.com/google-gemini/gemini-cli/pull/22667)
- feat(core): reinforce Inquiry constraints to prevent unauthorized changes by
@akh64bit in [#26310](https://github.com/google-gemini/gemini-cli/pull/26310)
- revert: fix(ci): robust version checking in release verification (#26337) by
@scidomino in [#26450](https://github.com/google-gemini/gemini-cli/pull/26450)
- refactor(UI): created constants file for ThemeDialog by @devr0306 in
[#26446](https://github.com/google-gemini/gemini-cli/pull/26446)
- docs: fix GitHub capitalization in releases guide by @haosenwang1018 in
[#26379](https://github.com/google-gemini/gemini-cli/pull/26379)
- fix(cli): ensure branch indicator updates in sub-directories and worktrees by
@Adib234 in [#26330](https://github.com/google-gemini/gemini-cli/pull/26330)
- feat: add minimal V8 heap snapshot utility for memory diagnostics by
@cocosheng-g in
[#26440](https://github.com/google-gemini/gemini-cli/pull/26440)
- fix(hooks): preserve non-text parts in fromHookLLMRequest by @SandyTao520 in
[#26275](https://github.com/google-gemini/gemini-cli/pull/26275)
- fix(cli): allow early stdout when config is undefined by @cocosheng-g in
[#26453](https://github.com/google-gemini/gemini-cli/pull/26453)
- fix(cli)#21297: clear skills consent dialog before reload by @manavmax in
[#26431](https://github.com/google-gemini/gemini-cli/pull/26431)
- fix(cli): render LaTeX-style output as Unicode in the TUI by @dimssu in
[#25802](https://github.com/google-gemini/gemini-cli/pull/25802)
- fix(core): use close event instead of exit in child_process fallback by
@tusaryan in [#25695](https://github.com/google-gemini/gemini-cli/pull/25695)
- feat(voice): add privacy and compliance UX warning for Gemini Live backend by
@cocosheng-g in
[#26454](https://github.com/google-gemini/gemini-cli/pull/26454)
- feat(memory): add Auto Memory inbox flow with canonical-patch contract by
@SandyTao520 in @SandyTao520 in
[#26338](https://github.com/google-gemini/gemini-cli/pull/26338) [#25816](https://github.com/google-gemini/gemini-cli/pull/25816)
- test(cleanup): fix temporary directory leaks in test suites by @Adib234 in - fix(release): exclude ripgrep binaries from npm tarballs by @SandyTao520 in
[#26217](https://github.com/google-gemini/gemini-cli/pull/26217) [#25841](https://github.com/google-gemini/gemini-cli/pull/25841)
- feat: add ignoreLocalEnv setting and --ignore-env flag (#2493) by @cocosheng-g - fix(patch): cherry-pick 048bf6e to release/v0.40.0-preview.3-pr-25941 to patch
in [#26445](https://github.com/google-gemini/gemini-cli/pull/26445) version v0.40.0-preview.3 and create version 0.40.0-preview.4 by
- docs(sdk): add JSDoc to all exported interfaces and types by @fauzan171 in
[#26277](https://github.com/google-gemini/gemini-cli/pull/26277)
- feat(cli): improve /agents refresh logging by @cocosheng-g in
[#26442](https://github.com/google-gemini/gemini-cli/pull/26442)
- Fix: make Dockerfile self-contained with multi-stage build by @Famous077 in
[#24277](https://github.com/google-gemini/gemini-cli/pull/24277)
- fix(core): filter unsupported multimodal types from tool responses by
@aishaneeshah in
[#26352](https://github.com/google-gemini/gemini-cli/pull/26352)
- fix(core): properly format markdown in AskUser tool by unescaping newlines by
@Adib234 in [#26349](https://github.com/google-gemini/gemini-cli/pull/26349)
- feat(bot): add actions spend metric script by @gundermanc in
[#26463](https://github.com/google-gemini/gemini-cli/pull/26463)
- feat(cli): add /bug-memory command and auto-capture heap snapshot in /bug by
@Anjaligarhwal in
[#25639](https://github.com/google-gemini/gemini-cli/pull/25639)
- fix(cli): make SkillInboxDialog fit and scroll in alternate buffer by
@SandyTao520 in
[#26455](https://github.com/google-gemini/gemini-cli/pull/26455)
- Robust Scale-Safe Lifecycle Consolidation by @gemini-cli-robot in
[#26355](https://github.com/google-gemini/gemini-cli/pull/26355)
- fix(ci): respect exempt labels when closing stale items by @gundermanc in
[#26475](https://github.com/google-gemini/gemini-cli/pull/26475)
- fix(cli): use os.homedir() for home directory warning check by @TirthNaik-99
in [#25890](https://github.com/google-gemini/gemini-cli/pull/25890)
- fix(a2a-server): resolve tool approval race condition and improve status
reporting by @kschaab in
[#26479](https://github.com/google-gemini/gemini-cli/pull/26479)
- fix(cli): prevent settings dialog border clipping using maxHeight by
@jackwotherspoon in
[#26507](https://github.com/google-gemini/gemini-cli/pull/26507)
- feat: allow queuing messages during compression (#24071) by @cocosheng-g in
[#26506](https://github.com/google-gemini/gemini-cli/pull/26506)
- fix(core): retry on ERR_STREAM_PREMATURE_CLOSE errors by @cocosheng-g in
[#26519](https://github.com/google-gemini/gemini-cli/pull/26519)
- fix(core): Minor fixes for generalist profile. by @joshualitt in
[#26357](https://github.com/google-gemini/gemini-cli/pull/26357)
- fix(patch): cherry-pick 3627f47 to release/v0.42.0-preview.0-pr-26542 to patch
version v0.42.0-preview.0 and create version 0.42.0-preview.1 by
@gemini-cli-robot in @gemini-cli-robot in
[#26544](https://github.com/google-gemini/gemini-cli/pull/26544) [#25942](https://github.com/google-gemini/gemini-cli/pull/25942)
- fix(patch): cherry-pick 02995ba to release/v0.42.0-preview.1-pr-26568 to patch - fix(patch): cherry-pick 54b7586 to release/v0.40.0-preview.4-pr-26066
version v0.42.0-preview.1 and create version 0.42.0-preview.2 by [CONFLICTS] by @gemini-cli-robot in
@gemini-cli-robot in [#26124](https://github.com/google-gemini/gemini-cli/pull/26124)
[#26590](https://github.com/google-gemini/gemini-cli/pull/26590)
**Full Changelog**: **Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.41.2...v0.42.0 https://github.com/google-gemini/gemini-cli/compare/v0.39.1...v0.40.0
+227 -178
View File
@@ -1,6 +1,6 @@
# Preview release: v0.43.0-preview.0 # Preview release: v0.42.0-preview.0
Released: May 12, 2026 Released: May 05, 2026
Our preview release includes the latest, new, and experimental features. This Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md). release may not be as stable as our [latest weekly release](latest.md).
@@ -13,184 +13,233 @@ npm install -g @google/gemini-cli@preview
## Highlights ## Highlights
- **Surgical Code Edits:** Steer models to use the `edit` tool for precise code - **Auto Memory Enhancements:** Introduced an Auto Memory inbox flow with a
modifications, improving accuracy and reducing context usage. canonical-patch contract for better memory management.
- **Session Portability:** Added ability to export chat sessions to files and - **Improved Voice Mode:** Added a wave animation, microphone icon updates, and
import them via a new CLI flag, enabling session persistence and sharing. privacy/compliance UX warnings for the Gemini Live backend.
- **Enhanced Security:** Introduced comprehensive shell command safety - **New CLI Commands & Flags:** Added a `--delete` flag to the `/exit` command
evaluations and strengthened model steering to prevent unauthorized changes. for session deletion, a `list` subcommand to `/commands`, and a `/bug-memory`
- **Context Management:** Implemented a new adaptive token calculator for more command for heap snapshots.
accurate content size estimations and optimized context pipelines. - **Expanded Model Support:** Gemma 4 models are now enabled by default via the
- **UX Improvements:** Enhanced tool call visibility with prefixed IDs and Gemini API.
improved the UI for session resumption and MCP list management. - **Enhanced Core Resilience:** Improved API resilience with reduced timeouts,
automatic retries for stream errors, and better handling of invalid stream
events.
## What's Changed ## What's Changed
- feat(core): steer model to use edit tool for surgical edits, fix a typo in - fix(cli): prevent automatic updates from switching to less stable channels in
[#26480](https://github.com/google-gemini/gemini-cli/pull/26480) [#26132](https://github.com/google-gemini/gemini-cli/pull/26132)
- docs: clarify Auto Memory proposes memory updates and skills in - chore(release): bump version to 0.42.0-nightly.20260428.g59b2dea0e in
[#26527](https://github.com/google-gemini/gemini-cli/pull/26527) [#26142](https://github.com/google-gemini/gemini-cli/pull/26142)
- fix(core): reject numeric project IDs in GOOGLE_CLOUD_PROJECT (#24695) in - fix(cli): pass node arguments via NODE_OPTIONS during relaunch to support SEA
[#26532](https://github.com/google-gemini/gemini-cli/pull/26532) in [#26130](https://github.com/google-gemini/gemini-cli/pull/26130)
- fix(core): remove unsafe type assertion suppressions in error utils in - fix(cli): handle DECKPAM keypad Enter sequences in terminal in
[#19881](https://github.com/google-gemini/gemini-cli/pull/19881) [#26092](https://github.com/google-gemini/gemini-cli/pull/26092)
- fix(core): allow redirection in YOLO and AUTO_EDIT modes without sandboxing in - docs(cli): point plan-mode session retention to actual /settings labels in
[#26542](https://github.com/google-gemini/gemini-cli/pull/26542) [#25978](https://github.com/google-gemini/gemini-cli/pull/25978)
- ci(release): build and attach unsigned macOS binaries to releases in - fix(core): add missing oauth fields support in subagent parsing in
[#26462](https://github.com/google-gemini/gemini-cli/pull/26462) [#26141](https://github.com/google-gemini/gemini-cli/pull/26141)
- fix(core): Fix chat corruption bug in context manager. in - fix(core): disconnect extension-backed MCP clients in stopExtension in
[#26534](https://github.com/google-gemini/gemini-cli/pull/26534) [#26136](https://github.com/google-gemini/gemini-cli/pull/26136)
- fix(cli): provide JSON output for AgentExecutionStopped in non-interactive - Update documentation workflows with workspace trust in
mode in [#26504](https://github.com/google-gemini/gemini-cli/pull/26504) [#26150](https://github.com/google-gemini/gemini-cli/pull/26150)
- feat(evals): add shell command safety evals in - refactor(acp): modularize monolithic acpClient into specialized files in
[#26528](https://github.com/google-gemini/gemini-cli/pull/26528) [#26143](https://github.com/google-gemini/gemini-cli/pull/26143)
- fix(core): handle invalid custom plans directory gracefully in - test: fix failures due to antigravity environment leakage in
[#26560](https://github.com/google-gemini/gemini-cli/pull/26560) [#26162](https://github.com/google-gemini/gemini-cli/pull/26162)
- fix(acp): move tool explanation from thought stream to tool call content in - fix(core): add explicit empty log guard in A2A pushMessage in
[#26554](https://github.com/google-gemini/gemini-cli/pull/26554) [#26198](https://github.com/google-gemini/gemini-cli/pull/26198)
- fix(a2a-server): Resolve race condition in tool completion waiting in - feat(cli): add --delete flag to /exit command for session deletion in
[#26568](https://github.com/google-gemini/gemini-cli/pull/26568) [#19332](https://github.com/google-gemini/gemini-cli/pull/19332)
- fix(cli): randomize sandbox container names in - test(core): add regression test for issue for ToolConfirmationResponse in
[#26014](https://github.com/google-gemini/gemini-cli/pull/26014) [#26194](https://github.com/google-gemini/gemini-cli/pull/26194)
- fix(core): Fix hysteresis in async context management pipelines. in - Add the ability to @ mention the gemini robot. in
[#26452](https://github.com/google-gemini/gemini-cli/pull/26452) [#26207](https://github.com/google-gemini/gemini-cli/pull/26207)
- Tighten private Auto Memory patch allowlist in - test(evals): add EvalMetadata JSDoc annotations to older tests in
[#26535](https://github.com/google-gemini/gemini-cli/pull/26535) [#26147](https://github.com/google-gemini/gemini-cli/pull/26147)
- fix(cli): hide read-only settings scopes in - fix(core): reduce default API timeout to 60s and enable retries for undici
[#26249](https://github.com/google-gemini/gemini-cli/pull/26249) timeouts in [#26191](https://github.com/google-gemini/gemini-cli/pull/26191)
- fix(ci): preserve executable bit for mac binaries in - fix(core): distinguish fallback chains and fix maxAttempts for auto vs
[#26600](https://github.com/google-gemini/gemini-cli/pull/26600) explicit model selection in
- fix(cli): improve mcp list UX in untrusted folders in [#26163](https://github.com/google-gemini/gemini-cli/pull/26163)
[#26457](https://github.com/google-gemini/gemini-cli/pull/26457) - fix(cli): handle InvalidStream event gracefully without throwing in
- fix(core): prevent silent hang during OAuth auth on headless Linux in [#26218](https://github.com/google-gemini/gemini-cli/pull/26218)
[#26571](https://github.com/google-gemini/gemini-cli/pull/26571) - ci(github-actions): switch to github app token and fix bot self-trigger in
- Changelog for v0.42.0-preview.0 in [#26223](https://github.com/google-gemini/gemini-cli/pull/26223)
[#26537](https://github.com/google-gemini/gemini-cli/pull/26537) - Respect logPrompts flag for logging sensitive fields in
- ci: fix Argument list too long in triage workflows in [#26153](https://github.com/google-gemini/gemini-cli/pull/26153)
[#26603](https://github.com/google-gemini/gemini-cli/pull/26603) - fix: correct API key validation logic in handleApiKeySubmit in
- refactor(cli): migrate core tools to native ToolDisplay property and fix UI [#25453](https://github.com/google-gemini/gemini-cli/pull/25453)
rendering in [#25186](https://github.com/google-gemini/gemini-cli/pull/25186) - fix(agent): prevent exit_plan_mode from being called via shell in
- don't wrap args unnecessarily in [#26230](https://github.com/google-gemini/gemini-cli/pull/26230)
[#26599](https://github.com/google-gemini/gemini-cli/pull/26599) - # Fix: Inconsistent Case-Sensitivity in GrepTool in [#26235](https://github.com/google-gemini/gemini-cli/pull/26235)
- fix(core): preserve system PATH in Git environment to fix ENOENT (#25034) in - docs(core): add automated gemma setup guide in
[#26587](https://github.com/google-gemini/gemini-cli/pull/26587) [#26233](https://github.com/google-gemini/gemini-cli/pull/26233)
- fix(routing): fix resolveClassifierModel argument mismatch in - Allow non-https proxy urls to support container environments in
ApprovalModeStrategy in [#26234](https://github.com/google-gemini/gemini-cli/pull/26234)
[#26658](https://github.com/google-gemini/gemini-cli/pull/26658) - fix(bot): productivity and backlog optimizations in
- docs: add vi mode shortcuts and clarify MCP/custom sandbox setup in [#26236](https://github.com/google-gemini/gemini-cli/pull/26236)
[#23853](https://github.com/google-gemini/gemini-cli/pull/23853) - refactor(acp): delegate prompt turn processing logic to GeminiClient in
- fix(ux): fixed issue with transcribed text not showing after releasing space [#26222](https://github.com/google-gemini/gemini-cli/pull/26222)
in [#26609](https://github.com/google-gemini/gemini-cli/pull/26609) - fix(cli): refine platform-specific undo/redo and smart bubbling for WSL in
- ci: fix json parsing in scheduled triage workflow in [#26202](https://github.com/google-gemini/gemini-cli/pull/26202)
[#26656](https://github.com/google-gemini/gemini-cli/pull/26656) - fix: suppress duplicate extension warnings during startup in
- fix(cli): hide /memory add subcommand when memoryV2 is enabled in [#26208](https://github.com/google-gemini/gemini-cli/pull/26208)
[#26605](https://github.com/google-gemini/gemini-cli/pull/26605) - fix(cli): use byte length instead of string length for readStdin size limits
- fix: prevent false command conflicts when launching from home directory in in [#26224](https://github.com/google-gemini/gemini-cli/pull/26224)
[#23069](https://github.com/google-gemini/gemini-cli/pull/23069) - fix(ui): made shell tool header wrap on Ctrl+O in
- fix(core): cache model routing decision in LocalAgentExecutor in [#26229](https://github.com/google-gemini/gemini-cli/pull/26229)
[#26548](https://github.com/google-gemini/gemini-cli/pull/26548) - Changelog for v0.41.0-preview.0 in
- Changelog for v0.42.0-preview.2 in [#26244](https://github.com/google-gemini/gemini-cli/pull/26244)
[#26597](https://github.com/google-gemini/gemini-cli/pull/26597) - Skip binary CLI relaunch in
- skip broken test in [#26261](https://github.com/google-gemini/gemini-cli/pull/26261)
[#26705](https://github.com/google-gemini/gemini-cli/pull/26705) - fix(cli): do not override GOOGLE_CLOUD_PROJECT in Cloud Shell when using
- feat: export session to file and import via flag in Vertex AI in [#24455](https://github.com/google-gemini/gemini-cli/pull/24455)
[#26514](https://github.com/google-gemini/gemini-cli/pull/26514) - docs(cli): add skill discovery troubleshooting checklist to tutorial in
- Feat: Add Machine Hostname to CLI interface in [#26018](https://github.com/google-gemini/gemini-cli/pull/26018)
[#25637](https://github.com/google-gemini/gemini-cli/pull/25637) - docs(policy-engine): link to tools reference for tool names and args in
- docs(extensions): refactor releasing guide and add update mechanisms in [#22081](https://github.com/google-gemini/gemini-cli/pull/22081)
[#26595](https://github.com/google-gemini/gemini-cli/pull/26595) - Fix posting invalid response to a comment in
- fix(ci): fix maintainer identification in lifecycle manager in [#26266](https://github.com/google-gemini/gemini-cli/pull/26266)
[#26706](https://github.com/google-gemini/gemini-cli/pull/26706) - fix(cli): prevent informational logs from polluting json output in
- fix(ui): added quotes around session id in resume tip in [#26264](https://github.com/google-gemini/gemini-cli/pull/26264)
[#26669](https://github.com/google-gemini/gemini-cli/pull/26669) - feat(ui): added microphone and updated placeholder for voice mode in
- Changelog for v0.41.0 in [#26270](https://github.com/google-gemini/gemini-cli/pull/26270)
[#26670](https://github.com/google-gemini/gemini-cli/pull/26670) - feat(cli): Add 'list' subcommand to '/commands' in
- refactor(core): agent session protocol changes in [#22324](https://github.com/google-gemini/gemini-cli/pull/22324)
[#26661](https://github.com/google-gemini/gemini-cli/pull/26661) - fix(core): ensure tool output cleanup on session deletion for legacy files in
- fix(context): implement loose boundary policy for gc backstop. in [#26263](https://github.com/google-gemini/gemini-cli/pull/26263)
[#26594](https://github.com/google-gemini/gemini-cli/pull/26594) - Docs: Update Agent Skills documentation in
- fix(core): throw explicit error on dropped tool responses in [#22388](https://github.com/google-gemini/gemini-cli/pull/22388)
[#26668](https://github.com/google-gemini/gemini-cli/pull/26668) - test(acp): add missing coverage for extensions command error paths in
- fix: resolve "function response turn must come immediately after function [#25313](https://github.com/google-gemini/gemini-cli/pull/25313)
call" error in - Changelog for v0.40.0 in
[#26691](https://github.com/google-gemini/gemini-cli/pull/26691) [#26245](https://github.com/google-gemini/gemini-cli/pull/26245)
- fix(core): resolve parallel tool call streaming ID collision in - fix: report AgentExecutionBlocked in non-interactive programmatic modes in
[#26646](https://github.com/google-gemini/gemini-cli/pull/26646) [#26262](https://github.com/google-gemini/gemini-cli/pull/26262)
- feat(core): add LocalSubagentProtocol behind AgentProtocol in - feat(extensions): add 'delete' as an alias for /extensions uninstall in
[#25302](https://github.com/google-gemini/gemini-cli/pull/25302) [#25660](https://github.com/google-gemini/gemini-cli/pull/25660)
- fix(cli): remove noisy theme registration logs from terminal in - fix(core): silently skip GEMINI.md paths that are directories (EISDIR) in
[#25858](https://github.com/google-gemini/gemini-cli/pull/25858) [#25662](https://github.com/google-gemini/gemini-cli/pull/25662)
- ci: implement codebase-aware effort level triage in - fix(ci): checkout PR branch instead of main in bot workflow in
[#26666](https://github.com/google-gemini/gemini-cli/pull/26666) [#26289](https://github.com/google-gemini/gemini-cli/pull/26289)
- feat(acp/core): prefix tool call IDs with tool names to support tool rendering - fix(cli): use resolved sandbox state for auto-update check in
in ACP compliant IDEs. in [#26285](https://github.com/google-gemini/gemini-cli/pull/26285)
[#26676](https://github.com/google-gemini/gemini-cli/pull/26676) - # Metrics Integrity & Standardized Reporting (BT-01) in [#26240](https://github.com/google-gemini/gemini-cli/pull/26240)
- fix(mcp): treat GET 404 as 405 in StreamableHTTPClientTransport in - Add Star History section to README in
[#24847](https://github.com/google-gemini/gemini-cli/pull/24847) [#26290](https://github.com/google-gemini/gemini-cli/pull/26290)
- feat(core): add RemoteSubagentProtocol behind AgentProtocol in - Add Star History section to README in
[#25303](https://github.com/google-gemini/gemini-cli/pull/25303) [#26308](https://github.com/google-gemini/gemini-cli/pull/26308)
- feat(context): Improvements to the snapshotter. in - Remove Star History section from README in
[#26655](https://github.com/google-gemini/gemini-cli/pull/26655) [#26309](https://github.com/google-gemini/gemini-cli/pull/26309)
- fix(context): Change snapshotter model config. in - test(evals): add behavioral eval for file creation and write_file tool
[#26745](https://github.com/google-gemini/gemini-cli/pull/26745) selection in [#26292](https://github.com/google-gemini/gemini-cli/pull/26292)
- fix(cli): allow installing extensions from ssh repo in - feat(config): enable Gemma 4 models by default via Gemini API in
[#26274](https://github.com/google-gemini/gemini-cli/pull/26274) [#26307](https://github.com/google-gemini/gemini-cli/pull/26307)
- fix(cli): prevent duplicate SessionStart systemMessage render in - fix(cli): insert voice transcription at cursor position instead of ap… in
[#25827](https://github.com/google-gemini/gemini-cli/pull/25827) [#26287](https://github.com/google-gemini/gemini-cli/pull/26287)
- fix(cli/acp): prevent infinite thought loop in ACP mode by disablig - fix(ui): fix issue with box edges in
nextSpeakerCheck in [#26148](https://github.com/google-gemini/gemini-cli/pull/26148)
[#26874](https://github.com/google-gemini/gemini-cli/pull/26874) - fix(cli): respect .env override for GOOGLE_CLOUD_PROJECT in
- fix(cli): use static tool name in confirmation prompt to avoid parsing errors [#26288](https://github.com/google-gemini/gemini-cli/pull/26288)
in [#26866](https://github.com/google-gemini/gemini-cli/pull/26866) - fix(ci): robust version checking in release verification in
- fix(routing): Refactor tool turn handling for the conversation history in [#26337](https://github.com/google-gemini/gemini-cli/pull/26337)
NumericalClassifierStrategy to prevent 400 Bad Request in - fix(cli): enable daemon relaunch in binary and bundle keytar in
[#26761](https://github.com/google-gemini/gemini-cli/pull/26761) [#26333](https://github.com/google-gemini/gemini-cli/pull/26333)
- fix(core): handle malformed projects.json in ProjectRegistry in - fix(core): discourage unprompted git add . in prompt snippets in
[#26885](https://github.com/google-gemini/gemini-cli/pull/26885) [#26220](https://github.com/google-gemini/gemini-cli/pull/26220)
- fix(ui): added a gutter width to the input prompt width calculation in - feat(ui): added wave animation for voice mode in
[#26882](https://github.com/google-gemini/gemini-cli/pull/26882) [#26284](https://github.com/google-gemini/gemini-cli/pull/26284)
- fix: prevent EISDIR crash when customIgnoreFilePaths contains directories - fix(cli): prevent Escape from clearing input buffer (#17083) in
(#19868) in [#19898](https://github.com/google-gemini/gemini-cli/pull/19898) [#26339](https://github.com/google-gemini/gemini-cli/pull/26339)
- revert 6b9b778d821728427eea07b1b97ba07378137d0b in - fix(cli): undeprecate --prompt and correct positional query docs in
[#26893](https://github.com/google-gemini/gemini-cli/pull/26893) [#26329](https://github.com/google-gemini/gemini-cli/pull/26329)
- Fix/vscode run current file ts in - Metrics updates in
[#22894](https://github.com/google-gemini/gemini-cli/pull/22894) [#26348](https://github.com/google-gemini/gemini-cli/pull/26348)
- Allow Enter to select session while in search mode in /resume in - fix(core): remove "System: Please continue." injection on InvalidStream events
[#21523](https://github.com/google-gemini/gemini-cli/pull/21523) in [#26340](https://github.com/google-gemini/gemini-cli/pull/26340)
- fix(core): ignore .pak and .rpa game archive formats by default in - docs(policy-engine): add tool argument keys reference and shell policy
[#26884](https://github.com/google-gemini/gemini-cli/pull/26884) cross-links in
- fix(cli): enable adk non-interactive session in [#25292](https://github.com/google-gemini/gemini-cli/pull/25292)
[#26895](https://github.com/google-gemini/gemini-cli/pull/26895) - fix(cli): resolve Ghostty/raw-mode False Cancellation in oauth flow in
- fix(cli): restore resume for legacy sessions in [#25026](https://github.com/google-gemini/gemini-cli/pull/25026)
[#26577](https://github.com/google-gemini/gemini-cli/pull/26577) - fix(core): reset session-scoped state on resumption in
- fix: respect explicit model selection after Flash quota exhaustion (#26759) in [#26342](https://github.com/google-gemini/gemini-cli/pull/26342)
[#26872](https://github.com/google-gemini/gemini-cli/pull/26872) - Fix bulk of remaining issues with generalist profile in
- feat(context): Introduce adaptive token calculator to more accurately [#26073](https://github.com/google-gemini/gemini-cli/pull/26073)
calculate content sizes. in - fix(core): make subagents aware of active approval modes in
[#26888](https://github.com/google-gemini/gemini-cli/pull/26888) [#23608](https://github.com/google-gemini/gemini-cli/pull/23608)
- chore: update checkout action configuration in workflows in - fix(acp): resolve agent mode disconnect and improve mode awareness in
[#26897](https://github.com/google-gemini/gemini-cli/pull/26897) [#26332](https://github.com/google-gemini/gemini-cli/pull/26332)
- fix (telemetry): inject quota_project_id to prevent fallback to default oauth - docs(sdk): add JSDoc to exported interfaces in packages/sdk/src/types.ts in
client in [#26698](https://github.com/google-gemini/gemini-cli/pull/26698) [#26441](https://github.com/google-gemini/gemini-cli/pull/26441)
- Exclude extension context from skill extraction agent in - perf: skip redundant GEMINI.md loading in partialConfig in
[#26879](https://github.com/google-gemini/gemini-cli/pull/26879) [#26443](https://github.com/google-gemini/gemini-cli/pull/26443)
- Enable NumericalRouter when using dynamic model configs in - Enhance React guidelines in
[#26929](https://github.com/google-gemini/gemini-cli/pull/26929) [#22667](https://github.com/google-gemini/gemini-cli/pull/22667)
- ci: actively triage missing priority labels and intelligently clean up - feat(core): reinforce Inquiry constraints to prevent unauthorized changes in
conflicting labels in [#26310](https://github.com/google-gemini/gemini-cli/pull/26310)
[#26865](https://github.com/google-gemini/gemini-cli/pull/26865) - revert: fix(ci): robust version checking in release verification (#26337) in
- refactor(core): introduce SubagentState enum for progress in [#26450](https://github.com/google-gemini/gemini-cli/pull/26450)
[#26934](https://github.com/google-gemini/gemini-cli/pull/26934) - refactor(UI): created constants file for ThemeDialog in
- fix(ci): replace brittle --no-tag with explicit staging-tmp tag in [#26446](https://github.com/google-gemini/gemini-cli/pull/26446)
[#26940](https://github.com/google-gemini/gemini-cli/pull/26940) - docs: fix GitHub capitalization in releases guide in
- Incremental refactor repo agent towards skills-based composition in [#26379](https://github.com/google-gemini/gemini-cli/pull/26379)
[#26717](https://github.com/google-gemini/gemini-cli/pull/26717) - fix(cli): ensure branch indicator updates in sub-directories and worktrees in
- fix(ui): fixed line wrap padding for selection lists in [#26330](https://github.com/google-gemini/gemini-cli/pull/26330)
[#26944](https://github.com/google-gemini/gemini-cli/pull/26944) - feat: add minimal V8 heap snapshot utility for memory diagnostics in
- fix(core): update read_file schema for v1 compatibility (#22183) in [#26440](https://github.com/google-gemini/gemini-cli/pull/26440)
[#26922](https://github.com/google-gemini/gemini-cli/pull/26922) - fix(hooks): preserve non-text parts in fromHookLLMRequest in
- fix(ci): configure git remote with token for authentication in [#26275](https://github.com/google-gemini/gemini-cli/pull/26275)
[#26949](https://github.com/google-gemini/gemini-cli/pull/26949) - fix(cli): allow early stdout when config is undefined in
[#26453](https://github.com/google-gemini/gemini-cli/pull/26453)
- fix(cli)#21297: clear skills consent dialog before reload in
[#26431](https://github.com/google-gemini/gemini-cli/pull/26431)
- fix(cli): render LaTeX-style output as Unicode in the TUI in
[#25802](https://github.com/google-gemini/gemini-cli/pull/25802)
- fix(core): use close event instead of exit in child_process fallback in
[#25695](https://github.com/google-gemini/gemini-cli/pull/25695)
- feat(voice): add privacy and compliance UX warning for Gemini Live backend in
[#26454](https://github.com/google-gemini/gemini-cli/pull/26454)
- feat(memory): add Auto Memory inbox flow with canonical-patch contract in
[#26338](https://github.com/google-gemini/gemini-cli/pull/26338)
- test(cleanup): fix temporary directory leaks in test suites in
[#26217](https://github.com/google-gemini/gemini-cli/pull/26217)
- feat: add ignoreLocalEnv setting and --ignore-env flag (#2493) in
[#26445](https://github.com/google-gemini/gemini-cli/pull/26445)
- docs(sdk): add JSDoc to all exported interfaces and types in
[#26277](https://github.com/google-gemini/gemini-cli/pull/26277)
- feat(cli): improve /agents refresh logging in
[#26442](https://github.com/google-gemini/gemini-cli/pull/26442)
- Fix: make Dockerfile self-contained with multi-stage build in
[#24277](https://github.com/google-gemini/gemini-cli/pull/24277)
- fix(core): filter unsupported multimodal types from tool responses in
[#26352](https://github.com/google-gemini/gemini-cli/pull/26352)
- fix(core): properly format markdown in AskUser tool by unescaping newlines in
[#26349](https://github.com/google-gemini/gemini-cli/pull/26349)
- feat(bot): add actions spend metric script in
[#26463](https://github.com/google-gemini/gemini-cli/pull/26463)
- feat(cli): add /bug-memory command and auto-capture heap snapshot in /bug in
[#25639](https://github.com/google-gemini/gemini-cli/pull/25639)
- fix(cli): make SkillInboxDialog fit and scroll in alternate buffer in
[#26455](https://github.com/google-gemini/gemini-cli/pull/26455)
- Robust Scale-Safe Lifecycle Consolidation in
[#26355](https://github.com/google-gemini/gemini-cli/pull/26355)
- fix(ci): respect exempt labels when closing stale items in
[#26475](https://github.com/google-gemini/gemini-cli/pull/26475)
- fix(cli): use os.homedir() for home directory warning check in
[#25890](https://github.com/google-gemini/gemini-cli/pull/25890)
- fix(a2a-server): resolve tool approval race condition and improve status
reporting in [#26479](https://github.com/google-gemini/gemini-cli/pull/26479)
- fix(cli): prevent settings dialog border clipping using maxHeight in
[#26507](https://github.com/google-gemini/gemini-cli/pull/26507)
- feat: allow queuing messages during compression (#24071) in
[#26506](https://github.com/google-gemini/gemini-cli/pull/26506)
- fix(core): retry on ERR_STREAM_PREMATURE_CLOSE errors in
[#26519](https://github.com/google-gemini/gemini-cli/pull/26519)
- fix(core): Minor fixes for generalist profile. in
[#26357](https://github.com/google-gemini/gemini-cli/pull/26357)
**Full Changelog**: **Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.42.0-preview.2...v0.43.0-preview.0 https://github.com/google-gemini/gemini-cli/compare/v0.41.0-preview.3...v0.42.0-preview.0
+5 -4
View File
@@ -29,10 +29,11 @@ You'll use Auto Memory when you want to:
avoid them. avoid them.
- **Bootstrap a skills library** without writing every `SKILL.md` by hand. - **Bootstrap a skills library** without writing every `SKILL.md` by hand.
Auto Memory complements direct memory-file editing. The agent can still persist Auto Memory complements—but does not replace—the
explicit user instructions by editing the appropriate Markdown memory file; Auto [`save_memory` tool](../tools/memory.md), which captures single facts into
Memory infers candidates from past sessions, writes reviewable patches or skill `GEMINI.md` when the agent explicitly calls it. Auto Memory infers candidates
drafts, and never applies them without your approval. from past sessions, writes reviewable patches or skill drafts, and never applies
them without your approval.
## Prerequisites ## Prerequisites
+2
View File
@@ -65,6 +65,8 @@ You can interact with the loaded context files by using the `/memory` command.
being provided to the model. being provided to the model.
- **`/memory reload`**: Forces a re-scan and reload of all `GEMINI.md` files - **`/memory reload`**: Forces a re-scan and reload of all `GEMINI.md` files
from all configured locations. from all configured locations.
- **`/memory add <text>`**: Appends your text to your global
`~/.gemini/GEMINI.md` file. This lets you add persistent memories on the fly.
## Modularize context with imports ## Modularize context with imports
+1
View File
@@ -138,6 +138,7 @@ These are the only allowed tools:
[`replace`](../tools/file-system.md#6-replace-edit) only allowed for `.md` [`replace`](../tools/file-system.md#6-replace-edit) only allowed for `.md`
files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory or your files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory or your
[custom plans directory](#custom-plan-directory-and-policies). [custom plans directory](#custom-plan-directory-and-policies).
- **Memory:** [`save_memory`](../tools/memory.md)
- **Skills:** [`activate_skill`](../cli/skills.md) (allows loading specialized - **Skills:** [`activate_skill`](../cli/skills.md) (allows loading specialized
instructions and resources in a read-only manner) instructions and resources in a read-only manner)
+19 -19
View File
@@ -40,7 +40,6 @@ they appear in the UI.
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` | | Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` | | Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` |
| Topic & Update Narration | `general.topicUpdateNarration` | Enable the Topic & Update communication model for reduced chattiness and structured progress reporting. | `true` | | Topic & Update Narration | `general.topicUpdateNarration` | Enable the Topic & Update communication model for reduced chattiness and structured progress reporting. | `true` |
| Log RAG Snippets | `general.logRagSnippets` | Log full Code Customization (RAG) retrieved snippets to a local file for debugging. | `false` |
### Output ### Output
@@ -163,24 +162,25 @@ they appear in the UI.
### Experimental ### Experimental
| UI Label | Setting | Description | Default | | UI Label | Setting | Description | Default |
| ---------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | ---------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| Gemma Models | `experimental.gemma` | Enable access to Gemma 4 models via Gemini API. | `true` | | Gemma Models | `experimental.gemma` | Enable access to Gemma 4 models via Gemini API. | `true` |
| Voice Mode | `experimental.voiceMode` | Enable experimental voice dictation and commands (/voice, /voice model). | `false` | | Voice Mode | `experimental.voiceMode` | Enable experimental voice dictation and commands (/voice, /voice model). | `false` |
| Voice Activation Mode | `experimental.voice.activationMode` | How to trigger voice recording with the Space key. | `"push-to-talk"` | | Voice Activation Mode | `experimental.voice.activationMode` | How to trigger voice recording with the Space key. | `"push-to-talk"` |
| Voice Transcription Backend | `experimental.voice.backend` | The backend to use for voice transcription. Note: When using the Gemini Live backend, voice recordings are sent to Google Cloud for transcription. | `"gemini-live"` | | Voice Transcription Backend | `experimental.voice.backend` | The backend to use for voice transcription. Note: When using the Gemini Live backend, voice recordings are sent to Google Cloud for transcription. | `"gemini-live"` |
| Whisper Model | `experimental.voice.whisperModel` | The Whisper model to use for local transcription. | `"ggml-base.en.bin"` | | Whisper Model | `experimental.voice.whisperModel` | The Whisper model to use for local transcription. | `"ggml-base.en.bin"` |
| Voice Stop Grace Period (ms) | `experimental.voice.stopGracePeriodMs` | How long to wait for final transcription after stopping recording. | `4000` | | Voice Stop Grace Period (ms) | `experimental.voice.stopGracePeriodMs` | How long to wait for final transcription after stopping recording. | `1000` |
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` | | Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` | | Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` | | Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` | | Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` | | Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
| Enable Gemma Model Router | `experimental.gemmaModelRouter.enabled` | Enable the Gemma Model Router (experimental). Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim. | `false` | | Enable Gemma Model Router | `experimental.gemmaModelRouter.enabled` | Enable the Gemma Model Router (experimental). Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim. | `false` |
| Auto-start LiteRT Server | `experimental.gemmaModelRouter.autoStartServer` | Automatically start the LiteRT-LM server when Gemini CLI starts and the Gemma router is enabled. | `false` | | Auto-start LiteRT Server | `experimental.gemmaModelRouter.autoStartServer` | Automatically start the LiteRT-LM server when Gemini CLI starts and the Gemma router is enabled. | `false` |
| Auto Memory | `experimental.autoMemory` | Automatically extract memory patches and skills from past sessions in the background. Every change is written as a unified diff `.patch` file under `<projectMemoryDir>/.inbox/<kind>/` and held for review in /memory inbox; nothing is applied until you approve it. | `false` | | Memory v2 | `experimental.memoryV2` | Disable the built-in save_memory tool and let the main agent persist project context by editing markdown files directly with edit/write_file. Route facts across four tiers: team-shared conventions go to project GEMINI.md files, project-specific personal notes go to the per-project private memory folder (MEMORY.md as index + sibling .md files for detail), and cross-project personal preferences go to the global ~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit — settings, credentials, etc. remain off-limits). Set to false to fall back to the legacy save_memory tool. | `true` |
| Use the generalist profile to manage agent contexts. | `experimental.generalistProfile` | Suitable for general coding and software development tasks. | `false` | | Auto Memory | `experimental.autoMemory` | Automatically extract memory patches and skills from past sessions in the background. Every change is written as a unified diff `.patch` file under `<projectMemoryDir>/.inbox/<kind>/` and held for review in /memory inbox; nothing is applied until you approve it. | `false` |
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` | | Use the generalist profile to manage agent contexts. | `experimental.generalistProfile` | Suitable for general coding and software development tasks. | `false` |
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
### Skills ### Skills
+2 -2
View File
@@ -71,8 +71,8 @@ Just tell the agent to remember something.
**Prompt:** `Remember that I prefer using 'const' over 'let' wherever possible.` **Prompt:** `Remember that I prefer using 'const' over 'let' wherever possible.`
The agent will edit the appropriate memory Markdown file, so the fact is loaded The agent will use the `save_memory` tool to store this fact in your global
in future sessions. memory file.
**Prompt:** `Save the fact that the staging server IP is 10.0.0.5.` **Prompt:** `Save the fact that the staging server IP is 10.0.0.5.`
-16
View File
@@ -210,22 +210,6 @@ To update an extension's settings:
gemini extensions config <name> [setting] [--scope <scope>] gemini extensions config <name> [setting] [--scope <scope>]
``` ```
#### Environment variable sanitization
For security reasons, sensitive environment variables are filtered out and not
passed to extensions or MCP servers by default.
Extensions **will not** inherit the user's full shell environment variables.
They will only have access to:
1. Standard safe variables (e.g., `HOME`, `PATH`, `TMPDIR`).
2. Variables explicitly declared and requested in the `gemini-extension.json`
manifest via the `settings` array (using the `envVar` property).
If your extension requires specific environment variables (like an API key,
custom host, or config path), you **must** declare them in the `settings` array
so the CLI can allowlist them for use within the extension.
### Custom commands ### Custom commands
Provide [custom commands](../cli/custom-commands.md) by placing TOML files in a Provide [custom commands](../cli/custom-commands.md) by placing TOML files in a
+23 -57
View File
@@ -1,8 +1,7 @@
# Release extensions # Release extensions
Release Gemini CLI extensions to your users through a Git repository or GitHub Release Gemini CLI extensions to your users through a Git repository or GitHub
Releases. This guide explains how to share your work, list it in the gallery, Releases.
and manage updates.
Git repository releases are the simplest approach and offer the most flexibility Git repository releases are the simplest approach and offer the most flexibility
for managing development branches. GitHub Releases are more efficient for for managing development branches. GitHub Releases are more efficient for
@@ -154,62 +153,29 @@ jobs:
release/win32.arm64.my-tool.zip release/win32.arm64.my-tool.zip
``` ```
## Migrate an extension repository ## Migrating an Extension Repository
If you move your extension to a new repository or rename it, use the If you need to move your extension to a new repository (for example, from a
`migratedTo` property in `gemini-extension.json` to seamlessly transition your personal account to an organization) or rename it, you can use the `migratedTo`
property in your `gemini-extension.json` file to seamlessly transition your
users. users.
1. **Create the new repository:** Set up your extension in its new location. 1. **Create the new repository**: Setup your extension in its new location.
2. **Update the old repository:** In your original repository, update the 2. **Update the old repository**: In your original repository, update the
`gemini-extension.json` file to include the `migratedTo` property pointing `gemini-extension.json` file to include the `migratedTo` property, pointing
to the new repository URL, and increment the version number. to the new repository URL, and bump the version number. You can optionally
```json change the `name` of your extension at this time in the new repository.
{ ```json
"name": "my-extension", {
"version": "1.1.0", "name": "my-extension",
"migratedTo": "https://github.com/new-owner/new-extension-repo" "version": "1.1.0",
} "migratedTo": "https://github.com/new-owner/new-extension-repo"
``` }
3. **Release the update:** Publish this new version in your old repository. ```
3. **Release the update**: Publish this new version in your old repository.
When users check for updates, Gemini CLI detects the `migratedTo` field, When users check for updates, Gemini CLI will detect the `migratedTo` field,
verifies the new repository, and automatically updates their local installation verify that the new repository contains a valid extension update, and
to track the new source. All settings migrate automatically. automatically update their local installation to track the new source and name
moving forward. All extension settings will automatically migrate to the new
## How updates work installation.
Gemini CLI automatically checks for extension updates based on the installation
method. Understanding these mechanisms helps you ensure your users always have
the latest version.
### Sync manifest and tags
For GitHub releases, always ensure the `version` in `gemini-extension.json`
matches your GitHub release tag. While the CLI uses tags for update detection,
it displays the manifest version in the UI. Keeping them in sync prevents
confusion.
### Update mechanisms
<details>
<summary>Technical update details</summary>
The CLI uses different strategies depending on the installation type:
- **GitHub releases:** The CLI queries the GitHub API for the latest release
tag. It ignores the `version` field in the manifest for detection.
- **Git clones:** The CLI runs `git ls-remote` to compare the latest remote
commit hash with your local `HEAD`.
- **Local extensions:** The CLI compares the `version` field in the source
directory's manifest with the installed version.
To verify an extension's installation type, inspect the `type` field in the
metadata file at `~/.gemini/extensions/<name>/.gemini-extension-install.json`.
</details>
<!-- prettier-ignore -->
> [!IMPORTANT]
> The `migratedTo` flow requires at least one release on the new repository for
> the CLI to recognize it as a valid update source.
-7
View File
@@ -159,13 +159,6 @@ When a user installs this extension, Gemini CLI will prompt them to enter the
`sensitive` is true) and injected into the MCP server's process as the `sensitive` is true) and injected into the MCP server's process as the
`MY_SERVICE_API_KEY` environment variable. `MY_SERVICE_API_KEY` environment variable.
> **Important (Environment Variable Sanitization):** For security reasons,
> sensitive environment variables are filtered out and not passed to extensions
> or MCP servers by default. Extensions will _only_ have access to environment
> variables that are explicitly declared in the `settings` array using the
> `envVar` property, plus a few standard safe variables. Do not expect host
> environment variables to be available otherwise.
## Step 4: Link your extension ## Step 4: Link your extension
Link your extension to your Gemini CLI installation for local development. Link your extension to your Gemini CLI installation for local development.
+2 -2
View File
@@ -111,8 +111,8 @@ You can also run Gemini CLI using one of the following advanced methods:
directly. This is useful for environments where you only have Docker and want directly. This is useful for environments where you only have Docker and want
to run the CLI. to run the CLI.
```bash ```bash
# Run the published sandbox image for a specified CLI version # Run the published sandbox image
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.42.0-nightly.20260428.g59b2dea0e docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
``` ```
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally - **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
(using the standard installation described above), you can instruct it to run (using the standard installation described above), you can instruct it to run
+3
View File
@@ -265,6 +265,9 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Manage the AI's instructional context (hierarchical memory - **Description:** Manage the AI's instructional context (hierarchical memory
loaded from `GEMINI.md` files). loaded from `GEMINI.md` files).
- **Sub-commands:** - **Sub-commands:**
- **`add`**:
- **Description:** Adds the following text to the AI's memory. Usage:
`/memory add <text to remember>`
- **`list`**: - **`list`**:
- **Description:** Lists the paths of the GEMINI.md files in use for - **Description:** Lists the paths of the GEMINI.md files in use for
hierarchical memory. hierarchical memory.
+63 -71
View File
@@ -203,11 +203,6 @@ their corresponding top-level category object in your `settings.json` file.
chattiness and structured progress reporting. chattiness and structured progress reporting.
- **Default:** `true` - **Default:** `true`
- **`general.logRagSnippets`** (boolean):
- **Description:** Log full Code Customization (RAG) retrieved snippets to a
local file for debugging.
- **Default:** `false`
#### `output` #### `output`
- **`output.format`** (enum): - **`output.format`** (enum):
@@ -705,19 +700,6 @@ their corresponding top-level category object in your `settings.json` file.
"extends": "gemini-3-flash-base", "extends": "gemini-3-flash-base",
"modelConfig": {} "modelConfig": {}
}, },
"context-snapshotter": {
"extends": "gemini-3-flash-base",
"modelConfig": {
"generateContentConfig": {
"thinkingConfig": {
"thinkingLevel": "HIGH"
},
"temperature": 1,
"topP": 0.95,
"topK": 64
}
}
},
"chat-compression-3-pro": { "chat-compression-3-pro": {
"modelConfig": { "modelConfig": {
"model": "gemini-3-pro-preview" "model": "gemini-3-pro-preview"
@@ -887,10 +869,9 @@ their corresponding top-level category object in your `settings.json` file.
} }
}, },
"auto": { "auto": {
"displayName": "Auto",
"tier": "auto", "tier": "auto",
"isPreview": true, "isPreview": true,
"isVisible": true, "isVisible": false,
"features": { "features": {
"thinking": true, "thinking": true,
"multimodalToolUse": false "multimodalToolUse": false
@@ -924,16 +905,26 @@ their corresponding top-level category object in your `settings.json` file.
} }
}, },
"auto-gemini-3": { "auto-gemini-3": {
"displayName": "Auto (Gemini 3)",
"tier": "auto", "tier": "auto",
"family": "gemini-3",
"isPreview": true, "isPreview": true,
"isVisible": false "isVisible": true,
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash",
"features": {
"thinking": true,
"multimodalToolUse": false
}
}, },
"auto-gemini-2.5": { "auto-gemini-2.5": {
"displayName": "Auto (Gemini 2.5)",
"tier": "auto", "tier": "auto",
"family": "gemini-2.5",
"isPreview": false, "isPreview": false,
"isVisible": false "isVisible": true,
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash",
"features": {
"thinking": false,
"multimodalToolUse": false
}
} }
} }
``` ```
@@ -1016,15 +1007,33 @@ their corresponding top-level category object in your `settings.json` file.
} }
] ]
}, },
"auto": { "auto-gemini-3": {
"default": "gemini-3-pro-preview", "default": "gemini-3-pro-preview",
"contexts": [ "contexts": [
{ {
"condition": { "condition": {
"releaseChannel": "stable" "hasAccessToPreview": false
}, },
"target": "gemini-2.5-pro" "target": "gemini-2.5-pro"
}, },
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto": {
"default": "gemini-3-pro-preview",
"contexts": [
{ {
"condition": { "condition": {
"hasAccessToPreview": false "hasAccessToPreview": false
@@ -1070,6 +1079,9 @@ their corresponding top-level category object in your `settings.json` file.
} }
] ]
}, },
"auto-gemini-2.5": {
"default": "gemini-2.5-pro"
},
"gemini-3.1-flash-lite-preview": { "gemini-3.1-flash-lite-preview": {
"default": "gemini-3.1-flash-lite-preview", "default": "gemini-3.1-flash-lite-preview",
"contexts": [ "contexts": [
@@ -1102,33 +1114,6 @@ their corresponding top-level category object in your `settings.json` file.
"target": "gemini-3.1-flash-lite-preview" "target": "gemini-3.1-flash-lite-preview"
} }
] ]
},
"auto-gemini-3": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto-gemini-2.5": {
"default": "gemini-2.5-pro"
} }
} }
``` ```
@@ -1147,15 +1132,15 @@ their corresponding top-level category object in your `settings.json` file.
"contexts": [ "contexts": [
{ {
"condition": { "condition": {
"hasAccessToPreview": false "requestedModels": ["auto-gemini-2.5", "gemini-2.5-pro"]
}, },
"target": "gemini-2.5-flash" "target": "gemini-2.5-flash"
}, },
{ {
"condition": { "condition": {
"requestedModels": ["gemini-2.5-pro", "auto-gemini-2.5"] "requestedModels": ["auto-gemini-3", "gemini-3-pro-preview"]
}, },
"target": "gemini-2.5-flash" "target": "gemini-3-flash-preview"
} }
] ]
}, },
@@ -1164,20 +1149,7 @@ their corresponding top-level category object in your `settings.json` file.
"contexts": [ "contexts": [
{ {
"condition": { "condition": {
"hasAccessToPreview": false "requestedModels": ["auto-gemini-2.5", "gemini-2.5-pro"]
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"releaseChannel": "stable",
"requestedModels": ["auto"]
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"requestedModels": ["gemini-2.5-pro", "auto-gemini-2.5"]
}, },
"target": "gemini-2.5-pro" "target": "gemini-2.5-pro"
}, },
@@ -1823,7 +1795,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.voice.stopGracePeriodMs`** (number): - **`experimental.voice.stopGracePeriodMs`** (number):
- **Description:** How long to wait for final transcription after stopping - **Description:** How long to wait for final transcription after stopping
recording. recording.
- **Default:** `4000` - **Default:** `1000`
- **`experimental.adk.agentSessionNoninteractiveEnabled`** (boolean): - **`experimental.adk.agentSessionNoninteractiveEnabled`** (boolean):
- **Description:** Enable non-interactive agent sessions. - **Description:** Enable non-interactive agent sessions.
@@ -1872,6 +1844,13 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false` - **Default:** `false`
- **Requires restart:** Yes - **Requires restart:** Yes
- **`experimental.jitContext`** (boolean):
- **Description:** Enable Just-In-Time (JIT) context loading. Defaults to
true; set to false to opt out and load all GEMINI.md files into the system
instruction up-front.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.useOSC52Paste`** (boolean): - **`experimental.useOSC52Paste`** (boolean):
- **Description:** Use OSC 52 for pasting. This may be more robust than the - **Description:** Use OSC 52 for pasting. This may be more robust than the
default system when using remote terminal sessions (if your terminal is default system when using remote terminal sessions (if your terminal is
@@ -1934,6 +1913,19 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `"gemma3-1b-gpu-custom"` - **Default:** `"gemma3-1b-gpu-custom"`
- **Requires restart:** Yes - **Requires restart:** Yes
- **`experimental.memoryV2`** (boolean):
- **Description:** Disable the built-in save_memory tool and let the main
agent persist project context by editing markdown files directly with
edit/write_file. Route facts across four tiers: team-shared conventions go
to project GEMINI.md files, project-specific personal notes go to the
per-project private memory folder (MEMORY.md as index + sibling .md files
for detail), and cross-project personal preferences go to the global
~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit
— settings, credentials, etc. remain off-limits). Set to false to fall back
to the legacy save_memory tool.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.stressTestProfile`** (boolean): - **`experimental.stressTestProfile`** (boolean):
- **Description:** Significantly lowers token limits to force early garbage - **Description:** Significantly lowers token limits to force early garbage
collection and distillation for testing purposes. collection and distillation for testing purposes.
-23
View File
@@ -326,29 +326,6 @@ lines and `3w` moves forward three words.
Counts are also supported for editing commands. For example, `3dd` deletes three Counts are also supported for editing commands. For example, `3dd` deletes three
lines and `2cw` changes two words. lines and `2cw` changes two words.
### Find, replace, yank, and paste in NORMAL mode
| Action | Keys |
| ----------------------------------------- | ----------- |
| Find next matching character | `f{char}` |
| Find previous matching character | `F{char}` |
| Move until before next matching character | `t{char}` |
| Move until after previous matching char | `T{char}` |
| Repeat latest character find | `;` |
| Repeat latest character find in reverse | `,` |
| Delete character before cursor | `X` |
| Toggle case under cursor | `~` |
| Replace character under cursor | `r{char}` |
| Yank line | `yy` |
| Yank to end of line | `Y` or `y$` |
| Yank word / WORD | `yw`, `yW` |
| Yank to end of word / WORD | `ye`, `yE` |
| Paste after cursor | `p` |
| Paste before cursor | `P` |
Delete and change operators also compose with character-find motions, so
commands such as `dfx`, `dtx`, `cFx`, and `cTx` are supported.
## Limitations ## Limitations
- On [Windows Terminal](https://en.wikipedia.org/wiki/Windows_Terminal): - On [Windows Terminal](https://en.wikipedia.org/wiki/Windows_Terminal):
+2
View File
@@ -120,6 +120,7 @@ each tool.
| :----------------------------------------------- | :------ | :----------------------------------------------------------------------------------- | | :----------------------------------------------- | :------ | :----------------------------------------------------------------------------------- |
| [`activate_skill`](../tools/activate-skill.md) | `Other` | Loads specialized procedural expertise from the `.gemini/skills` directory. | | [`activate_skill`](../tools/activate-skill.md) | `Other` | Loads specialized procedural expertise from the `.gemini/skills` directory. |
| [`get_internal_docs`](../tools/internal-docs.md) | `Think` | Accesses Gemini CLI's own documentation for accurate answers about its capabilities. | | [`get_internal_docs`](../tools/internal-docs.md) | `Think` | Accesses Gemini CLI's own documentation for accurate answers about its capabilities. |
| [`save_memory`](../tools/memory.md) | `Think` | Persists specific facts and project details to your `GEMINI.md` file. |
### Planning ### Planning
@@ -172,6 +173,7 @@ representation of each tool's arguments.
| `replace` | `file_path`, `old_string`, `new_string`, `instruction`, `allow_multiple` | | `replace` | `file_path`, `old_string`, `new_string`, `instruction`, `allow_multiple` |
| `ask_user` | `questions` (array of `question`, `header`, `type`, `options`) | | `ask_user` | `questions` (array of `question`, `header`, `type`, `options`) |
| `write_todos` | `todos` (array of `description`, `status`) | | `write_todos` | `todos` (array of `description`, `status`) |
| `save_memory` | `fact` |
| `activate_skill` | `name` | | `activate_skill` | `name` |
| `get_internal_docs` | `path` | | `get_internal_docs` | `path` |
| `enter_plan_mode` | `reason` | | `enter_plan_mode` | `reason` |
+3 -24
View File
@@ -221,10 +221,8 @@ spawning MCP server processes.
#### Automatic redaction #### Automatic redaction
By default, the CLI redacts sensitive environment variables from the base By default, the CLI redacts sensitive environment variables from the base
environment (inherited from the host process). This prevents the accidental environment (inherited from the host process) to prevent unintended exposure to
leakage of sensitive host environment variables (like AWS keys or GitHub tokens) third-party MCP servers. This includes:
to arbitrary third-party MCP servers that might execute malicious code or log
your environment. This includes:
- Core project keys: `GEMINI_API_KEY`, `GOOGLE_API_KEY`, etc. - Core project keys: `GEMINI_API_KEY`, `GOOGLE_API_KEY`, etc.
- Variables matching sensitive patterns: `*TOKEN*`, `*SECRET*`, `*PASSWORD*`, - Variables matching sensitive patterns: `*TOKEN*`, `*SECRET*`, `*PASSWORD*`,
@@ -234,8 +232,7 @@ your environment. This includes:
#### Explicit overrides #### Explicit overrides
If an environment variable must be passed to an MCP server, you must explicitly If an environment variable must be passed to an MCP server, you must explicitly
state it in the `env` property of the server configuration in `settings.json` state it in the `env` property of the server configuration in `settings.json`.
(or `mcp_config.json` if configuring standard MCP clients or remote skills).
Explicitly defined variables (including those from extensions) are trusted and Explicitly defined variables (including those from extensions) are trusted and
are **not** subjected to the automatic redaction process. are **not** subjected to the automatic redaction process.
@@ -250,24 +247,6 @@ specific data with that server.
> (for example, `"MY_KEY": "$MY_KEY"`) to securely pull the value from your host > (for example, `"MY_KEY": "$MY_KEY"`) to securely pull the value from your host
> environment at runtime. > environment at runtime.
**Example: Passing a GitHub Token securely to the
[official GitHub MCP server](https://github.com/github/github-mcp-server) via
`mcp_config.json`**
```json
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@github/github-mcp-server"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_PERSONAL_ACCESS_TOKEN"
}
}
}
}
```
### OAuth support for remote MCP servers ### OAuth support for remote MCP servers
Gemini CLI supports OAuth 2.0 authentication for remote MCP servers using SSE or Gemini CLI supports OAuth 2.0 authentication for remote MCP servers using SSE or
+13 -10
View File
@@ -1,22 +1,25 @@
# Memory files # Memory tool (`save_memory`)
Gemini CLI persists durable facts, user preferences, and project details by The `save_memory` tool allows the Gemini agent to persist specific facts, user
editing Markdown memory files directly. preferences, and project details across sessions.
## Technical reference ## Technical reference
The agent routes memories to the appropriate Markdown file: shared project This tool appends information to the `## Gemini Added Memories` section of your
instructions go in repository `GEMINI.md` files, private project notes go in the global `GEMINI.md` file (typically located at `~/.gemini/GEMINI.md`).
per-project private memory folder, and cross-project personal preferences go in
the global `~/.gemini/GEMINI.md` file. ### Arguments
- `fact` (string, required): A clear, self-contained statement in natural
language.
## Technical behavior ## Technical behavior
- **Storage:** Edits Markdown files with `write_file` or `replace`. - **Storage:** Appends to the global context file in the user's home directory.
- **Loading:** The stored facts are automatically included in the hierarchical - **Loading:** The stored facts are automatically included in the hierarchical
context system for all future sessions. context system for all future sessions.
- **Format:** Keeps durable instructions concise and avoids duplicating the same - **Format:** Saves data as a bulleted list item within a dedicated Markdown
fact across multiple memory tiers. section.
## Use cases ## Use cases
-94
View File
@@ -1,94 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { LlmRole, type BaseLlmClient } from '@google/gemini-cli-core';
export interface JudgeOptions {
/**
* The number of parallel generations to run for majority voting.
* Defaults to 1. Use 3 or 5 for self-consistency.
*/
selfConsistencyRuns?: number;
/**
* The model to use for judging. Defaults to gemini-3-flash-base.
*/
model?: string;
}
export interface JudgeResult {
verdict: boolean;
reasoning: string[];
votes: { yes: number; no: number; other: number };
}
/**
* A reusable LLM-as-a-judge utility for behavioral evaluations.
*/
export class LLMJudge {
constructor(private readonly llmClient: BaseLlmClient) {}
/**
* Asks the LLM a Yes/No question and returns a boolean verdict.
* If selfConsistencyRuns > 1, it runs in parallel and returns the majority vote.
*/
async judgeYesNo(
question: string,
options: JudgeOptions = {},
): Promise<JudgeResult> {
const runs = options.selfConsistencyRuns ?? 1;
const model = options.model ?? 'gemini-3-flash-base';
const systemPrompt = `You are a strict, impartial expert judge. Read the provided evidence and question carefully. You MUST answer the question with ONLY "YES" or "NO". Do not provide any conversational filler or explanation before your answer.`;
const generateCall = async (): Promise<string> => {
try {
const response = await this.llmClient.generateContent({
modelConfigKey: { model },
contents: [{ role: 'user', parts: [{ text: question }] }],
systemInstruction: {
role: 'system',
parts: [{ text: systemPrompt }],
},
promptId: 'llm-judge-eval',
role: LlmRole.UTILITY_TOOL,
abortSignal: new AbortController().signal,
});
const text =
response.candidates?.[0]?.content?.parts?.[0]?.text
?.trim()
?.toUpperCase() || 'ERROR';
return text;
} catch (e: any) {
return `ERROR: ${e.message}`;
}
};
const promises = Array.from({ length: runs }).map(() => generateCall());
const rawResults = await Promise.all(promises);
let yes = 0;
let no = 0;
let other = 0;
for (const res of rawResults) {
// Remove any punctuation the model might have appended
const cleanRes = res.replace(/[^A-Z]/g, '');
if (cleanRes.startsWith('YES')) yes++;
else if (cleanRes.startsWith('NO')) no++;
else other++;
}
// Pass if YES > NO and YES > OTHER (plurality)
const pass = yes > no && yes > other;
return {
verdict: pass,
reasoning: rawResults,
votes: { yes, no, other },
};
}
}
@@ -11,7 +11,11 @@ import {
loadConversationRecord, loadConversationRecord,
SESSION_FILE_PREFIX, SESSION_FILE_PREFIX,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { evalTest, assertModelHasOutput } from './test-helper.js'; import {
evalTest,
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
function findDir(base: string, name: string): string | null { function findDir(base: string, name: string): string | null {
if (!fs.existsSync(base)) return null; if (!fs.existsSync(base)) return null;
@@ -73,13 +77,336 @@ async function waitForSessionScratchpad(
return loadLatestSessionRecord(homeDir, sessionId); return loadLatestSessionRecord(homeDir, sessionId);
} }
describe('memory persistence', () => { describe('save_memory', () => {
const TEST_PREFIX = 'Save memory test: ';
const rememberingFavoriteColor = "Agent remembers user's favorite color";
evalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingFavoriteColor,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `remember that my favorite color is blue.
what is my favorite color? tell me that and surround it with $ symbol`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: 'blue',
testName: `${TEST_PREFIX}${rememberingFavoriteColor}`,
});
},
});
const rememberingCommandRestrictions = 'Agent remembers command restrictions';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingCommandRestrictions,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `I don't want you to ever run npm commands.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/not run npm commands|remember|ok/i],
testName: `${TEST_PREFIX}${rememberingCommandRestrictions}`,
});
},
});
const rememberingWorkflow = 'Agent remembers workflow preferences';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingWorkflow,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `I want you to always lint after building.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/always|ok|remember|will do/i],
testName: `${TEST_PREFIX}${rememberingWorkflow}`,
});
},
});
const ignoringTemporaryInformation =
'Agent ignores temporary conversation details';
evalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: ignoringTemporaryInformation,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `I'm going to get a coffee.`,
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const wasToolCalled = rig
.readToolLogs()
.some((log) => log.toolRequest.name === 'save_memory');
expect(
wasToolCalled,
'save_memory should not be called for temporary information',
).toBe(false);
assertModelHasOutput(result);
checkModelOutputContent(result, {
testName: `${TEST_PREFIX}${ignoringTemporaryInformation}`,
forbiddenContent: [/remember|will do/i],
});
},
});
const rememberingPetName = "Agent remembers user's pet's name";
evalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingPetName,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `Please remember that my dog's name is Buddy.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/Buddy/i],
testName: `${TEST_PREFIX}${rememberingPetName}`,
});
},
});
const rememberingCommandAlias = 'Agent remembers custom command aliases';
evalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingCommandAlias,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `When I say 'start server', you should run 'npm run dev'.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/npm run dev|start server|ok|remember|will do/i],
testName: `${TEST_PREFIX}${rememberingCommandAlias}`,
});
},
});
const savingDbSchemaLocationAsProjectMemory =
'Agent saves workspace database schema location as project memory';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: savingDbSchemaLocationAsProjectMemory,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `The database schema for this workspace is located in \`db/schema.sql\`.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall(
'save_memory',
undefined,
(args) => {
try {
const params = JSON.parse(args);
return params.scope === 'project';
} catch {
return false;
}
},
);
expect(
wasToolCalled,
'Expected save_memory to be called with scope="project" for workspace-specific information',
).toBe(true);
assertModelHasOutput(result);
},
});
const rememberingCodingStyle =
"Agent remembers user's coding style preference";
evalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingCodingStyle,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `I prefer to use tabs instead of spaces for indentation.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/tabs instead of spaces|ok|remember|will do/i],
testName: `${TEST_PREFIX}${rememberingCodingStyle}`,
});
},
});
const savingBuildArtifactLocationAsProjectMemory =
'Agent saves workspace build artifact location as project memory';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: savingBuildArtifactLocationAsProjectMemory,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `In this workspace, build artifacts are stored in the \`dist/artifacts\` directory.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall(
'save_memory',
undefined,
(args) => {
try {
const params = JSON.parse(args);
return params.scope === 'project';
} catch {
return false;
}
},
);
expect(
wasToolCalled,
'Expected save_memory to be called with scope="project" for workspace-specific information',
).toBe(true);
assertModelHasOutput(result);
},
});
const savingMainEntryPointAsProjectMemory =
'Agent saves workspace main entry point as project memory';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: savingMainEntryPointAsProjectMemory,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `The main entry point for this workspace is \`src/index.js\`.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall(
'save_memory',
undefined,
(args) => {
try {
const params = JSON.parse(args);
return params.scope === 'project';
} catch {
return false;
}
},
);
expect(
wasToolCalled,
'Expected save_memory to be called with scope="project" for workspace-specific information',
).toBe(true);
assertModelHasOutput(result);
},
});
const rememberingBirthday = "Agent remembers user's birthday";
evalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingBirthday,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `My birthday is on June 15th.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/June 15th|ok|remember|will do/i],
testName: `${TEST_PREFIX}${rememberingBirthday}`,
});
},
});
const proactiveMemoryFromLongSession = const proactiveMemoryFromLongSession =
'Agent saves preference from earlier in conversation history'; 'Agent saves preference from earlier in conversation history';
evalTest('USUALLY_PASSES', { evalTest('USUALLY_PASSES', {
suiteName: 'default', suiteName: 'default',
suiteType: 'behavioral', suiteType: 'behavioral',
name: proactiveMemoryFromLongSession, name: proactiveMemoryFromLongSession,
params: {
settings: {
experimental: { memoryV2: true },
},
},
messages: [ messages: [
{ {
id: 'msg-1', id: 'msg-1',
@@ -135,9 +462,9 @@ describe('memory persistence', () => {
prompt: prompt:
'Please save any persistent preferences or facts about me from our conversation to memory.', 'Please save any persistent preferences or facts about me from our conversation to memory.',
assert: async (rig, result) => { assert: async (rig, result) => {
// The agent persists memories by editing markdown files directly with // Under experimental.memoryV2, the agent persists memories by
// write_file or replace. The user said // editing markdown files directly with write_file or replace — not via
// "I always prefer Vitest over // a save_memory subagent. The user said "I always prefer Vitest over
// Jest for testing in all my projects" — that matches the new // Jest for testing in all my projects" — that matches the new
// cross-project cue phrase ("across all my projects"), so under the // cross-project cue phrase ("across all my projects"), so under the
// 4-tier model the correct destination is the global personal memory // 4-tier model the correct destination is the global personal memory
@@ -195,12 +522,17 @@ describe('memory persistence', () => {
}, },
}); });
const memoryRoutesTeamConventionsToProjectGemini = const memoryV2RoutesTeamConventionsToProjectGemini =
'Agent routes team-shared project conventions to ./GEMINI.md'; 'Agent routes team-shared project conventions to ./GEMINI.md';
evalTest('USUALLY_PASSES', { evalTest('USUALLY_PASSES', {
suiteName: 'default', suiteName: 'default',
suiteType: 'behavioral', suiteType: 'behavioral',
name: memoryRoutesTeamConventionsToProjectGemini, name: memoryV2RoutesTeamConventionsToProjectGemini,
params: {
settings: {
experimental: { memoryV2: true },
},
},
messages: [ messages: [
{ {
id: 'msg-1', id: 'msg-1',
@@ -241,11 +573,11 @@ describe('memory persistence', () => {
], ],
prompt: 'Please save the preferences I mentioned earlier to memory.', prompt: 'Please save the preferences I mentioned earlier to memory.',
assert: async (rig, result) => { assert: async (rig, result) => {
// The prompt enforces an explicit one-tier-per-fact rule: team-shared // Under experimental.memoryV2, the prompt enforces an explicit
// project conventions (the team's test command, project-wide // one-tier-per-fact rule: team-shared project conventions (the team's
// indentation rules) belong in the committed project-root ./GEMINI.md // test command, project-wide indentation rules) belong in the
// and must NOT be mirrored or cross-referenced into the private project // committed project-root ./GEMINI.md and must NOT be mirrored or
// memory folder // cross-referenced into the private project memory folder
// (~/.gemini/tmp/<hash>/memory/). The global ~/.gemini/GEMINI.md must // (~/.gemini/tmp/<hash>/memory/). The global ~/.gemini/GEMINI.md must
// never be touched in this mode either. // never be touched in this mode either.
await rig.waitForToolCall('write_file').catch(() => {}); await rig.waitForToolCall('write_file').catch(() => {});
@@ -303,13 +635,18 @@ describe('memory persistence', () => {
}, },
}); });
const memorySessionScratchpad = const memoryV2SessionScratchpad =
'Session summary persists memory scratchpad for memory-saving sessions'; 'Session summary persists memory scratchpad for memory-saving sessions';
evalTest('USUALLY_PASSES', { evalTest('USUALLY_PASSES', {
suiteName: 'default', suiteName: 'default',
suiteType: 'behavioral', suiteType: 'behavioral',
name: memorySessionScratchpad, name: memoryV2SessionScratchpad,
sessionId: 'memory-scratchpad-eval', sessionId: 'memory-scratchpad-eval',
params: {
settings: {
experimental: { memoryV2: true },
},
},
messages: [ messages: [
{ {
id: 'msg-1', id: 'msg-1',
@@ -358,7 +695,7 @@ describe('memory persistence', () => {
expect( expect(
writeCalls.length, writeCalls.length,
'Expected memory save flow to edit a markdown memory file', 'Expected memoryV2 save flow to edit a markdown memory file',
).toBeGreaterThan(0); ).toBeGreaterThan(0);
await rig.run({ await rig.run({
@@ -395,12 +732,17 @@ describe('memory persistence', () => {
}, },
}); });
const memoryRoutesUserProject = const memoryV2RoutesUserProject =
'Agent routes personal-to-user project notes to user-project memory'; 'Agent routes personal-to-user project notes to user-project memory';
evalTest('USUALLY_PASSES', { evalTest('USUALLY_PASSES', {
suiteName: 'default', suiteName: 'default',
suiteType: 'behavioral', suiteType: 'behavioral',
name: memoryRoutesUserProject, name: memoryV2RoutesUserProject,
params: {
settings: {
experimental: { memoryV2: true },
},
},
prompt: `Please remember my personal local dev setup for THIS project's Postgres database. This is private to my machine — do NOT commit it to the repo. prompt: `Please remember my personal local dev setup for THIS project's Postgres database. This is private to my machine — do NOT commit it to the repo.
Connection details: Connection details:
@@ -419,11 +761,11 @@ Quirks to remember:
- The migrations runner sometimes hangs on my machine if I forget step 1; kill it with Ctrl+C and rerun. - The migrations runner sometimes hangs on my machine if I forget step 1; kill it with Ctrl+C and rerun.
- I keep an extra \`scratch\` schema for ad-hoc experiments — never reference it from project code.`, - I keep an extra \`scratch\` schema for ad-hoc experiments — never reference it from project code.`,
assert: async (rig, result) => { assert: async (rig, result) => {
// With the Private Project Memory bullet surfaced in the prompt, a fact // Under experimental.memoryV2 with the Private Project Memory bullet
// that is project-specific AND personal-to-the-user (must not be // surfaced in the prompt, a fact that is project-specific AND
// committed) should land in the private project memory folder under // personal-to-the-user (must not be committed) should land in the
// ~/.gemini/tmp/<hash>/memory/. The detailed note should be written to a // private project memory folder under ~/.gemini/tmp/<hash>/memory/. The
// sibling markdown file, with // detailed note should be written to a sibling markdown file, with
// MEMORY.md updated as the index. It must NOT go to committed // MEMORY.md updated as the index. It must NOT go to committed
// ./GEMINI.md or the global ~/.gemini/GEMINI.md. // ./GEMINI.md or the global ~/.gemini/GEMINI.md.
await rig.waitForToolCall('write_file').catch(() => {}); await rig.waitForToolCall('write_file').catch(() => {});
@@ -486,19 +828,24 @@ Quirks to remember:
}, },
}); });
const memoryRoutesCrossProjectToGlobal = const memoryV2RoutesCrossProjectToGlobal =
'Agent routes cross-project personal preferences to ~/.gemini/GEMINI.md'; 'Agent routes cross-project personal preferences to ~/.gemini/GEMINI.md';
evalTest('USUALLY_PASSES', { evalTest('USUALLY_PASSES', {
suiteName: 'default', suiteName: 'default',
suiteType: 'behavioral', suiteType: 'behavioral',
name: memoryRoutesCrossProjectToGlobal, name: memoryV2RoutesCrossProjectToGlobal,
params: {
settings: {
experimental: { memoryV2: true },
},
},
prompt: prompt:
'Please remember this about me in general: across all my projects I always prefer Prettier with single quotes and trailing commas, and I always prefer tabs over spaces for indentation. These are my personal coding-style defaults that follow me into every workspace.', 'Please remember this about me in general: across all my projects I always prefer Prettier with single quotes and trailing commas, and I always prefer tabs over spaces for indentation. These are my personal coding-style defaults that follow me into every workspace.',
assert: async (rig, result) => { assert: async (rig, result) => {
// With the Global Personal Memory tier surfaced in the prompt, a fact // Under experimental.memoryV2 with the Global Personal Memory
// that explicitly applies to the user "across all my projects" / "in // tier surfaced in the prompt, a fact that explicitly applies to the
// every workspace" must land in the global ~/.gemini/GEMINI.md (the // user "across all my projects" / "in every workspace" must land in
// cross-project tier). It must // the global ~/.gemini/GEMINI.md (the cross-project tier). It must
// NOT be mirrored into a committed project-root ./GEMINI.md (that // NOT be mirrored into a committed project-root ./GEMINI.md (that
// tier is for team-shared conventions) or into the per-project // tier is for team-shared conventions) or into the per-project
// private memory folder (that tier is for project-specific personal // private memory folder (that tier is for project-specific personal
-147
View File
@@ -1,147 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import {
componentEvalTest,
type ComponentEvalCase,
} from './component-test-helper.js';
import { type EvalPolicy } from './test-helper.js';
import { SnapshotGenerator } from '@google/gemini-cli-core';
import { NodeType, type ConcreteNode } from '@google/gemini-cli-core';
import { LLMJudge } from './llm-judge.js';
function snapshotEvalTest(policy: EvalPolicy, evalCase: ComponentEvalCase) {
return componentEvalTest(policy, evalCase);
}
describe('snapshot_fidelity', () => {
snapshotEvalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'SnapshotGenerator strictly retains specific empirical facts',
assert: async (config) => {
// 1. Construct a highly specific mock transcript containing 3 empirical facts we can test for:
// Fact A: File path -> src/compiler/server.ts
// Fact B: Error code -> COMPILE_ERR_404
// Fact C: Active Directive -> "do not fix it just yet"
const mockNodes: ConcreteNode[] = [
{
id: '1',
turnId: '1',
type: NodeType.USER_PROMPT,
timestamp: Date.now(),
role: 'user',
payload: {
text: 'I am trying to debug a weird timeout issue when compiling the TS server.',
},
},
{
id: '2',
turnId: '2',
type: NodeType.TOOL_EXECUTION,
timestamp: Date.now() + 100,
role: 'model',
payload: {
functionCall: {
name: 'run_shell_command',
args: { cmd: 'grep -rn "timeout" src/' },
},
},
},
{
id: '3',
turnId: '2',
type: NodeType.TOOL_EXECUTION,
timestamp: Date.now() + 200,
role: 'user',
payload: {
functionResponse: {
name: 'run_shell_command',
response: {
output:
'src/compiler/server.ts:442: setTimeout(() => reject(new Error("COMPILE_ERR_404")), 5000);',
},
},
},
},
{
id: '4',
turnId: '3',
type: NodeType.AGENT_YIELD,
timestamp: Date.now() + 300,
role: 'model',
payload: {
text: 'I found the exact line. It looks like the compiler throws COMPILE_ERR_404 if it hits 5 seconds.',
},
},
{
id: '5',
turnId: '4',
type: NodeType.USER_PROMPT,
timestamp: Date.now() + 400,
role: 'user',
payload: {
text: 'Okay, do not fix it just yet. I want you to remember this error code (COMPILE_ERR_404) and file path. First, list all the files in the directory.',
},
},
];
// 2. Extract the LLM Client from the component container
const llmClient = config.getBaseLlmClient();
const generator = new SnapshotGenerator({
llmClient,
promptId: 'eval-snapshot-test',
tokenCalculator: {
estimateTokensForString(str: string): number {
return str.length * 4;
},
},
} as any);
// 3. Generate the snapshot using the CURRENT system prompt
const snapshotText = await generator.synthesizeSnapshot(mockNodes);
// 4. Use LLM-as-a-Judge with Self-Consistency to evaluate factual fidelity
const judge = new LLMJudge(llmClient);
const judgePrompt = `
EVIDENCE (CONTEXT SNAPSHOT):
"""
${snapshotText}
"""
QUESTION:
Does the EVIDENCE explicitly contain all three of the following facts:
1. The specific file path "src/compiler/server.ts"
2. The specific error code "COMPILE_ERR_404"
3. The user's active constraint/directive to "do not fix it just yet" (or equivalent warning that implementation is paused)
Answer ONLY with "YES" if all three are unambiguously present.
Answer "NO" if any of the three are missing, abstracted away, or generalized (e.g., if it says "found an error" instead of "COMPILE_ERR_404").`;
// Use a self-consistency of 3 runs to get a robust majority vote
const result = await judge.judgeYesNo(judgePrompt, {
selfConsistencyRuns: 3,
});
// 5. Assert the verdict
const formattedVotes = JSON.stringify(result.votes);
const formattedReasoning = JSON.stringify(result.reasoning);
expect(
result.verdict,
`Snapshot failed to retain empirical facts.
Votes: ${formattedVotes}
Reasoning: ${formattedReasoning}
Generated Snapshot:
${snapshotText}`,
).toBe(true);
},
});
});
+1 -1
View File
@@ -32,7 +32,7 @@ export const EVAL_MODEL =
// Indicates the consistency expectation for this test. // Indicates the consistency expectation for this test.
// - ALWAYS_PASSES - Means that the test is expected to pass 100% of the time. These // - ALWAYS_PASSES - Means that the test is expected to pass 100% of the time. These
// These tests are typically trivial and test basic functionality with unambiguous // These tests are typically trivial and test basic functionality with unambiguous
// prompts. For example: "remember foo" should be fairly reliable. // prompts. For example: "call save_memory to remember foo" should be fairly reliable.
// These are the first line of defense against regressions in key behaviors and run in // These are the first line of defense against regressions in key behaviors and run in
// every CI. You can run these locally with 'npm run test:always_passing_evals'. // every CI. You can run these locally with 'npm run test:always_passing_evals'.
// //
+1 -1
View File
@@ -172,7 +172,7 @@ describe('file-system', () => {
).toBeDefined(); ).toBeDefined();
const newFileContent = rig.readFile(fileName); const newFileContent = rig.readFile(fileName);
expect(newFileContent.trimEnd()).toBe('1.0.1'); expect(newFileContent).toBe('1.0.1');
}); });
it.skip('should replace multiple instances of a string', async () => { it.skip('should replace multiple instances of a string', async () => {
+2 -2
View File
@@ -12,7 +12,7 @@ if (process.env['NO_COLOR'] !== undefined) {
import { mkdir, readdir, rm, readFile } from 'node:fs/promises'; import { mkdir, readdir, rm, readFile } from 'node:fs/promises';
import { join, dirname, extname } from 'node:path'; import { join, dirname, extname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { resolveRipgrepPath } from '../packages/core/src/tools/ripGrep.js'; import { canUseRipgrep } from '../packages/core/src/tools/ripGrep.js';
import { disableMouseTracking } from '@google/gemini-cli-core'; import { disableMouseTracking } from '@google/gemini-cli-core';
import { isolateTestEnv } from '../packages/test-utils/src/env-setup.js'; import { isolateTestEnv } from '../packages/test-utils/src/env-setup.js';
import { createServer, type Server } from 'node:http'; import { createServer, type Server } from 'node:http';
@@ -93,7 +93,7 @@ export async function setup() {
isolateTestEnv(runDir); isolateTestEnv(runDir);
// Download ripgrep to avoid race conditions in parallel tests // Download ripgrep to avoid race conditions in parallel tests
const available = await resolveRipgrepPath(); const available = await canUseRipgrep();
if (!available) { if (!available) {
throw new Error('Failed to download ripgrep binary'); throw new Error('Failed to download ripgrep binary');
} }
+1 -2
View File
@@ -231,8 +231,7 @@ describe('Plan Mode', () => {
`Expected write_file to succeed, but it failed with error: ${'error' in (planWrite?.toolRequest || {}) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'}`, `Expected write_file to succeed, but it failed with error: ${'error' in (planWrite?.toolRequest || {}) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'}`,
).toBe(true); ).toBe(true);
}); });
it('should switch from a pro model to a flash model after exiting plan mode', async () => {
it.skip('should switch from a pro model to a flash model after exiting plan mode', async () => {
const plansDir = 'plans-folder'; const plansDir = 'plans-folder';
const planFilename = 'my-plan.md'; const planFilename = 'my-plan.md';
+1 -8
View File
@@ -8,10 +8,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as path from 'node:path'; import * as path from 'node:path';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as os from 'node:os'; import * as os from 'node:os';
import { import { RipGrepTool } from '../packages/core/src/tools/ripGrep.js';
RipGrepTool,
resolveRipgrepPath,
} from '../packages/core/src/tools/ripGrep.js';
import { Config } from '../packages/core/src/config/config.js'; import { Config } from '../packages/core/src/config/config.js';
import { WorkspaceContext } from '../packages/core/src/utils/workspaceContext.js'; import { WorkspaceContext } from '../packages/core/src/utils/workspaceContext.js';
import { createMockMessageBus } from '../packages/core/src/test-utils/mock-message-bus.js'; import { createMockMessageBus } from '../packages/core/src/test-utils/mock-message-bus.js';
@@ -51,10 +48,6 @@ class MockConfig {
validatePathAccess() { validatePathAccess() {
return null; return null;
} }
async getRipgrepPath() {
return resolveRipgrepPath();
}
} }
describe('ripgrep-real-direct', () => { describe('ripgrep-real-direct', () => {
+2 -2
View File
@@ -7,7 +7,7 @@
import { mkdir, readdir, rm } from 'node:fs/promises'; import { mkdir, readdir, rm } from 'node:fs/promises';
import { join, dirname } from 'node:path'; import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { resolveRipgrepPath } from '../packages/core/src/tools/ripGrep.js'; import { canUseRipgrep } from '../packages/core/src/tools/ripGrep.js';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..'); const rootDir = join(__dirname, '..');
@@ -27,7 +27,7 @@ export async function setup() {
process.env['GEMINI_CONFIG_DIR'] = join(runDir, '.gemini'); process.env['GEMINI_CONFIG_DIR'] = join(runDir, '.gemini');
// Download ripgrep to avoid race conditions // Download ripgrep to avoid race conditions
const available = await resolveRipgrepPath(); const available = await canUseRipgrep();
if (!available) { if (!available) {
throw new Error('Failed to download ripgrep binary'); throw new Error('Failed to download ripgrep binary');
} }
+357 -373
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@google/gemini-cli", "name": "@google/gemini-cli",
"version": "0.44.0-nightly.20260512.g022e8baef", "version": "0.42.0-nightly.20260428.g59b2dea0e",
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git" "url": "git+https://github.com/google-gemini/gemini-cli.git"
}, },
"config": { "config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.44.0-nightly.20260512.g022e8baef" "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.42.0-nightly.20260428.g59b2dea0e"
}, },
"scripts": { "scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js", "start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -418,7 +418,7 @@ confirmations for tool calls (like executing a shell command), will be sent as
```proto ```proto
// Request to execute a specific slash command. // Request to execute a specific slash command.
message ExecuteSlashCommandRequest { message ExecuteSlashCommandRequest {
// The path to the command, e.g., ["memory", "list"] for /memory list // The path to the command, e.g., ["memory", "add"] for /memory add
repeated string command_path = 1; repeated string command_path = 1;
// The arguments for the command as a single string. // The arguments for the command as a single string.
string args = 2; string args = 2;
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@google/gemini-cli-a2a-server", "name": "@google/gemini-cli-a2a-server",
"version": "0.44.0-nightly.20260512.g022e8baef", "version": "0.42.0-nightly.20260428.g59b2dea0e",
"description": "Gemini CLI A2A Server", "description": "Gemini CLI A2A Server",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -26,7 +26,7 @@
], ],
"dependencies": { "dependencies": {
"@a2a-js/sdk": "0.3.11", "@a2a-js/sdk": "0.3.11",
"@google-cloud/storage": "^7.19.0", "@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core", "@google/gemini-cli-core": "file:../core",
"express": "^5.1.0", "express": "^5.1.0",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
@@ -5,13 +5,17 @@
*/ */
import { import {
addMemory,
listMemoryFiles, listMemoryFiles,
refreshMemory, refreshMemory,
showMemory, showMemory,
type AnyDeclarativeTool,
type Config, type Config,
type ToolRegistry,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { import {
AddMemoryCommand,
ListMemoryCommand, ListMemoryCommand,
MemoryCommand, MemoryCommand,
RefreshMemoryCommand, RefreshMemoryCommand,
@@ -28,23 +32,44 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
showMemory: vi.fn(), showMemory: vi.fn(),
refreshMemory: vi.fn(), refreshMemory: vi.fn(),
listMemoryFiles: vi.fn(), listMemoryFiles: vi.fn(),
addMemory: vi.fn(),
}; };
}); });
const mockShowMemory = vi.mocked(showMemory); const mockShowMemory = vi.mocked(showMemory);
const mockRefreshMemory = vi.mocked(refreshMemory); const mockRefreshMemory = vi.mocked(refreshMemory);
const mockListMemoryFiles = vi.mocked(listMemoryFiles); const mockListMemoryFiles = vi.mocked(listMemoryFiles);
const mockAddMemory = vi.mocked(addMemory);
describe('a2a-server memory commands', () => { describe('a2a-server memory commands', () => {
let mockContext: CommandContext; let mockContext: CommandContext;
let mockConfig: Config; let mockConfig: Config;
let mockToolRegistry: ToolRegistry;
let mockSaveMemoryTool: AnyDeclarativeTool;
beforeEach(() => { beforeEach(() => {
mockConfig = {} as unknown as Config; mockSaveMemoryTool = {
name: 'save_memory',
description: 'Saves memory',
buildAndExecute: vi.fn().mockResolvedValue(undefined),
} as unknown as AnyDeclarativeTool;
mockToolRegistry = {
getTool: vi.fn(),
} as unknown as ToolRegistry;
mockConfig = {
get toolRegistry() {
return mockToolRegistry;
},
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
} as unknown as Config;
mockContext = { mockContext = {
config: mockConfig, config: mockConfig,
}; };
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockSaveMemoryTool);
}); });
describe('MemoryCommand', () => { describe('MemoryCommand', () => {
@@ -111,4 +136,76 @@ describe('a2a-server memory commands', () => {
expect(response.data).toBe('file1.md\nfile2.md'); expect(response.data).toBe('file1.md\nfile2.md');
}); });
}); });
describe('AddMemoryCommand', () => {
it('returns message content if addMemory returns a message', async () => {
const command = new AddMemoryCommand();
mockAddMemory.mockReturnValue({
type: 'message',
messageType: 'error',
content: 'error message',
});
const response = await command.execute(mockContext, []);
expect(mockAddMemory).toHaveBeenCalledWith('');
expect(response.name).toBe('memory add');
expect(response.data).toBe('error message');
});
it('executes the save_memory tool if found', async () => {
const command = new AddMemoryCommand();
const fact = 'this is a new fact';
mockAddMemory.mockReturnValue({
type: 'tool',
toolName: 'save_memory',
toolArgs: { fact },
});
const response = await command.execute(mockContext, [
'this',
'is',
'a',
'new',
'fact',
]);
expect(mockAddMemory).toHaveBeenCalledWith(fact);
expect(mockToolRegistry.getTool).toHaveBeenCalledWith('save_memory');
expect(mockSaveMemoryTool.buildAndExecute).toHaveBeenCalledWith(
{ fact },
expect.any(AbortSignal),
undefined,
{
shellExecutionConfig: {
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
},
sandboxManager: undefined,
},
},
);
expect(mockRefreshMemory).toHaveBeenCalledWith(mockContext.config);
expect(response.name).toBe('memory add');
expect(response.data).toBe(`Added memory: "${fact}"`);
});
it('returns an error if the tool is not found', async () => {
const command = new AddMemoryCommand();
const fact = 'another fact';
mockAddMemory.mockReturnValue({
type: 'tool',
toolName: 'save_memory',
toolArgs: { fact },
});
vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined);
const response = await command.execute(mockContext, ['another', 'fact']);
expect(response.name).toBe('memory add');
expect(response.data).toBe('Error: Tool save_memory not found.');
});
});
}); });
@@ -5,6 +5,7 @@
*/ */
import { import {
addMemory,
listMemoryFiles, listMemoryFiles,
refreshMemory, refreshMemory,
showMemory, showMemory,
@@ -14,6 +15,13 @@ import type {
CommandContext, CommandContext,
CommandExecutionResponse, CommandExecutionResponse,
} from './types.js'; } from './types.js';
import type { AgentLoopContext } from '@google/gemini-cli-core';
const DEFAULT_SANITIZATION_CONFIG = {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
};
export class MemoryCommand implements Command { export class MemoryCommand implements Command {
readonly name = 'memory'; readonly name = 'memory';
@@ -22,6 +30,7 @@ export class MemoryCommand implements Command {
new ShowMemoryCommand(), new ShowMemoryCommand(),
new RefreshMemoryCommand(), new RefreshMemoryCommand(),
new ListMemoryCommand(), new ListMemoryCommand(),
new AddMemoryCommand(),
]; ];
readonly topLevel = true; readonly topLevel = true;
readonly requiresWorkspace = true; readonly requiresWorkspace = true;
@@ -72,3 +81,43 @@ export class ListMemoryCommand implements Command {
return { name: this.name, data: result.content }; return { name: this.name, data: result.content };
} }
} }
export class AddMemoryCommand implements Command {
readonly name = 'memory add';
readonly description = 'Add content to the memory.';
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const textToAdd = args.join(' ').trim();
const result = addMemory(textToAdd);
if (result.type === 'message') {
return { name: this.name, data: result.content };
}
const loopContext: AgentLoopContext = context.config;
const toolRegistry = loopContext.toolRegistry;
const tool = toolRegistry.getTool(result.toolName);
if (tool) {
const abortController = new AbortController();
const abortSignal = abortController.signal;
await tool.buildAndExecute(result.toolArgs, abortSignal, undefined, {
shellExecutionConfig: {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: loopContext.sandboxManager,
},
});
await refreshMemory(context.config);
return {
name: this.name,
data: `Added memory: "${textToAdd}"`,
};
} else {
return {
name: this.name,
data: `Error: Tool ${result.toolName} not found.`,
};
}
}
}
@@ -10,6 +10,7 @@ import { loadConfig } from './config.js';
import type { Settings } from './settings.js'; import type { Settings } from './settings.js';
import { import {
type ExtensionLoader, type ExtensionLoader,
FileDiscoveryService,
getCodeAssistServer, getCodeAssistServer,
Config, Config,
ExperimentFlags, ExperimentFlags,
@@ -47,10 +48,16 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
}; };
return mockConfig; return mockConfig;
}), }),
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
memoryContent: { global: '', extension: '', project: '' },
fileCount: 0,
filePaths: [],
}),
startupProfiler: { startupProfiler: {
flush: vi.fn(), flush: vi.fn(),
}, },
isHeadlessMode: vi.fn().mockReturnValue(false), isHeadlessMode: vi.fn().mockReturnValue(false),
FileDiscoveryService: vi.fn(),
getCodeAssistServer: vi.fn(), getCodeAssistServer: vi.fn(),
fetchAdminControlsOnce: vi.fn(), fetchAdminControlsOnce: vi.fn(),
coreEvents: { coreEvents: {
@@ -261,6 +268,24 @@ describe('loadConfig', () => {
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([]); expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([]);
}); });
it('should initialize FileDiscoveryService with correct options', async () => {
const testPath = '/tmp/ignore';
vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', testPath);
const settings: Settings = {
fileFiltering: {
respectGitIgnore: false,
},
};
await loadConfig(settings, mockExtensionLoader, taskId);
expect(FileDiscoveryService).toHaveBeenCalledWith(expect.any(String), {
respectGitIgnore: false,
respectGeminiIgnore: undefined,
customIgnoreFilePaths: [testPath],
});
});
describe('tool configuration', () => { describe('tool configuration', () => {
it('should pass V1 allowedTools to Config properly', async () => { it('should pass V1 allowedTools to Config properly', async () => {
const settings: Settings = { const settings: Settings = {
+19
View File
@@ -11,7 +11,9 @@ import * as dotenv from 'dotenv';
import { import {
AuthType, AuthType,
Config, Config,
FileDiscoveryService,
ApprovalMode, ApprovalMode,
loadServerHierarchicalMemory,
GEMINI_DIR, GEMINI_DIR,
DEFAULT_GEMINI_EMBEDDING_MODEL, DEFAULT_GEMINI_EMBEDDING_MODEL,
startupProfiler, startupProfiler,
@@ -127,6 +129,23 @@ export async function loadConfig(
enableAgents: settings.experimental?.enableAgents ?? true, enableAgents: settings.experimental?.enableAgents ?? true,
}; };
const fileService = new FileDiscoveryService(workspaceDir, {
respectGitIgnore: configParams?.fileFiltering?.respectGitIgnore,
respectGeminiIgnore: configParams?.fileFiltering?.respectGeminiIgnore,
customIgnoreFilePaths: configParams?.fileFiltering?.customIgnoreFilePaths,
});
const { memoryContent, fileCount, filePaths } =
await loadServerHierarchicalMemory(
workspaceDir,
[workspaceDir],
fileService,
extensionLoader,
folderTrust,
);
configParams.userMemory = memoryContent;
configParams.geminiMdFileCount = fileCount;
configParams.geminiMdFilePaths = filePaths;
// Set an initial config to use to get a code assist server. // Set an initial config to use to get a code assist server.
// This is needed to fetch admin controls. // This is needed to fetch admin controls.
const initialConfig = new Config({ const initialConfig = new Config({
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@google/gemini-cli", "name": "@google/gemini-cli",
"version": "0.44.0-nightly.20260512.g022e8baef", "version": "0.42.0-nightly.20260428.g59b2dea0e",
"description": "Gemini CLI", "description": "Gemini CLI",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
@@ -27,7 +27,7 @@
"dist" "dist"
], ],
"config": { "config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.44.0-nightly.20260512.g022e8baef" "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.42.0-nightly.20260428.g59b2dea0e"
}, },
"dependencies": { "dependencies": {
"@agentclientprotocol/sdk": "^0.16.1", "@agentclientprotocol/sdk": "^0.16.1",
@@ -17,8 +17,9 @@ describe('CommandHandler', () => {
expect(memShow.commandToExecute?.name).toBe('memory show'); expect(memShow.commandToExecute?.name).toBe('memory show');
expect(memShow.args).toBe(''); expect(memShow.args).toBe('');
const memList = parse('/memory list'); const memAdd = parse('/memory add hello world');
expect(memList.commandToExecute?.name).toBe('memory list'); expect(memAdd.commandToExecute?.name).toBe('memory add');
expect(memAdd.args).toBe('hello world');
const extList = parse('/extensions list'); const extList = parse('/extensions list');
expect(extList.commandToExecute?.name).toBe('extensions list'); expect(extList.commandToExecute?.name).toBe('extensions list');
+2 -5
View File
@@ -60,11 +60,8 @@ export class AcpFileSystemService implements FileSystemService {
sessionId: this.sessionId, sessionId: this.sessionId,
}); });
const content: unknown = response.content; // eslint-disable-next-line @typescript-eslint/no-unsafe-return
if (typeof content !== 'string') { return response.content;
throw new Error('content must be a string'); // replace with other response type formats when modified in the future
}
return content;
} catch (err: unknown) { } catch (err: unknown) {
this.normalizeFileSystemError(err); this.normalizeFileSystemError(err);
} }
@@ -19,7 +19,6 @@ import type * as acp from '@agentclientprotocol/sdk';
import { import {
AuthType, AuthType,
type Config, type Config,
GEMINI_MODEL_ALIAS_AUTO,
type MessageBus, type MessageBus,
type Storage, type Storage,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
@@ -209,7 +208,7 @@ describe('AcpSessionManager', () => {
expect(response.models?.availableModels).toEqual( expect(response.models?.availableModels).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
modelId: GEMINI_MODEL_ALIAS_AUTO, modelId: 'auto-gemini-3',
name: expect.stringContaining('Auto'), name: expect.stringContaining('Auto'),
}), }),
]), ]),
+2 -10
View File
@@ -69,10 +69,7 @@ export class AcpSessionManager {
); );
const authType = const authType =
loadedSettings.merged.security.auth.selectedType || loadedSettings.merged.security.auth.selectedType || AuthType.USE_GEMINI;
(authDetails.baseUrl || process.env['GOOGLE_GEMINI_BASE_URL']
? AuthType.GATEWAY
: AuthType.USE_GEMINI);
let isAuthenticated = false; let isAuthenticated = false;
let authErrorMessage = ''; let authErrorMessage = '';
@@ -234,12 +231,7 @@ export class AcpSessionManager {
mcpServers: acp.McpServer[], mcpServers: acp.McpServer[],
authDetails: AuthDetails, authDetails: AuthDetails,
): Promise<Config> { ): Promise<Config> {
const selectedAuthType = const selectedAuthType = this.settings.merged.security.auth.selectedType;
this.settings.merged.security.auth.selectedType ||
(authDetails.baseUrl || process.env['GOOGLE_GEMINI_BASE_URL']
? AuthType.GATEWAY
: undefined);
if (!selectedAuthType) { if (!selectedAuthType) {
throw acp.RequestError.authRequired(); throw acp.RequestError.authRequired();
} }
+17 -10
View File
@@ -10,7 +10,8 @@ import {
type ToolCallConfirmationDetails, type ToolCallConfirmationDetails,
Kind, Kind,
ApprovalMode, ApprovalMode,
GEMINI_MODEL_ALIAS_AUTO, DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO,
DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL, DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL, DEFAULT_GEMINI_FLASH_LITE_MODEL,
@@ -22,8 +23,6 @@ import {
getDisplayString, getDisplayString,
AuthType, AuthType,
ToolConfirmationOutcome, ToolConfirmationOutcome,
getChannelFromVersion,
getAutoModelDescription,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type * as acp from '@agentclientprotocol/sdk'; import type * as acp from '@agentclientprotocol/sdk';
import { z } from 'zod'; import { z } from 'zod';
@@ -263,7 +262,7 @@ export function buildAvailableModels(
}>; }>;
currentModelId: string; currentModelId: string;
} { } {
const preferredModel = config.getModel() || GEMINI_MODEL_ALIAS_AUTO; const preferredModel = config.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
const shouldShowPreviewModels = config.getHasAccessToPreviewModel(); const shouldShowPreviewModels = config.getHasAccessToPreviewModel();
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false; const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
const useGemini31FlashLite = const useGemini31FlashLite =
@@ -272,8 +271,6 @@ export function buildAvailableModels(
const useCustomToolModel = const useCustomToolModel =
useGemini31 && selectedAuthType === AuthType.USE_GEMINI; useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
const releaseChannel = getChannelFromVersion(config.clientVersion);
// --- DYNAMIC PATH --- // --- DYNAMIC PATH ---
if ( if (
config.getExperimentalDynamicModelConfiguration?.() === true && config.getExperimentalDynamicModelConfiguration?.() === true &&
@@ -284,7 +281,6 @@ export function buildAvailableModels(
useGemini3_1FlashLite: useGemini31FlashLite, useGemini3_1FlashLite: useGemini31FlashLite,
useCustomTools: useCustomToolModel, useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels, hasAccessToPreview: shouldShowPreviewModels,
releaseChannel,
}); });
return { return {
@@ -296,12 +292,23 @@ export function buildAvailableModels(
// --- LEGACY PATH --- // --- LEGACY PATH ---
const mainOptions = [ const mainOptions = [
{ {
value: GEMINI_MODEL_ALIAS_AUTO, value: DEFAULT_GEMINI_MODEL_AUTO,
title: getDisplayString(GEMINI_MODEL_ALIAS_AUTO), title: getDisplayString(DEFAULT_GEMINI_MODEL_AUTO),
description: getAutoModelDescription(releaseChannel, useGemini31), description:
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
}, },
]; ];
if (shouldShowPreviewModels) {
mainOptions.unshift({
value: PREVIEW_GEMINI_MODEL_AUTO,
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
description: useGemini31
? 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash'
: 'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
});
}
const manualOptions = [ const manualOptions = [
{ {
value: DEFAULT_GEMINI_MODEL, value: DEFAULT_GEMINI_MODEL,
+50
View File
@@ -5,6 +5,7 @@
*/ */
import { import {
addMemory,
listInboxMemoryPatches, listInboxMemoryPatches,
listInboxSkills, listInboxSkills,
listInboxPatches, listInboxPatches,
@@ -18,6 +19,12 @@ import type {
CommandExecutionResponse, CommandExecutionResponse,
} from './types.js'; } from './types.js';
const DEFAULT_SANITIZATION_CONFIG = {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
};
export class MemoryCommand implements Command { export class MemoryCommand implements Command {
readonly name = 'memory'; readonly name = 'memory';
readonly description = 'Manage memory.'; readonly description = 'Manage memory.';
@@ -25,6 +32,7 @@ export class MemoryCommand implements Command {
new ShowMemoryCommand(), new ShowMemoryCommand(),
new RefreshMemoryCommand(), new RefreshMemoryCommand(),
new ListMemoryCommand(), new ListMemoryCommand(),
new AddMemoryCommand(),
new InboxMemoryCommand(), new InboxMemoryCommand(),
]; ];
readonly requiresWorkspace = true; readonly requiresWorkspace = true;
@@ -77,6 +85,48 @@ export class ListMemoryCommand implements Command {
} }
} }
export class AddMemoryCommand implements Command {
readonly name = 'memory add';
readonly description = 'Add content to the memory.';
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const textToAdd = args.join(' ').trim();
const result = addMemory(textToAdd);
if (result.type === 'message') {
return { name: this.name, data: result.content };
}
const toolRegistry = context.agentContext.toolRegistry;
const tool = toolRegistry.getTool(result.toolName);
if (tool) {
const abortController = new AbortController();
const signal = abortController.signal;
await context.sendMessage(`Saving memory via ${result.toolName}...`);
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
shellExecutionConfig: {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: context.agentContext.sandboxManager,
},
});
await refreshMemory(context.agentContext.config);
return {
name: this.name,
data: `Added memory: "${textToAdd}"`,
};
} else {
return {
name: this.name,
data: `Error: Tool ${result.toolName} not found.`,
};
}
}
}
export class InboxMemoryCommand implements Command { export class InboxMemoryCommand implements Command {
readonly name = 'memory inbox'; readonly name = 'memory inbox';
readonly description = readonly description =
@@ -27,7 +27,7 @@ export interface ConfigLogger {
export type RequestSettingCallback = ( export type RequestSettingCallback = (
setting: ExtensionSetting, setting: ExtensionSetting,
) => Promise<string | undefined>; ) => Promise<string>;
export type RequestConfirmationCallback = (message: string) => Promise<boolean>; export type RequestConfirmationCallback = (message: string) => Promise<boolean>;
const defaultLogger: ConfigLogger = { const defaultLogger: ConfigLogger = {
@@ -47,7 +47,8 @@ const defaultRequestConfirmation: RequestConfirmationCallback = async (
message, message,
initial: false, initial: false,
}); });
return typeof response.confirm === 'boolean' ? response.confirm : false; // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response.confirm;
}; };
export async function getExtensionManager() { export async function getExtensionManager() {
+2 -11
View File
@@ -8,15 +8,6 @@ import { AuthType } from '@google/gemini-cli-core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { validateAuthMethod } from './auth.js'; import { validateAuthMethod } from './auth.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
loadApiKey: vi.fn().mockResolvedValue(null),
};
});
vi.mock('./settings.js', () => ({ vi.mock('./settings.js', () => ({
loadEnvironment: vi.fn(), loadEnvironment: vi.fn(),
loadSettings: vi.fn().mockReturnValue({ loadSettings: vi.fn().mockReturnValue({
@@ -99,10 +90,10 @@ describe('validateAuthMethod', () => {
envs: {}, envs: {},
expected: 'Invalid auth method selected.', expected: 'Invalid auth method selected.',
}, },
])('$description', async ({ authType, envs, expected }) => { ])('$description', ({ authType, envs, expected }) => {
for (const [key, value] of Object.entries(envs)) { for (const [key, value] of Object.entries(envs)) {
vi.stubEnv(key, value as string); vi.stubEnv(key, value as string);
} }
expect(await validateAuthMethod(authType)).toBe(expected); expect(validateAuthMethod(authType)).toBe(expected);
}); });
}); });
+3 -6
View File
@@ -4,12 +4,10 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { AuthType, loadApiKey } from '@google/gemini-cli-core'; import { AuthType } from '@google/gemini-cli-core';
import { loadEnvironment, loadSettings } from './settings.js'; import { loadEnvironment, loadSettings } from './settings.js';
export async function validateAuthMethod( export function validateAuthMethod(authMethod: string): string | null {
authMethod: string,
): Promise<string | null> {
loadEnvironment(loadSettings().merged, process.cwd()); loadEnvironment(loadSettings().merged, process.cwd());
if ( if (
authMethod === AuthType.LOGIN_WITH_GOOGLE || authMethod === AuthType.LOGIN_WITH_GOOGLE ||
@@ -19,8 +17,7 @@ export async function validateAuthMethod(
} }
if (authMethod === AuthType.USE_GEMINI) { if (authMethod === AuthType.USE_GEMINI) {
const key = process.env['GEMINI_API_KEY'] || (await loadApiKey()); if (!process.env['GEMINI_API_KEY']) {
if (!key) {
return ( return (
'When using Gemini API, you must specify the GEMINI_API_KEY environment variable.\n' + 'When using Gemini API, you must specify the GEMINI_API_KEY environment variable.\n' +
'Update your environment and try again (no reload needed if using .env)!' 'Update your environment and try again (no reload needed if using .env)!'
+166 -21
View File
@@ -15,6 +15,7 @@ import {
EDIT_TOOL_NAME, EDIT_TOOL_NAME,
WEB_FETCH_TOOL_NAME, WEB_FETCH_TOOL_NAME,
ASK_USER_TOOL_NAME, ASK_USER_TOOL_NAME,
type ExtensionLoader,
debugLogger, debugLogger,
ApprovalMode, ApprovalMode,
type MCPServerConfig, type MCPServerConfig,
@@ -111,6 +112,27 @@ vi.mock('@google/gemini-cli-core', async () => {
}), }),
}, },
loadEnvironment: vi.fn(), loadEnvironment: vi.fn(),
loadServerHierarchicalMemory: vi.fn(
(
cwd,
dirs,
fileService,
extensionLoader: ExtensionLoader,
_folderTrust,
_importFormat,
_fileFilteringOptions,
_maxDirs,
) => {
const extensionPaths =
extensionLoader?.getExtensions?.()?.flatMap((e) => e.contextFiles) ||
[];
return Promise.resolve({
memoryContent: extensionPaths.join(',') || '',
fileCount: extensionPaths?.length || 0,
filePaths: extensionPaths,
});
},
),
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS: { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS: {
respectGitIgnore: false, respectGitIgnore: false,
respectGeminiIgnore: true, respectGeminiIgnore: true,
@@ -212,7 +234,7 @@ describe('parseArguments', () => {
afterEach(() => { afterEach(() => {
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
it('should fail if multiple session flags are provided', async () => { it('should fail if both --resume and --session-id are provided', async () => {
process.argv = [ process.argv = [
'node', 'node',
'script.js', 'script.js',
@@ -233,7 +255,7 @@ describe('parseArguments', () => {
expect(mockConsoleError).toHaveBeenCalledWith( expect(mockConsoleError).toHaveBeenCalledWith(
expect.stringContaining( expect.stringContaining(
'The flags --resume, --session-id, and --session-file are mutually exclusive. Please provide only one.', 'Cannot use both --resume (-r) and --session-id together',
), ),
); );
}); });
@@ -1021,27 +1043,150 @@ describe('loadCliConfig', () => {
expect(config.isInteractive()).toBe(false); expect(config.isInteractive()).toBe(false);
}); });
});
describe('isAcpMode', () => { describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
it('should force skipNextSpeakerCheck to true when in ACP mode', async () => { beforeEach(() => {
process.argv = ['node', 'script.js', '--acp']; vi.resetAllMocks();
const argv = await parseArguments(createTestMergedSettings()); vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '');
const settings = createTestMergedSettings({ // Restore ExtensionManager mocks that were reset
model: { skipNextSpeakerCheck: false }, ExtensionManager.prototype.getExtensions = vi.fn().mockReturnValue([]);
}); ExtensionManager.prototype.loadExtensions = vi
const config = await loadCliConfig(settings, 'test-session', argv); .fn()
expect(config.getSkipNextSpeakerCheck()).toBe(true); .mockResolvedValue(undefined);
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
// Other common mocks would be reset here.
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it('should pass extension context file paths to loadServerHierarchicalMemory', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { jitContext: false },
});
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
{
path: '/path/to/ext1',
name: 'ext1',
id: 'ext1-id',
version: '1.0.0',
contextFiles: ['/path/to/ext1/GEMINI.md'],
isActive: true,
},
{
path: '/path/to/ext2',
name: 'ext2',
id: 'ext2-id',
version: '1.0.0',
contextFiles: [],
isActive: true,
},
{
path: '/path/to/ext3',
name: 'ext3',
id: 'ext3-id',
version: '1.0.0',
contextFiles: [
'/path/to/ext3/context1.md',
'/path/to/ext3/context2.md',
],
isActive: true,
},
]);
const argv = await parseArguments(createTestMergedSettings());
await loadCliConfig(settings, 'session-id', argv);
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect.any(String),
[],
expect.any(Object),
expect.any(ExtensionManager),
true,
'tree',
expect.objectContaining({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
200, // maxDirs
['.git'], // boundaryMarkers
);
});
it('should pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is true', async () => {
process.argv = ['node', 'script.js'];
const includeDir = path.resolve(path.sep, 'path', 'to', 'include');
const settings = createTestMergedSettings({
experimental: { jitContext: false },
context: {
includeDirectories: [includeDir],
loadMemoryFromIncludeDirectories: true,
},
}); });
it('should respect settings.model.skipNextSpeakerCheck when not in ACP mode', async () => { const argv = await parseArguments(settings);
process.argv = ['node', 'script.js']; await loadCliConfig(settings, 'session-id', argv);
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({ expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
model: { skipNextSpeakerCheck: false }, expect.any(String),
}); [includeDir],
const config = await loadCliConfig(settings, 'test-session', argv); expect.any(Object),
expect(config.getSkipNextSpeakerCheck()).toBe(false); expect.any(ExtensionManager),
true,
'tree',
expect.objectContaining({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
200,
['.git'], // boundaryMarkers
);
});
it('should NOT pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is false', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { jitContext: false },
context: {
includeDirectories: ['/path/to/include'],
loadMemoryFromIncludeDirectories: false,
},
}); });
const argv = await parseArguments(settings);
await loadCliConfig(settings, 'session-id', argv);
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect.any(String),
[],
expect.any(Object),
expect.any(ExtensionManager),
true,
'tree',
expect.objectContaining({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
200,
['.git'], // boundaryMarkers
);
});
it('should NOT call loadServerHierarchicalMemory when skipMemoryLoad is true', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { jitContext: false },
});
const argv = await parseArguments(settings);
await loadCliConfig(settings, 'session-id', argv, {
skipMemoryLoad: true,
});
expect(ServerConfig.loadServerHierarchicalMemory).not.toHaveBeenCalled();
}); });
}); });
@@ -1891,7 +2036,7 @@ describe('loadCliConfig model selection', () => {
argv, argv,
); );
expect(config.getModel()).toBe('auto'); expect(config.getModel()).toBe('auto-gemini-3');
}); });
it('always prefers model from argv', async () => { it('always prefers model from argv', async () => {
@@ -1935,7 +2080,7 @@ describe('loadCliConfig model selection', () => {
argv, argv,
); );
expect(config.getModel()).toBe('auto'); expect(config.getModel()).toBe('auto-gemini-3');
}); });
}); });
+55 -25
View File
@@ -16,19 +16,22 @@ import { hooksCommand } from '../commands/hooks.js';
import { gemmaCommand } from '../commands/gemma.js'; import { gemmaCommand } from '../commands/gemma.js';
import { import {
setGeminiMdFilename as setServerGeminiMdFilename, setGeminiMdFilename as setServerGeminiMdFilename,
resetGeminiMdFilename, getCurrentGeminiMdFilename,
DEFAULT_CONTEXT_FILENAME,
ApprovalMode, ApprovalMode,
DEFAULT_GEMINI_EMBEDDING_MODEL, DEFAULT_GEMINI_EMBEDDING_MODEL,
DEFAULT_FILE_FILTERING_OPTIONS, DEFAULT_FILE_FILTERING_OPTIONS,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
FileDiscoveryService, FileDiscoveryService,
resolveTelemetrySettings, resolveTelemetrySettings,
FatalConfigError, FatalConfigError,
getErrorMessage, getErrorMessage,
getPty, getPty,
debugLogger, debugLogger,
loadServerHierarchicalMemory,
ASK_USER_TOOL_NAME, ASK_USER_TOOL_NAME,
getVersion, getVersion,
PREVIEW_GEMINI_MODEL_AUTO,
type HierarchicalMemory,
coreEvents, coreEvents,
GEMINI_MODEL_ALIAS_AUTO, GEMINI_MODEL_ALIAS_AUTO,
getAdminErrorMessage, getAdminErrorMessage,
@@ -94,7 +97,6 @@ export interface CliArgs {
extensions: string[] | undefined; extensions: string[] | undefined;
listExtensions: boolean | undefined; listExtensions: boolean | undefined;
resume: string | typeof RESUME_LATEST | undefined; resume: string | typeof RESUME_LATEST | undefined;
sessionFile?: string | undefined;
sessionId: string | undefined; sessionId: string | undefined;
listSessions: boolean | undefined; listSessions: boolean | undefined;
deleteSession: string | undefined; deleteSession: string | undefined;
@@ -237,14 +239,8 @@ export async function parseArguments(
? query.length > 0 ? query.length > 0
: !!query; : !!query;
const sessionFlags = [ if (argv['resume'] !== undefined && argv['session-id'] !== undefined) {
argv['resume'] !== undefined, return 'Cannot use both --resume (-r) and --session-id together';
argv['session-id'] !== undefined,
argv['session-file'] !== undefined,
].filter(Boolean).length;
if (sessionFlags > 1) {
return 'The flags --resume, --session-id, and --session-file are mutually exclusive. Please provide only one.';
} }
if (argv['prompt'] && hasPositionalQuery) { if (argv['prompt'] && hasPositionalQuery) {
@@ -416,11 +412,6 @@ export async function parseArguments(
return trimmed; return trimmed;
}, },
}) })
.option('session-file', {
type: 'string',
nargs: 1,
description: 'Load a session from a JSON file',
})
.option('session-id', { .option('session-id', {
type: 'string', type: 'string',
nargs: 1, nargs: 1,
@@ -569,6 +560,7 @@ export interface LoadCliConfigOptions {
}; };
worktreeSettings?: WorktreeSettings; worktreeSettings?: WorktreeSettings;
skipExtensions?: boolean; skipExtensions?: boolean;
skipMemoryLoad?: boolean;
} }
export async function loadCliConfig( export async function loadCliConfig(
@@ -577,7 +569,12 @@ export async function loadCliConfig(
argv: CliArgs, argv: CliArgs,
options: LoadCliConfigOptions = {}, options: LoadCliConfigOptions = {},
): Promise<Config> { ): Promise<Config> {
const { cwd = process.cwd(), projectHooks, skipExtensions = false } = options; const {
cwd = process.cwd(),
projectHooks,
skipExtensions = false,
skipMemoryLoad = false,
} = options;
const debugMode = isDebugMode(argv); const debugMode = isDebugMode(argv);
const worktreeSettings = const worktreeSettings =
@@ -587,6 +584,7 @@ export async function loadCliConfig(
process.env['GEMINI_SANDBOX'] = 'true'; process.env['GEMINI_SANDBOX'] = 'true';
} }
const memoryImportFormat = settings.context?.importFormat || 'tree';
const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true; const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true;
const ideMode = settings.ide?.enabled ?? false; const ideMode = settings.ide?.enabled ?? false;
@@ -602,7 +600,7 @@ export async function loadCliConfig(
query: argv.query, query: argv.query,
})?.isTrusted ?? false; })?.isTrusted ?? false;
// Set the context filename in the server's memory file helpers before loading memory // Set the context filename in the server's memoryTool module BEFORE loading memory
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed // TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
// directly to the Config constructor in core, and have core handle setGeminiMdFilename. // directly to the Config constructor in core, and have core handle setGeminiMdFilename.
// However, loadHierarchicalGeminiMemory is called *before* createServerConfig. // However, loadHierarchicalGeminiMemory is called *before* createServerConfig.
@@ -610,11 +608,16 @@ export async function loadCliConfig(
setServerGeminiMdFilename(settings.context.fileName); setServerGeminiMdFilename(settings.context.fileName);
} else { } else {
// Reset to default if not provided in settings. // Reset to default if not provided in settings.
resetGeminiMdFilename(DEFAULT_CONTEXT_FILENAME); setServerGeminiMdFilename(getCurrentGeminiMdFilename());
} }
const fileService = new FileDiscoveryService(cwd); const fileService = new FileDiscoveryService(cwd);
const memoryFileFiltering = {
...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
...settings.context?.fileFiltering,
};
const fileFiltering = { const fileFiltering = {
...DEFAULT_FILE_FILTERING_OPTIONS, ...DEFAULT_FILE_FILTERING_OPTIONS,
...settings.context?.fileFiltering, ...settings.context?.fileFiltering,
@@ -665,6 +668,8 @@ export async function loadCliConfig(
?.getExtensions() ?.getExtensions()
?.find((ext) => ext.isActive && ext.plan?.directory)?.plan; ?.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
const experimentalJitContext = settings.experimental.jitContext ?? true;
let extensionRegistryURI = let extensionRegistryURI =
process.env['GEMINI_CLI_EXTENSION_REGISTRY_URI'] ?? process.env['GEMINI_CLI_EXTENSION_REGISTRY_URI'] ??
(trustedFolder ? settings.experimental?.extensionRegistryURI : undefined); (trustedFolder ? settings.experimental?.extensionRegistryURI : undefined);
@@ -675,9 +680,33 @@ export async function loadCliConfig(
); );
} }
let memoryContent: string | HierarchicalMemory = '';
let fileCount = 0;
let filePaths: string[] = [];
const finalExtensionLoader = const finalExtensionLoader =
extensionManager ?? new SimpleExtensionLoader([]); extensionManager ?? new SimpleExtensionLoader([]);
if (!experimentalJitContext && !skipMemoryLoad) {
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
const result = await loadServerHierarchicalMemory(
cwd,
settings.context?.loadMemoryFromIncludeDirectories || false
? includeDirectories
: [],
fileService,
finalExtensionLoader,
trustedFolder,
memoryImportFormat,
memoryFileFiltering,
settings.context?.discoveryMaxDirs,
settings.context?.memoryBoundaryMarkers,
);
memoryContent = result.memoryContent;
fileCount = result.fileCount;
filePaths = result.filePaths;
}
const question = argv.promptInteractive || argv.prompt || ''; const question = argv.promptInteractive || argv.prompt || '';
// Determine approval mode with backward compatibility // Determine approval mode with backward compatibility
@@ -825,7 +854,7 @@ export async function loadCliConfig(
interactive, interactive,
); );
const defaultModel = GEMINI_MODEL_ALIAS_AUTO; const defaultModel = PREVIEW_GEMINI_MODEL_AUTO;
const rawModel = const rawModel =
argv.model || process.env['GEMINI_MODEL'] || settings.model?.name; argv.model || process.env['GEMINI_MODEL'] || settings.model?.name;
@@ -989,6 +1018,9 @@ export async function loadCliConfig(
settings.security?.environmentVariableRedaction?.allowed, settings.security?.environmentVariableRedaction?.allowed,
enableEnvironmentVariableRedaction: enableEnvironmentVariableRedaction:
settings.security?.environmentVariableRedaction?.enabled, settings.security?.environmentVariableRedaction?.enabled,
userMemory: memoryContent,
geminiMdFileCount: fileCount,
geminiMdFilePaths: filePaths,
approvalMode, approvalMode,
disableYoloMode: disableYoloMode:
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled, settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
@@ -1033,6 +1065,8 @@ export async function loadCliConfig(
enableEventDrivenScheduler: true, enableEventDrivenScheduler: true,
skillsSupport: settings.skills?.enabled ?? true, skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled, disabledSkills: settings.skills?.disabled,
experimentalJitContext,
experimentalMemoryV2: settings.experimental?.memoryV2,
experimentalAutoMemory: settings.experimental?.autoMemory, experimentalAutoMemory: settings.experimental?.autoMemory,
experimentalGemma: settings.experimental?.gemma, experimentalGemma: settings.experimental?.gemma,
contextManagement, contextManagement,
@@ -1059,11 +1093,7 @@ export async function loadCliConfig(
shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout, shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout,
enableShellOutputEfficiency: enableShellOutputEfficiency:
settings.tools?.shell?.enableShellOutputEfficiency ?? true, settings.tools?.shell?.enableShellOutputEfficiency ?? true,
// In ACP mode, always skip the next-speaker check. This check triggers skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
// recursive continuation turns inside GeminiClient.processTurn() that
// conflict with ACP's explicit turn management via session/prompt,
// causing infinite agent_thought_chunk loops.
skipNextSpeakerCheck: isAcpMode || settings.model?.skipNextSpeakerCheck,
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold, truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
eventEmitter: coreEvents, eventEmitter: coreEvents,
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos, useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
@@ -109,7 +109,6 @@ describe('ExtensionManager theme loading', () => {
getFileExclusions: () => ({ getFileExclusions: () => ({
isIgnored: () => false, isIgnored: () => false,
}), }),
getMemoryContextManager: () => undefined,
getGeminiMdFilePaths: () => [], getGeminiMdFilePaths: () => [],
getMcpServers: () => ({}), getMcpServers: () => ({}),
getAllowedMcpServers: () => [], getAllowedMcpServers: () => [],
@@ -186,7 +185,6 @@ describe('ExtensionManager theme loading', () => {
getWorkspaceContext: () => ({ getWorkspaceContext: () => ({
getDirectories: () => [], getDirectories: () => [],
}), }),
getMemoryContextManager: () => undefined,
getDebugMode: () => false, getDebugMode: () => false,
getFileService: () => ({ getFileService: () => ({
findFiles: async () => [], findFiles: async () => [],
+4 -7
View File
@@ -88,9 +88,7 @@ interface ExtensionManagerParams {
enabledExtensionOverrides?: string[]; enabledExtensionOverrides?: string[];
settings: MergedSettings; settings: MergedSettings;
requestConsent: (consent: string) => Promise<boolean>; requestConsent: (consent: string) => Promise<boolean>;
requestSetting: requestSetting: ((setting: ExtensionSetting) => Promise<string>) | null;
| ((setting: ExtensionSetting) => Promise<string | undefined>)
| null;
workspaceDir: string; workspaceDir: string;
eventEmitter?: EventEmitter<ExtensionEvents>; eventEmitter?: EventEmitter<ExtensionEvents>;
clientVersion?: string; clientVersion?: string;
@@ -108,7 +106,7 @@ export class ExtensionManager extends ExtensionLoader {
private settings: MergedSettings; private settings: MergedSettings;
private requestConsent: (consent: string) => Promise<boolean>; private requestConsent: (consent: string) => Promise<boolean>;
private requestSetting: private requestSetting:
| ((setting: ExtensionSetting) => Promise<string | undefined>) | ((setting: ExtensionSetting) => Promise<string>)
| undefined; | undefined;
private telemetryConfig: Config; private telemetryConfig: Config;
private workspaceDir: string; private workspaceDir: string;
@@ -163,7 +161,7 @@ export class ExtensionManager extends ExtensionLoader {
} }
setRequestSetting( setRequestSetting(
requestSetting?: (setting: ExtensionSetting) => Promise<string | undefined>, requestSetting?: (setting: ExtensionSetting) => Promise<string>,
): void { ): void {
this.requestSetting = requestSetting; this.requestSetting = requestSetting;
} }
@@ -1302,8 +1300,7 @@ export async function inferInstallMetadata(
source.startsWith('git@') || source.startsWith('git@') ||
source.startsWith('sso://') || source.startsWith('sso://') ||
source.startsWith('github:') || source.startsWith('github:') ||
source.startsWith('gitlab:') || source.startsWith('gitlab:')
source.startsWith('ssh://')
) { ) {
return { return {
source, source,
@@ -94,8 +94,9 @@ export class ExtensionRegistryClient {
fuzzy: true, fuzzy: true,
}); });
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const results: Array<{ item: RegistryExtension }> = await fzf.find(query); const results = await fzf.find(query);
return results.map((r) => r.item); // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return results.map((r: { item: RegistryExtension }) => r.item);
} }
async getExtension(id: string): Promise<RegistryExtension | undefined> { async getExtension(id: string): Promise<RegistryExtension | undefined> {
@@ -8,7 +8,6 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { coreEvents, type GeminiCLIExtension } from '@google/gemini-cli-core'; import { coreEvents, type GeminiCLIExtension } from '@google/gemini-cli-core';
import { ExtensionStorage } from './storage.js'; import { ExtensionStorage } from './storage.js';
import { z } from 'zod';
export interface ExtensionEnablementConfig { export interface ExtensionEnablementConfig {
overrides: string[]; overrides: string[];
@@ -180,12 +179,8 @@ export class ExtensionEnablementManager {
readConfig(): AllExtensionsEnablementConfig { readConfig(): AllExtensionsEnablementConfig {
try { try {
const content = fs.readFileSync(this.configFilePath, 'utf-8'); const content = fs.readFileSync(this.configFilePath, 'utf-8');
const parsed: unknown = JSON.parse(content); // eslint-disable-next-line @typescript-eslint/no-unsafe-return
const schema = z.record( return JSON.parse(content);
z.string(),
z.object({ overrides: z.array(z.string()) }),
);
return schema.parse(parsed);
} catch (error) { } catch (error) {
if ( if (
error instanceof Error && error instanceof Error &&
@@ -62,7 +62,7 @@ export const getEnvFilePath = (
export async function maybePromptForSettings( export async function maybePromptForSettings(
extensionConfig: ExtensionConfig, extensionConfig: ExtensionConfig,
extensionId: string, extensionId: string,
requestSetting: (setting: ExtensionSetting) => Promise<string | undefined>, requestSetting: (setting: ExtensionSetting) => Promise<string>,
previousExtensionConfig?: ExtensionConfig, previousExtensionConfig?: ExtensionConfig,
previousSettings?: Record<string, string>, previousSettings?: Record<string, string>,
): Promise<void> { ): Promise<void> {
@@ -106,9 +106,7 @@ export async function maybePromptForSettings(
settingsChanges.promptForEnv, settingsChanges.promptForEnv,
)) { )) {
const answer = await requestSetting(setting); const answer = await requestSetting(setting);
if (answer !== undefined) { allSettings[setting.envVar] = answer;
allSettings[setting.envVar] = answer;
}
} }
const nonSensitiveSettings: Record<string, string> = {}; const nonSensitiveSettings: Record<string, string> = {};
@@ -161,13 +159,14 @@ function formatEnvContent(settings: Record<string, string>): string {
export async function promptForSetting( export async function promptForSetting(
setting: ExtensionSetting, setting: ExtensionSetting,
): Promise<string | undefined> { ): Promise<string> {
const response = await prompts({ const response = await prompts({
type: setting.sensitive ? 'password' : 'text', type: setting.sensitive ? 'password' : 'text',
name: 'value', name: 'value',
message: `${setting.name}\n${setting.description}`, message: `${setting.name}\n${setting.description}`,
}); });
return typeof response.value === 'string' ? response.value : undefined; // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response.value;
} }
export async function getScopedEnvContents( export async function getScopedEnvContents(
@@ -231,7 +230,7 @@ export async function updateSetting(
extensionConfig: ExtensionConfig, extensionConfig: ExtensionConfig,
extensionId: string, extensionId: string,
settingKey: string, settingKey: string,
requestSetting: (setting: ExtensionSetting) => Promise<string | undefined>, requestSetting: (setting: ExtensionSetting) => Promise<string>,
scope: ExtensionSettingScope, scope: ExtensionSettingScope,
workspaceDir: string, workspaceDir: string,
): Promise<void> { ): Promise<void> {
@@ -251,10 +250,6 @@ export async function updateSetting(
} }
const newValue = await requestSetting(settingToUpdate); const newValue = await requestSetting(settingToUpdate);
if (newValue === undefined) {
return;
}
const keychain = new KeychainTokenStorage( const keychain = new KeychainTokenStorage(
getKeychainStorageName(extensionName, extensionId, scope, workspaceDir), getKeychainStorageName(extensionName, extensionId, scope, workspaceDir),
); );
@@ -67,7 +67,8 @@ export function recursivelyHydrateStrings<T>(
} }
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return (obj as unknown[]).map((item) => return obj.map((item) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
recursivelyHydrateStrings(item, values), recursivelyHydrateStrings(item, values),
) as unknown as T; ) as unknown as T;
} }
@@ -120,7 +120,6 @@ describe('footerItems', () => {
'quota', 'quota',
'memory-usage', 'memory-usage',
'session-id', 'session-id',
'hostname',
'code-changes', 'code-changes',
'token-count', 'token-count',
]); ]);
-6
View File
@@ -47,11 +47,6 @@ export const ALL_ITEMS = [
header: 'session', header: 'session',
description: 'Unique identifier for the current session', description: 'Unique identifier for the current session',
}, },
{
id: 'hostname',
header: 'machine',
description: 'Current machine hostname',
},
{ {
id: 'auth', id: 'auth',
header: '/auth', header: '/auth',
@@ -80,7 +75,6 @@ export const DEFAULT_ORDER = [
'quota', 'quota',
'memory-usage', 'memory-usage',
'session-id', 'session-id',
'hostname',
'auth', 'auth',
'code-changes', 'code-changes',
'token-count', 'token-count',
@@ -1,44 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { parseArguments } from './config.js';
import { createTestMergedSettings } from './settings.js';
describe('parseArguments mutual exclusivity', () => {
afterEach(() => {
vi.restoreAllMocks();
});
const combinations = [
['--resume', '--session-id', 'test-id'],
['--resume', '--session-file', 'test.json'],
['--session-id', 'test-id', '--session-file', 'test.json'],
['--resume', '--session-id', 'test-id', '--session-file', 'test.json'],
];
combinations.forEach((args) => {
it(`should fail if ${args.filter((a) => a.startsWith('--')).join(' and ')} are provided`, async () => {
process.argv = ['node', 'script.js', ...args];
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
await expect(parseArguments(createTestMergedSettings())).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect.stringContaining(
'The flags --resume, --session-id, and --session-file are mutually exclusive. Please provide only one.',
),
);
});
});
});
+22 -16
View File
@@ -429,16 +429,6 @@ const SETTINGS_SCHEMA = {
'Enable the Topic & Update communication model for reduced chattiness and structured progress reporting.', 'Enable the Topic & Update communication model for reduced chattiness and structured progress reporting.',
showInDialog: true, showInDialog: true,
}, },
logRagSnippets: {
type: 'boolean',
label: 'Log RAG Snippets',
category: 'General',
requiresRestart: false,
default: false,
description:
'Log full Code Customization (RAG) retrieved snippets to a local file for debugging.',
showInDialog: true,
},
}, },
}, },
output: { output: {
@@ -2159,7 +2149,7 @@ const SETTINGS_SCHEMA = {
label: 'Voice Stop Grace Period (ms)', label: 'Voice Stop Grace Period (ms)',
category: 'Experimental', category: 'Experimental',
requiresRestart: false, requiresRestart: false,
default: 4000, default: 1000,
description: description:
'How long to wait for final transcription after stopping recording.', 'How long to wait for final transcription after stopping recording.',
showInDialog: true, showInDialog: true,
@@ -2262,6 +2252,16 @@ const SETTINGS_SCHEMA = {
'Enables extension loading/unloading within the CLI session.', 'Enables extension loading/unloading within the CLI session.',
showInDialog: false, showInDialog: false,
}, },
jitContext: {
type: 'boolean',
label: 'JIT Context Loading',
category: 'Experimental',
requiresRestart: true,
default: true,
description:
'Enable Just-In-Time (JIT) context loading. Defaults to true; set to false to opt out and load all GEMINI.md files into the system instruction up-front.',
showInDialog: false,
},
useOSC52Paste: { useOSC52Paste: {
type: 'boolean', type: 'boolean',
label: 'Use OSC 52 Paste', label: 'Use OSC 52 Paste',
@@ -2392,6 +2392,16 @@ const SETTINGS_SCHEMA = {
}, },
}, },
}, },
memoryV2: {
type: 'boolean',
label: 'Memory v2',
category: 'Experimental',
requiresRestart: true,
default: true,
description:
'Disable the built-in save_memory tool and let the main agent persist project context by editing markdown files directly with edit/write_file. Route facts across four tiers: team-shared conventions go to project GEMINI.md files, project-specific personal notes go to the per-project private memory folder (MEMORY.md as index + sibling .md files for detail), and cross-project personal preferences go to the global ~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit — settings, credentials, etc. remain off-limits). Set to false to fall back to the legacy save_memory tool.',
showInDialog: true,
},
stressTestProfile: { stressTestProfile: {
type: 'boolean', type: 'boolean',
label: label:
@@ -3461,11 +3471,7 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
family: { type: 'string' }, family: { type: 'string' },
isPreview: { type: 'boolean' }, isPreview: { type: 'boolean' },
isVisible: { type: 'boolean' }, isVisible: { type: 'boolean' },
dialogDescription: { dialogDescription: { type: 'string' },
type: 'string',
description:
"A description of the model to display in the model selection dialog. For the 'auto' alias, this value is dynamically generated and any value provided here will be ignored.",
},
features: { features: {
type: 'object', type: 'object',
properties: { properties: {
@@ -26,6 +26,11 @@ vi.mock('@google/gemini-cli-core', async () => {
); );
return { return {
...actual, ...actual,
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
}),
createPolicyEngineConfig: vi.fn().mockResolvedValue({ createPolicyEngineConfig: vi.fn().mockResolvedValue({
rules: [], rules: [],
checkers: [], checkers: [],

Some files were not shown because too many files have changed in this diff Show More