Skip to main content
The Pope Bot uses a two-layer architecture that separates interactive event handling from autonomous task execution. This design enables the agent to respond instantly to user requests while offloading intensive work to isolated containers on GitHub Actions.

System Overview

The framework consists of two primary layers:
  1. Event Handler - Next.js application for webhooks, chat interfaces, and job orchestration
  2. Docker Agent - Isolated containers running Pi or Claude Code for autonomous task execution

Architecture Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚  β”‚  Event Handler  β”‚ ──1──►  β”‚     GitHub      β”‚                     β”‚
β”‚  β”‚  (creates job)  β”‚         β”‚ (job/* branch)  β”‚                     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚
β”‚           β”‚                           β”‚                              β”‚
β”‚           β”‚                           2 (triggers run-job.yml)       β”‚
β”‚           β”‚                           β”‚                              β”‚
β”‚           β”‚                           β–Ό                              β”‚
β”‚           β”‚                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚           β”‚                  β”‚  Docker Agent   β”‚                     β”‚
β”‚           β”‚                  β”‚  (runs Pi, PRs) β”‚                     β”‚
β”‚           β”‚                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚
β”‚           β”‚                           β”‚                              β”‚
β”‚           β”‚                           3 (creates PR)                 β”‚
β”‚           β”‚                           β”‚                              β”‚
β”‚           β”‚                           β–Ό                              β”‚
β”‚           β”‚                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚           β”‚                  β”‚     GitHub      β”‚                     β”‚
β”‚           β”‚                  β”‚   (PR opened)   β”‚                     β”‚
β”‚           β”‚                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚
β”‚           β”‚                           β”‚                              β”‚
β”‚           β”‚                           4a (auto-merge.yml)            β”‚
β”‚           β”‚                           4b (rebuild-event-handler.yml) β”‚
β”‚           β”‚                           β”‚                              β”‚
β”‚           5 (notify-pr-complete.yml / β”‚                              β”‚
β”‚           β”‚  notify-job-failed.yml)   β”‚                              β”‚
β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                              β”‚
β”‚                                                                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Job Lifecycle

When a job is created (via web chat, Telegram, webhook, or cron), it flows through the following stages:

1. Event Handler Creates Job

The Event Handler receives a job request and:
  • Generates a UUID for the job
  • Creates a descriptive title using the LLM (structured output prevents token leaks)
  • Builds job.config.json containing job metadata, title, description, and optional LLM overrides
  • Creates a job/{uuid} branch via GitHub API
  • Pushes the config file to logs/{uuid}/job.config.json
const jobId = uuidv4();
const branch = `job/${jobId}`;
const title = await generateJobTitle(jobDescription);

const config = { title, job: jobDescription };
if (options.llmProvider) config.llm_provider = options.llmProvider;
if (options.llmModel) config.llm_model = options.llmModel;
if (options.agentBackend) config.agent_backend = options.agentBackend;

2. GitHub Actions Triggers Workflow

Branch creation triggers .github/workflows/run-job.yml:
  • Detects job/* branch pattern
  • Reads thepopebot version from package-lock.json
  • Reads job config for LLM/agent overrides
  • Collects AGENT_* secrets (protected from LLM)
  • Collects AGENT_LLM_* secrets (accessible to LLM for skills)
  • Selects Docker image based on agent backend (Pi or Claude Code)
if: github.ref_type == 'branch' && startsWith(github.ref_name, 'job/')

steps:
  - name: Get thepopebot version
    id: version
    run: |
      VERSION=$(jq -r '.packages["node_modules/thepopebot"].version // "latest"' package-lock.json)
      echo "tag=$VERSION" >> $GITHUB_OUTPUT

  - name: Read job config overrides
    id: job-config
    run: |
      JOB_ID="${{ github.ref_name }}"
      JOB_ID="${JOB_ID#job/}"
      CONFIG_FILE="logs/${JOB_ID}/job.config.json"
      if [ -f "$CONFIG_FILE" ]; then
        echo "llm_provider=$(jq -r '.llm_provider // empty' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
        echo "llm_model=$(jq -r '.llm_model // empty' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
        echo "agent_backend=$(jq -r '.agent_backend // empty' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
      fi

3. Docker Agent Executes Task

The container clones the job branch and executes the task autonomously (see Docker Agent for details).

4. Agent Creates Pull Request

After completing the task:
  • Commits all changes to the job branch
  • Commits session logs to logs/{uuid}/
  • Captures log commit SHA for permalink
  • Removes logs from the branch (prevents merging session data into main)
  • Creates a PR with a permalink to the log commit
# Commit everything
git add -A
git add -f "${LOG_DIR}"
git commit -m "πŸ€– Agent Job: ${TITLE}" || true
git push origin

# Capture log commit SHA, then remove logs
LOG_SHA=$(git rev-parse HEAD)
git rm -rf "${LOG_DIR}"
git commit -m "done." || true
git push origin

# Create PR with log permalink
REPO_SLUG=$(gh repo view --json nameWithOwner -q .nameWithOwner)
LOG_URL="https://github.com/${REPO_SLUG}/tree/${LOG_SHA}/logs/${JOB_ID}"
gh pr create --title "πŸ€– Agent Job: ${TITLE}" \
  --body "πŸ“‹ [View Job Logs](${LOG_URL})"$'\n\n---\n\n'"${JOB_DESCRIPTION}" \
  --base main || true

5. Auto-Merge and Notification

Two workflows run after PR creation: auto-merge.yml - Checks merge policy:
  • Validates AUTO_MERGE is enabled
  • Checks modified files against ALLOWED_PATHS configuration
  • Squash merges if all checks pass
notify-pr-complete.yml (after merge) or notify-job-failed.yml (on failure):
  • Gathers job data (title, logs, PR number)
  • Sends notification to event handler via /api/github/webhook
  • Event handler delivers notification through original channel (web chat, Telegram, etc.)

File Structure

After running npx thepopebot init, your project has this structure:
/
β”œβ”€β”€ .github/workflows/
β”‚   β”œβ”€β”€ auto-merge.yml             # Auto-merges job PRs
β”‚   β”œβ”€β”€ build-image.yml            # Builds Docker image to GHCR
β”‚   β”œβ”€β”€ notify-job-failed.yml      # Failure notifications
β”‚   β”œβ”€β”€ notify-pr-complete.yml     # Success notifications
β”‚   β”œβ”€β”€ rebuild-event-handler.yml  # Rebuilds on push to main
β”‚   └── run-job.yml                # Runs Docker agent
β”œβ”€β”€ .pi/
β”‚   β”œβ”€β”€ extensions/                # Pi extensions (env-sanitizer)
β”‚   └── skills/                    # Symlinks to skills/active/
β”œβ”€β”€ config/
β”‚   β”œβ”€β”€ SOUL.md                    # Agent personality
β”‚   β”œβ”€β”€ JOB_PLANNING.md            # Event handler prompt
β”‚   β”œβ”€β”€ JOB_AGENT.md               # Agent runtime prompt
β”‚   β”œβ”€β”€ CRONS.json                 # Scheduled jobs
β”‚   └── TRIGGERS.json              # Webhook triggers
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ api/[...thepopebot]/       # Catch-all API route
β”‚   └── stream/chat/               # Chat streaming route
β”œβ”€β”€ docker/
β”‚   β”œβ”€β”€ pi-coding-agent-job/       # Pi agent Dockerfile
β”‚   β”œβ”€β”€ claude-code-job/           # Claude Code Dockerfile
β”‚   └── event-handler/             # Event handler Dockerfile
β”œβ”€β”€ logs/                          # Per-job directories
β”œβ”€β”€ skills/                        # Available skills
β”‚   └── active/                    # Activated skills (symlinks)
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ .env
└── package.json
All core logic lives in the thepopebot NPM package. Your project contains only configuration files and thin Next.js wiring.

GitHub Actions Workflows

WorkflowTriggerPurpose
run-job.ymljob/* branch createdRuns Docker agent container
auto-merge.ymlPR opened from job/*Checks merge policy and auto-merges
notify-pr-complete.ymlAfter auto-merge.ymlSends success notification
notify-job-failed.ymlrun-job.yml failsSends failure notification
build-image.ymlPush to main (docker changes)Builds and pushes Docker image
rebuild-event-handler.ymlPush to mainRebuilds Next.js in container

Why This Architecture?

Every action your agent takes is a git commit. You can see exactly what it did, when, and why. If it makes a mistake, revert it. Want to clone your agent? Fork the repo β€” code, personality, scheduled jobs, and full history all go with your fork.
Every GitHub account comes with free cloud computing time (2,000 minutes/month on free tier, 3,000 on Pro). The Pope Bot uses GitHub Actions to run your agent jobs, so you’re using compute you already have.
The agent modifies its own code through pull requests. Every change is auditable, every change is reversible. You stay in control through the auto-merge policy and path restrictions.
Job containers are ephemeral and isolated. Protected secrets (like your GitHub token) are filtered from the agent’s bash environment. Each job runs in a clean environment with no persistent state.

Next Steps

Event Handler

Deep dive into the Event Handler layer: API routes, chat interfaces, and job orchestration

Docker Agent

Learn how jobs execute in isolated containers with Pi or Claude Code

Skills System

Extend your agent with custom skills and tools

Build docs developers (and LLMs) love