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:
Event Handler - Next.js application for webhooks, chat interfaces, and job orchestration
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)
.github/workflows/run-job.yml
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
docker/pi-coding-agent-job/entrypoint.sh
# 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
Workflow Trigger Purpose 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.yml Sends 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 main Rebuilds Next.js in container
Why This Architecture?
The repository IS the agent
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