name: '🧠 Gemini CLI Bot: Brain' on: schedule: - cron: '0 0 * * *' # Every 24 hours issue_comment: types: ['created'] workflow_dispatch: inputs: run_interactive: description: 'Run interactive flow (requires issue_number)' type: 'boolean' default: false issue_number: description: 'Issue/PR number to simulate context from' type: 'string' required: false comment_id: description: 'Specific comment ID to simulate' type: 'string' required: false clear_memory: description: 'Clear memory (drops learnings from previous runs)' type: 'boolean' default: false enable_prs: description: 'Enable PRs (automatically promote changes to PRs)' type: 'boolean' default: false concurrency: group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.inputs.issue_number || github.ref }}' cancel-in-progress: true jobs: reasoning: name: 'Brain (Reasoning Layer)' runs-on: 'ubuntu-latest' if: | github.repository == 'google-gemini/gemini-cli' && ( 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 == '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. permissions: contents: 'read' issues: 'read' pull-requests: 'read' actions: 'read' env: GEMINI_CLI_TRUST_WORKSPACE: 'true' steps: - name: 'Checkout' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 with: fetch-depth: 0 persist-credentials: false - name: 'Setup Node.js' uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: 'Install dependencies' run: 'npm ci' - name: 'Build Gemini CLI' run: 'npm run bundle' - name: 'Download Previous State' env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' run: | if [ "${{ github.event.inputs.clear_memory }}" = "true" ]; then echo "Memory clear requested. Skipping previous state download." exit 0 fi # Find the last successful run of this workflow LAST_RUN_ID=$(gh run list --workflow "${{ github.workflow }}" --status success --limit 1 --json databaseId --jq '.[0].databaseId') if [ -n "$LAST_RUN_ID" ]; then echo "Found previous successful run: $LAST_RUN_ID" # Download brain memory (all state in one artifact) gh run download "$LAST_RUN_ID" -n brain-data -D . || echo "brain-data not found" else echo "No previous successful run found." fi - name: 'Collect Current Metrics' env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' run: 'npx tsx tools/gemini-cli-bot/metrics/index.ts' - name: 'Run Brain Phases' env: GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}' GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' GEMINI_MODEL: 'gemini-3-flash-preview' ENABLE_PRS: "${{ github.event.inputs.enable_prs || 'false' }}" TRIGGER_ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}' TRIGGER_COMMENT_ID: '${{ github.event.comment.id || github.event.inputs.comment_id }}' run: | PROMPT_PATH="tools/gemini-cli-bot/brain/metrics.md" if [ "${{ github.event_name }}" = "issue_comment" ] || [ "${{ github.event.inputs.run_interactive }}" = "true" ]; then PROMPT_PATH="tools/gemini-cli-bot/brain/interactive.md" export ENABLE_PRS="true" fi touch trigger_context.md if [ -n "$TRIGGER_ISSUE_NUMBER" ]; then echo "" > trigger_context.md echo "# Interactive Trigger Context" >> trigger_context.md echo "You were invoked by a user in issue/PR #$TRIGGER_ISSUE_NUMBER." >> trigger_context.md if [ -n "$TRIGGER_COMMENT_ID" ]; then echo "## User Comment" >> trigger_context.md gh api "repos/${{ github.repository }}/issues/comments/$TRIGGER_COMMENT_ID" -q '.body' >> trigger_context.md 2>/dev/null || gh api "repos/${{ github.repository }}/pulls/comments/$TRIGGER_COMMENT_ID" -q '.body' >> trigger_context.md echo "" >> trigger_context.md fi echo "## Issue/PR Context" >> trigger_context.md gh issue view "$TRIGGER_ISSUE_NUMBER" >> trigger_context.md 2>/dev/null || gh pr view "$TRIGGER_ISSUE_NUMBER" >> trigger_context.md echo "" >> trigger_context.md fi cat trigger_context.md "$PROMPT_PATH" tools/gemini-cli-bot/brain/common.md > combined_prompt.md node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml -p "$(cat combined_prompt.md)" - name: 'Run Critique Phase' 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: GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}' GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' GEMINI_MODEL: 'gemini-3-flash-preview' run: | if git diff --staged --quiet; then echo "No changes staged. Skipping critique." echo "[APPROVED]" > critique_result.txt else 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 echo "[APPROVED]" > critique_result.txt else echo "Critique failed, rejected, or did not explicitly approve changes. Skipping PR creation." echo "[REJECTED]" > critique_result.txt fi fi - name: 'Generate Patch' 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: | touch bot-changes.patch touch pr-description.md if [ -f critique_result.txt ] && grep -q "\[APPROVED\]" critique_result.txt && ! grep -q "\[REJECTED\]" critique_result.txt; then git diff --staged > bot-changes.patch else echo "Critique did not approve. Skipping patch generation." fi - name: 'Archive Brain Data' uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4 with: name: 'brain-data' path: | tools/gemini-cli-bot/lessons-learned.md tools/gemini-cli-bot/history/*.csv bot-changes.patch pr-description.md branch-name.txt pr-comment.md pr-number.txt issue-comment.md retention-days: 90 publish: name: 'Publish Artifacts (Archive Layer)' needs: 'reasoning' runs-on: 'ubuntu-latest' if: "github.repository == 'google-gemini/gemini-cli'" # The publish phase is for archiving artifacts and optionally creating PRs. permissions: contents: 'write' pull-requests: 'write' actions: 'write' steps: - name: 'Generate GitHub App Token 🔑' id: 'generate_token' 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 with: app-id: '${{ secrets.APP_ID }}' private-key: '${{ secrets.PRIVATE_KEY }}' owner: '${{ github.repository_owner }}' repositories: '${{ github.event.repository.name }}' permission-contents: 'write' permission-pull-requests: 'write' permission-issues: 'write' - name: 'Checkout' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 with: ref: 'main' fetch-depth: 0 persist-credentials: false - name: 'Download Brain Data' uses: 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' # ratchet:actions/download-artifact@v4 with: name: 'brain-data' path: '${{ runner.temp }}/brain-data/' - name: 'Create or Update PR' 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: GH_TOKEN: '${{ steps.generate_token.outputs.token }}' FALLBACK_PAT: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' run: | if [ -s "${{ runner.temp }}/brain-data/bot-changes.patch" ]; then git config user.name "gemini-cli[bot]" git config user.email "gemini-cli[bot]@users.noreply.github.com" git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" BRANCH_NAME="bot/productivity-updates-$(date +'%Y%m%d%H%M%S')-${{ github.run_id }}" if [ -f "${{ runner.temp }}/brain-data/branch-name.txt" ]; then BRANCH_NAME=$(cat "${{ runner.temp }}/brain-data/branch-name.txt") fi if [[ ! "$BRANCH_NAME" =~ ^bot/ ]]; then echo "Error: Branch name '$BRANCH_NAME' does not start with 'bot/'. Safety abort." exit 1 fi git checkout -b "$BRANCH_NAME" git apply "${{ runner.temp }}/brain-data/bot-changes.patch" git add . if [ -s "${{ runner.temp }}/brain-data/pr-description.md" ]; then git commit -F "${{ runner.temp }}/brain-data/pr-description.md" else git commit -m "🤖 Gemini Bot Productivity Optimizations" fi PR_TITLE="🤖 Gemini Bot Productivity Optimizations" if [ -s "${{ runner.temp }}/brain-data/pr-description.md" ]; then PR_TITLE=$(head -n 1 "${{ runner.temp }}/brain-data/pr-description.md") fi if ! git push origin "$BRANCH_NAME" --force; then echo "Push failed. Retrying with FALLBACK_PAT..." export GH_TOKEN="$FALLBACK_PAT" git remote set-url origin "https://x-access-token:${FALLBACK_PAT}@github.com/${{ github.repository }}.git" git push origin "$BRANCH_NAME" --force fi if ! gh pr view "$BRANCH_NAME" > /dev/null 2>&1; then gh pr create --draft --title "$PR_TITLE" --body-file "${{ runner.temp }}/brain-data/pr-description.md" --head "$BRANCH_NAME" --base main || \ gh pr create --draft --title "🤖 Gemini Bot Productivity Optimizations" --body "Automated changes generated by Gemini CLI Bot." --head "$BRANCH_NAME" --base main else PR_STATE=$(gh pr view "$BRANCH_NAME" --json state --jq .state) if [ "$PR_STATE" = "CLOSED" ]; then NEW_BRANCH_NAME="${BRANCH_NAME}-retry-${{ github.run_id }}" git checkout -b "$NEW_BRANCH_NAME" git push origin "$NEW_BRANCH_NAME" --force gh pr create --draft --title "$PR_TITLE" --body-file "${{ runner.temp }}/brain-data/pr-description.md" --head "$NEW_BRANCH_NAME" --base main || \ gh pr create --draft --title "🤖 Gemini Bot Productivity Optimizations" --body "Automated changes generated by Gemini CLI Bot." --head "$NEW_BRANCH_NAME" --base main fi fi fi - name: 'Post PR/Issue Comment' env: GH_TOKEN: '${{ steps.generate_token.outputs.token }}' TRIGGER_ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}' run: | if [ -s "${{ runner.temp }}/brain-data/issue-comment.md" ] && [ -n "$TRIGGER_ISSUE_NUMBER" ]; then echo "Posting comment to triggering issue #$TRIGGER_ISSUE_NUMBER" # Use REST API (gh api) instead of GraphQL (gh issue comment) to ensure robot identity # while avoiding potential GraphQL-specific authorization hurdles with PATs. gh api "repos/${{ github.repository }}/issues/$TRIGGER_ISSUE_NUMBER/comments" -F body=@"${{ runner.temp }}/brain-data/issue-comment.md" fi if [ -s "${{ runner.temp }}/brain-data/pr-comment.md" ] && [ -f "${{ runner.temp }}/brain-data/pr-number.txt" ]; then PR_NUM=$(cat "${{ runner.temp }}/brain-data/pr-number.txt") # Using GitHub App, so author check is no longer valid against gemini-cli-robot # Skipping author validation here to let the app post. # Use REST API (gh api) for consistency and robot identity gh api "repos/${{ github.repository }}/issues/$PR_NUM/comments" -F body=@"${{ runner.temp }}/brain-data/pr-comment.md" fi