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.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)) ) # The reasoning phase is strictly readonly. permissions: contents: 'read' issues: 'read' actions: 'read' env: GEMINI_CLI_TRUST_WORKSPACE: 'true' steps: - name: 'Determine Checkout Ref' id: 'determine_ref' env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}' run: | REF="${{ github.ref }}" if [ -n "$ISSUE_NUMBER" ]; then PR_HEAD=$(gh pr view "$ISSUE_NUMBER" --repo "${{ github.repository }}" --json headRefName --jq .headRefName 2>/dev/null || echo "") if [ -n "$PR_HEAD" ]; then REF="$PR_HEAD" fi fi echo "ref=$REF" >> "$GITHUB_OUTPUT" - name: 'Checkout' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 with: ref: '${{ steps.determine_ref.outputs.ref }}' 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 to a temp dir so we can selectively restore only persistent state mkdir -p .temp_brain_data gh run download "$LAST_RUN_ID" -n brain-data -D .temp_brain_data || echo "brain-data not found" # Restore only persistent memory files cp .temp_brain_data/tools/gemini-cli-bot/lessons-learned.md tools/gemini-cli-bot/lessons-learned.md 2>/dev/null || true mkdir -p tools/gemini-cli-bot/history/ cp .temp_brain_data/tools/gemini-cli-bot/history/*.csv tools/gemini-cli-bot/history/ 2>/dev/null || true rm -rf .temp_brain_data 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' GEMINI_CLI_HOME: 'tools/gemini-cli-bot' 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/scheduled.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 if [ "$ENABLE_PRS" = "true" ]; then 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 --prompt="$(cat combined_prompt.md)" if [ -n "$TRIGGER_ISSUE_NUMBER" ] && [ ! -s "issue-comment.md" ] && [ ! -s "pr-comment.md" ]; then echo "Agent failed to respond. Generating fallback error message." echo "⚠️ **Gemini CLI Bot failed to generate a response.**" > "issue-comment.md" echo "" >> "issue-comment.md" echo "I encountered an error or failed to generate a complete response to your request. You can check the [GitHub Actions Run Log](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details on what went wrong." >> "issue-comment.md" fi - name: 'Run Critique Phase' if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_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' GEMINI_CLI_HOME: 'tools/gemini-cli-bot' 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 --prompt="$(cat tools/gemini-cli-bot/.gemini/skills/critique/SKILL.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.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.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: 'Determine Checkout Ref' id: 'determine_ref' env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}' run: | REF="main" if [ -n "$ISSUE_NUMBER" ]; then PR_HEAD=$(gh pr view "$ISSUE_NUMBER" --repo "${{ github.repository }}" --json headRefName --jq .headRefName 2>/dev/null || echo "") if [ -n "$PR_HEAD" ]; then REF="$PR_HEAD" fi fi echo "ref=$REF" >> "$GITHUB_OUTPUT" - name: 'Checkout' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 with: ref: '${{ steps.determine_ref.outputs.ref }}' 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.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