scrum4me-docker/README.md
Madhura68 cb8f48d49e feat: switch source URLs from GitHub to Forgejo
Hybride model (PBI-86 in Scrum4Me): de worker clonet en pusht naar
Forgejo (`origin`); GitHub-PR's ontstaan via een handmatige
promote-Action in Forgejo. Variabele-namen blijven `GH_TOKEN` en
`GH_PRECLONE_REPOS` (historisch); inhoud is voortaan een Forgejo-PAT.

- Dockerfile: MCP_GIT_REPO default →
  git.jp-visser.nl/janpeter/scrum4me-mcp.git
- bin/repo-bootstrap.sh: credential-helper host + clone-URL →
  git.jp-visser.nl
- bin/job-prepare.sh: cache-slug comment example bijgewerkt
- .env.example: documentatie + default `GH_PRECLONE_REPOS` naar
  janpeter/Scrum4Me + janpeter/scrum4me-mcp; instructies omgezet naar
  Forgejo-PAT-flow; `gh pr create` (auto_pr) verwijderd uit comment.
- README.md: internet-egress, token-instructies, clone-URL en
  repo-bootstrap-sectie verwijzen nu naar Forgejo. Promote-flow gelinkt.

gh CLI install blijft in Dockerfile staan (no-op zonder gh-aanroepen,
maar weinig kosten om voor ad-hoc gebruik te bewaren).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:59:11 +02:00

470 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 18080) │ │
│ │ └─ claude -p (per-batch, met MCP via stdio) │ │
│ │ └─ scrum4me-mcp → Neon Postgres │ │
│ │ │ │
│ │ /tmp/job-<id> 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-<id>` |
| `bin/job-cleanup.sh` | Per-job: logs naar `/var/log`, working tree weg |
| `bin/health-server.js` | HTTP-endpoint op 8080 (intern) 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)
- **`Agent` als QTS Shared Folder** op een echte volume (bv. `CACHEDEV1_DATA`).
Niet een `mkdir /share/Agent``/share` zelf is een 16 MB tmpfs en handmatige
directories overleven geen reboot. Aanmaken via Control Panel → Privilege →
Shared Folders → Create. QTS legt dan automatisch de symlink
`/share/Agent → /share/CACHEDEV1_DATA/Agent`.
- Drie subdirs onder die share: `/share/Agent/cache`, `/share/Agent/logs`,
`/share/Agent/state`. Aanmaken via File Station of via SSH na share-creatie.
- Internet-uitgang naar `api.anthropic.com`, `git.jp-visser.nl` (Forgejo HTTPS-clone/push), `cli.github.com` (build-time voor de gh CLI), je Neon-host, `registry.npmjs.org`.
> **Verifieer** vóór je deployt dat `/share/Agent` echt op disk staat:
> ```bash
> ssh admin@<nas> 'ls -la /share/ | grep Agent; df -h /share/Agent'
> ```
> Verwacht een symlink (`l...Agent -> /share/CACHEDEV1_DATA/Agent`) en een
> df-uitvoer met TB-grootte op `cachedev1`/`cachedev2`. Als je hier `tmpfs 16M`
> ziet, is de share geen geregistreerde QTS Shared Folder en zal elke transfer
> >16 MB falen met `scp: write remote ... Failure`.
## 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 → Forgejo → avatar → Settings →
# Applications → Generate New Token; scope
# minimaal `write:repository` op de twee
# repos (janpeter/Scrum4Me + janpeter/
# scrum4me-mcp). Wordt gebruikt voor clone
# en push naar Forgejo. PBI-86 (hybride
# model): `gh pr create` is uit de
# worker-flow verwijderd — de GitHub-PR
# komt via de handmatige promote-Action
# in Forgejo.
# 2. Repo op de NAS plaatsen
ssh admin@nas
cd /share/Agent
git clone https://git.jp-visser.nl/<jij>/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:18080/health
docker compose logs -f
```
> **QNAP-port:** host-poort 8080 is bezet door de QTS-webinterface; daarom
> mapt deze stack standaard `18080:8080`. Override via
> `AGENT_HEALTH_PORT_HOST` in `.env` als je een andere host-poort wilt.
## Snelle redeploy — `bin/deploy-to-nas.sh`
Voor een **bestaande deploy** die je opnieuw wil bouwen + deployen
(bijvoorbeeld na een merge in `scrum4me-mcp` of een aanpassing aan
`CLAUDE.md`):
```bash
# Eenmalig: NAS-target instellen
cp .env.deploy.example .env.deploy
vi .env.deploy # zet NAS_HOST=admin@<nas>
# Daarna: één commando voor de hele cyclus
bin/deploy-to-nas.sh
```
Het script doet:
1. `docker buildx build --platform linux/amd64 --load`
2. `docker save | gzip → scrum4me-agent-runner-amd64.tar.gz`
3. `scp` van tarball + `docker-compose.yml` + (eerste keer) `.env` naar NAS
4. `ssh` op NAS: `docker load` + sanity-check op `.env` + `docker compose up -d --force-recreate`
5. `docker compose logs -f` — lokaal-volgbaar terwijl pre-flight + eerste batch starten
`.env` op de NAS wordt **niet** overschreven als 'ie er al staat. Bij een
verse NAS-installatie wordt 'ie wél geüpload + ge-sed't (NAS_BASE,
AGENT_UID, etc.). Voor de **eerste deploy** of een schoon volume zie de
volledige procedure hieronder.
## Deploy — cross-build vanaf Mac (Apple Silicon → amd64-NAS)
Alternatief voor de in-place build hierboven. Bouw de image op je Mac voor
`linux/amd64`, schrijf 'm naar een tarball, transfer naar de NAS en laad daar.
Handig als de NAS te langzaam is om te builden (npm install op een N5095 met
NAS-storage is traag) of als je geen `git push` wilt voor elke iteratie.
**Vóór je begint:** controleer dat `/share/Agent` een echte QTS Shared Folder is
(zie [Vereisten op de NAS](#vereisten-op-de-nas)). Dat is de meest voorkomende
val.
### 1. Tokens en `.env` op je Mac
Zelfde tokens als in [Deploy](#deploy). Hou er rekening mee dat je **twee
`.env`-bestanden** kunt willen: één voor lokaal Mac-testen
(`AGENT_PLATFORM=linux/arm64`, `AGENT_UID=501`, paths onder `/Users/...`) en
één voor NAS-runtime. De NAS-versie wordt bij stap 4 ge-scp'd en met `sed`
geschikt gemaakt.
### 2. Image bouwen voor amd64
```bash
cd /Users/<jij>/Development/scrum4me-docker
docker buildx build \
--platform linux/amd64 \
--build-arg MCP_GIT_REF=main \
--build-arg CLAUDE_CODE_VERSION=latest \
--build-arg AGENT_UID=1000 \
--build-arg AGENT_GID=1000 \
-t scrum4me-agent-runner:local \
--load \
.
```
`AGENT_UID/GID=1000` zijn de **NAS-UIDs**, niet je Mac-UIDs (vaak `501/20`).
Bij verkeerde UIDs kan de container niet schrijven naar de bind-mounts.
Verifieer architectuur:
```bash
docker image inspect scrum4me-agent-runner:local --format '{{.Architecture}}'
# verwacht: amd64
```
### 3. Image naar tarball
```bash
docker save scrum4me-agent-runner:local | gzip > scrum4me-agent-runner-amd64.tar.gz
shasum -a 256 scrum4me-agent-runner-amd64.tar.gz
```
De tarball is ~580 MB voor de huidige image-size. Hij staat in `.gitignore`,
dus geen risico op accidenteel committen.
### 4. NAS voorbereiden + bestanden transferren
```bash
# .env naar de NAS (de Mac-versie; we patchen path-velden zo direct)
scp .env admin@<nas>:/share/Agent/scrum4me-agent-runner/.env
# image-tarball + bijgewerkte compose
scp scrum4me-agent-runner-amd64.tar.gz docker-compose.yml package.json README.md \
admin@<nas>:/share/Agent/scrum4me-agent-runner/
```
Zet de NAS-specifieke waarden in `.env`:
```bash
ssh admin@<nas> "
source /etc/profile
cd /share/Agent/scrum4me-agent-runner
sed -i \
-e 's|^NAS_BASE=.*|NAS_BASE=/share/Agent|' \
-e 's|^AGENT_BASE=.*|AGENT_BASE=/share/Agent|' \
-e 's|^AGENT_PLATFORM=.*|AGENT_PLATFORM=linux/amd64|' \
-e 's|^AGENT_UID=.*|AGENT_UID=1000|' \
-e 's|^AGENT_GID=.*|AGENT_GID=1000|' \
-e 's|^AGENT_HEALTH_PORT_HOST=.*|AGENT_HEALTH_PORT_HOST=18080|' \
.env
chmod 600 .env
"
```
> **`source /etc/profile` is verplicht** in non-interactieve ssh op QNAP. Zonder
> die regel staat `docker` niet op `$PATH` (Container Station's binary zit onder
> `/share/CACHEDEV*_DATA/.qpkg/container-station/...`) en faalt elk
> `docker`-commando met `command not found`.
### 5. Image laden + container recreaten
```bash
ssh admin@<nas> "
source /etc/profile
set -e
cd /share/Agent/scrum4me-agent-runner
# checksum-verificatie (optioneel maar aan te raden)
sha256sum scrum4me-agent-runner-amd64.tar.gz
# image laden — overschrijft bestaande :local tag
gunzip -c scrum4me-agent-runner-amd64.tar.gz | docker load
# container recreate; --no-build voorkomt onbedoelde NAS-side build
docker compose up -d --no-build --force-recreate agent
docker compose ps
curl -fsS http://localhost:18080/health | head -c 800
"
```
Verwacht in de health-output `cache_free_bytes` met een groot getal
(TB-orde) — dat is je signaal dat `/var/cache` op echte disk zit.
Een waarde rond `16 MB` (`16777216`) betekent dat je per ongeluk
nog steeds op tmpfs draait.
## 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://<nas>:18080/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://git.jp-visser.nl/...` (Forgejo) zonder
prompt werkt.
2. Voor elke repo in `GH_PRECLONE_REPOS` (komma-gescheiden owner/name):
- Bestaat `~/Projects/<name>/.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/<name>/.git`. Worktrees
voor jobs landen vervolgens onder `~/.scrum4me-agent-worktrees/<jobId>/`
zodat de hoofd-clone niet wordt aangeraakt.
Push gaat over dezelfde token: `git push -u origin feat/story-<id>`
slaagt zonder prompt. **`gh pr create` is in PBI-86 (T-1005) verwijderd
uit de worker-flow** — de GitHub-PR ontstaat via een handmatig
getriggerde promote-Action in Forgejo (zie de Scrum4Me-repo
`docs/runbooks/forgejo-hybrid-flow.md`).
## Veelvoorkomende issues
Verzameld uit echte deploy-sessies op een TS-664 met QTS 5.x. Eerst checken
voor je gaat tweaken.
### `/share/Agent` is `tmpfs 16M` — scp faalt op grote files
**Symptoom:**
```
scp: write remote ".../scrum4me-agent-runner-amd64.tar.gz": Failure
```
Eerst 100 % bij kleine files, dan blijvend "Failure" zodra een file de tmpfs
vol maakt.
**Oorzaak:** `/share/Agent` is geen geregistreerde QTS Shared Folder maar een
gewone directory in de root-tmpfs. Na elke reboot is alle inhoud weg en is
`/share/Agent` slechts een entry in de 16 MB `/share`-tmpfs.
**Fix:** maak `Agent` aan via Control Panel → Privilege → Shared Folders →
Create. Daarna verschijnt automatisch `lrwxrwxrwx /share/Agent → /share/CACHEDEV1_DATA/Agent`.
Als die symlink ontbreekt omdat er al een tmpfs-directory `/share/Agent` staat:
```bash
ssh admin@<nas> "
source /etc/profile
docker stop scrum4me-agent 2>/dev/null
docker rm scrum4me-agent 2>/dev/null
rm -rf /share/Agent
ln -s /share/CACHEDEV1_DATA/Agent /share/Agent
mkdir -p /share/Agent/{cache,logs,state,scrum4me-agent-runner}
ls -la /share/ | grep Agent # moet nu een symlink tonen
"
```
> **`rm -rf /share/Agent` is veilig** zolang `/share/Agent` nog tmpfs is —
> alle content stond in RAM en zou de volgende reboot toch verdwenen zijn.
> Maar verifieer eerst met `df -h /share/Agent` dat 't echt tmpfs is.
### `docker: command not found` in non-interactieve ssh
**Symptoom:**
```bash
ssh admin@<nas> 'docker ps'
# sh: line 1: docker: command not found
```
**Oorzaak:** QNAP's `admin`-user heeft `docker` op `$PATH` via
`/etc/profile.d/*.sh` van Container Station. Login-shells laden die scripts;
non-interactieve `ssh user@host 'cmd'` doet dat **niet**.
**Fix:** `source /etc/profile` aan het begin van je remote command:
```bash
ssh admin@<nas> "source /etc/profile && docker ps"
```
### `yaml: control characters are not allowed`
**Symptoom:**
```bash
docker compose down
# yaml: control characters are not allowed
```
**Oorzaak:** een eerdere `scp` van `docker-compose.yml` faalde halverwege en
liet een file met NUL-bytes / partial writes achter. De file-grootte klopt
maar de inhoud is corrupt.
**Fix:** scp opnieuw vanaf je werkstation. Voor het stoppen van een al-
draaiende container heb je de yml niet nodig:
```bash
docker stop scrum4me-agent && docker rm scrum4me-agent
```
Daarna `scp docker-compose.yml admin@<nas>:/share/Agent/scrum4me-agent-runner/`
en pas dan `docker compose up -d --no-build --force-recreate`.
### QTS-console-menu Python-traceback bij ssh-commando
**Symptoom:** lange Python-traceback over `consolemenu_q/prompt_utils.py:31`
en `Inappropriate ioctl for device`, gevolgd door je daadwerkelijke output.
**Oorzaak:** QTS' admin-shell start een Python-TUI (qts-console-menu) die
crasht zonder echte TTY. Niet-fataal — je commando wordt alsnog gerund.
**Fix:** negeer de traceback. Als je 'm écht weg wilt, gebruik
`ssh -tt admin@<nas> '...'` voor een geforceerde pseudo-TTY, maar pas op:
sommige scripts hangen daarop omdat ze interactie verwachten.
### `.env` is weg na `rm -rf /share/Agent`
**Symptoom:** na het opruimen van een tmpfs-`/share/Agent` weigert
`docker compose up`:
```
env file /share/Agent/scrum4me-agent-runner/.env not found
```
**Oorzaak:** `.env` zit in `.gitignore` en wordt nooit door `git pull` of `scp
.` automatisch teruggezet. Bij het wegnuken van een corrupte share verdwijnt-ie
mee.
**Fix:** scp 'm vanaf je werkstation, en patch path-velden op de NAS-zijde
(zie [Deploy — cross-build](#deploy--cross-build-vanaf-mac-apple-silicon--amd64-nas)
stap 4). Zorg dat je `.env` op je werkstation **alle** secrets bevat — vooral
`SCRUM4ME_TOKEN`, dat in een Mac-only `.env` makkelijk ontbreekt omdat
lokale tests soms zonder Scrum4Me-API draaien.
## 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 ~3060 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.