# scrum4me-agent-runner Headless Claude Code worker die de Scrum4Me job-queue (M13) leegtrekt vanaf een QNAP NAS via Container Station. Geen Vercel, geen browser, geen toetsenbord — Claude Code draait als daemon, claimt jobs uit `mcp__scrum4me__wait_for_job`, voert ze uit in een per-job clone, en pusht nooit zelf. ## Architectuur in één plaatje ``` ┌─ QNAP TS-664 (Container Station) ─────────────────────────────┐ │ │ │ ┌─ container: agent-runner ────────────────────────────────┐ │ │ │ PID 1: tini → run-agent.sh (daemon-loop) │ │ │ │ ├─ health-server.js (8080 → host) │ │ │ │ └─ claude -p (per-batch, met MCP via stdio) │ │ │ │ └─ scrum4me-mcp → Neon Postgres │ │ │ │ │ │ │ │ /tmp/job- ephemeral working trees │ │ │ │ /var/cache/repos bare git mirrors (volume) │ │ │ │ /var/cache/npm npm cache (volume) │ │ │ │ /var/log/agent run + job logs (volume) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ /share/Agent/cache /share/Agent/logs /share/Agent/state │ └────────────────────────────────────────────────────────────────┘ │ ▼ HTTPS Neon Postgres (Scrum4Me DB) ▲ │ Vercel ─── Scrum4Me UI (gebruikers enqueueen jobs) ``` Eén `claude -p`-invocation roept intern `wait_for_job` aan totdat de queue leeg is (≈600 s lege block-time → afsluiten). De wrapper start `claude -p` opnieuw zodra hij eindigt, met exponentiële backoff bij fouten. ## Wat zit waar | Bestand | Doel | |--------------------------|-----------------------------------------------------------------| | `Dockerfile` | Ubuntu 22.04 + Node 22 + Claude Code + scrum4me-mcp + scripts | | `docker-compose.yml` | Service-definitie, volumes, env-file, restart-policy, limits | | `package.json` | Npm-dependencies van de runner zelf (alleen `scrum4me-mcp` pin) | | `mcp-config.json` | Claude Code MCP-config (verwijst stdio naar scrum4me-mcp) | | `CLAUDE.md` | Agent-rol-instructies, auto-geladen door `claude -p` | | `bin/entrypoint.sh` | Container-startup: dirs, health-server, daemon-loop | | `bin/run-agent.sh` | Daemon-loop met backoff, exit-code-routing en state-writes | | `bin/check-tokens.sh` | Pre-flight: API-token, OAuth-token, DB-bereikbaarheid | | `bin/job-prepare.sh` | Per-job: bare-fetch + clone-via-reference naar `/tmp/job-` | | `bin/job-cleanup.sh` | Per-job: logs naar `/var/log`, working tree weg | | `bin/health-server.js` | HTTP-endpoint op 8080 dat state.json en marker-files leest | | `bin/rotate-logs.sh` | Compress/cleanup van oude `.log`-bestanden | | `.env.example` | Alle env-vars met uitleg | ## Vereisten op de NAS - Container Station 2+ (Docker compose v2) - Drie shares aangemaakt: `/share/Agent/cache`, `/share/Agent/logs`, `/share/Agent/state` - Of één share `/share/Agent` waaronder de drie subdirs vallen - Internet-uitgang naar `api.anthropic.com`, `github.com`, je Neon-host, `registry.npmjs.org` ## Deploy ```bash # 1. Op je werkstation: token's regelen # a. CLAUDE_CODE_OAUTH_TOKEN → draai `claude setup-token` (browser-flow) # b. SCRUM4ME_TOKEN → log in als de dedicated agent-user in # Scrum4Me, /settings/tokens, label "NAS-runner" # c. DATABASE_URL/DIRECT_URL → Neon dashboard # d. GH_TOKEN → github.com → Settings → Developer settings → # Personal access tokens → Fine-grained. # Repository access op madhura68/Scrum4Me + # madhura68/scrum4me-mcp; Permissions: # Contents (RW), Pull requests (RW), # Metadata (R). Wordt gebruikt voor clone, # push en `gh pr create` (auto_pr). # 2. Repo op de NAS plaatsen ssh admin@nas cd /share/Agent git clone https://github.com//scrum4me-agent-runner.git cd scrum4me-agent-runner # 3. Env aanmaken cp .env.example .env chmod 600 .env vi .env # vul alle waarden in # 4. Build + start docker compose build docker compose up -d # 5. Verifiëren curl http://nas.local:8080/health docker compose logs -f ``` ## Updaten (handmatig, bewust) `SCRUM4ME_TOKEN` of `CLAUDE_CODE_OAUTH_TOKEN` rouleer je via een rebuild: ```bash cd /share/Agent/scrum4me-agent-runner git pull vi .env # nieuwe waarden docker compose build # nieuwe scrum4me-mcp-versie als dat veranderd is docker compose up -d ``` Dezelfde flow voor schema-drift in scrum4me-mcp: pin een nieuwe `MCP_GIT_REF` in `.env` of in `docker-compose.yml`, rebuild. ### Wijzigingen in `docker-compose.yml` (volumes, tmpfs, env_file, ports) > **Let op:** `docker compose restart` herstart alleen het proces in de > bestaande container met de **oude** config. Wijzigingen in volumes, > tmpfs-mounts, env_file of ports worden daarmee **niet** doorgevoerd. Gebruik altijd `--force-recreate` als je `docker-compose.yml` is veranderd: ```bash docker compose up -d --force-recreate agent ``` Verifieer daarna dat `/var/cache` op de NAS-overlay staat en **niet** op tmpfs: ```bash docker exec scrum4me-agent df -h /var/cache # Verwacht: Filesystem op /dev/mapper/cachedev* of een NAS-share # Fout: tmpfs 16M ... (dan is force-recreate niet uitgevoerd) ``` ## Health-endpoint `GET http://:8080/health` retourneert: ```json { "status": "running", // running | idle | unhealthy | token-expired "lastBatchAt": "2026-05-01T12:34:56Z", "lastBatchExit": 0, "consecutiveFailures": 0, "tokenStatus": { "anthropic": "ok", "scrum4me": "ok", "db": "ok" } } ``` HTTP-status: `200` als running/idle, `503` bij token-expired of als de laatste heartbeat ouder is dan 5 minuten. ## Filesystem-grenzen De agent-user heeft geen SSH-keys en geen toegang tot andere shares dan `/share/Agent/*`. Wel een `~/.git-credentials` met de `GH_TOKEN` voor HTTPS-clone/push (zie volgende sectie) — die token is scoped tot de twee configured repos en mag worden gerouleerd door rebuild + redeploy. ## Repo bootstrap (clone-on-start) Bij elke container-start runt `bin/repo-bootstrap.sh` (als de `agent`-user, ná drop-privileges) en zet zo'n setup neer: 1. Configureert git's credential-helper met `GH_TOKEN` zodat `git clone`/`push` naar `https://github.com/...` zonder prompt werkt. 2. Voor elke repo in `GH_PRECLONE_REPOS` (komma-gescheiden owner/name): - Bestaat `~/Projects//.git` al? → `git fetch origin --prune` - Anders → fresh `git clone` Daarna vindt scrum4me-mcp's `resolveRepoRoot` (in `wait_for_job`) de clone via z'n convention-fallback `~/Projects//.git`. Worktrees voor jobs landen vervolgens onder `~/.scrum4me-agent-worktrees//` zodat de hoofd-clone niet wordt aangeraakt. Push gaat over dezelfde token: `git push -u origin feat/story-` slaagt zonder prompt. `gh pr create` (voor producten met `auto_pr=true`) gebruikt dezelfde `GH_TOKEN` via de `gh` CLI's standaard env-detect. ## Bekende grenzen - **Eén actieve job tegelijk.** De wrapper-loop is sequentieel. Voor parallellisme zou je meerdere containers met dezelfde `SCRUM4ME_TOKEN` kunnen draaien — `wait_for_job` gebruikt `FOR UPDATE SKIP LOCKED` dus dat is veilig op DB-niveau, maar dan moet je je `node_modules`-cache per container scheiden. - **OAuth-token: 1 jaar geldig.** Bij verloop schrijft de wrapper een `TOKEN_EXPIRED`-marker en wordt de container `unhealthy`. Geen auto-rotatie. - **`npm install` per job** kost op een N5095 ~30–60 s per Next.js-clone, óók met de pnpm-store. Voor zeer kleine fixes is dat de dominante factor. Kan later vervangen worden door een persistente warm-`node_modules` per repo als dat een knelpunt wordt.