name: '🧠 Gemini CLI Bot: Brain' on: schedule: - cron: '0 0 * * *' # Every 24 hours workflow_dispatch: inputs: 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.ref }}' cancel-in-progress: true jobs: reasoning: name: 'Brain (Reasoning Layer)' runs-on: 'ubuntu-latest' if: "github.repository == 'google-gemini/gemini-cli'" # 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' }}" run: 'node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml -p "$(cat tools/gemini-cli-bot/brain/metrics.md)"' - name: 'Run Critique Phase' if: "${{ github.event.inputs.enable_prs == 'true' }}" env: GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}' # This token is strictly readonly as enforced by the job-level permissions. 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 # PIPESTATUS[0] captures the exit code of the node command before the pipe if [ "${PIPESTATUS[0]}" -ne 0 ] || grep -q "\[REJECTED\]" critique_output.log; then echo "Critique failed or rejected changes. Skipping PR creation." echo "[REJECTED]" > critique_result.txt else echo "[APPROVED]" > critique_result.txt fi fi - name: 'Generate Patch' if: "${{ github.event.inputs.enable_prs == 'true' }}" run: | touch bot-changes.patch touch pr-description.md if [ -f critique_result.txt ] && grep -q "\[REJECTED\]" critique_result.txt; then echo "Critique rejected. Skipping patch generation." else git diff --staged > bot-changes.patch 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 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: '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' }}" env: GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' run: | if [ -s "${{ runner.temp }}/brain-data/bot-changes.patch" ]; then git config user.name "gemini-cli-robot" git config user.email "gemini-cli-robot@google.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 # SECURITY: Only allow pushing to branches starting with 'bot/' 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 # Use force to update existing PR branches git push origin "$BRANCH_NAME" --force 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 # Create PR if it doesn't exist 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 fi fi - name: 'Post PR Comment' if: "${{ github.event.inputs.enable_prs == 'true' }}" env: GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' run: | 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") # SECURITY: Only allow commenting on PRs authored by the bot PR_AUTHOR=$(gh pr view "$PR_NUM" --json author --jq '.author.login') if [ "$PR_AUTHOR" != "gemini-cli-robot" ]; then echo "Error: PR #$PR_NUM is authored by '$PR_AUTHOR', not 'gemini-cli-robot'. Safety abort." exit 1 fi gh pr comment "$PR_NUM" -F "${{ runner.temp }}/brain-data/pr-comment.md" fi