No description
Find a file
Madhura68 318a071f11 chore(deploy): docker resources sizing for Apple M2 host
Verruimt de container-limits van 3 cores / 4 GB (oude QNAP N5095 target)
naar 6 cores / 8 GB voor draaien op een Apple M2 met 16 GB unified
memory. Reservations omhoog naar 1 core / 1 GB.

Aanname: M2 base (8 cores). Pas omlaag aan voor M2 base met 8 GB host
of omhoog voor M2 Pro/Max.

NB: README.md en CLAUDE.md verwijzen nog steeds naar QNAP/N5095. Niet
in deze PR aangepast — apart op te schonen als de QNAP-deploy
definitief wegvalt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 08:05:54 +02:00
bin feat(M13): worker-loop pre-flight quota-check 2026-05-06 04:36:43 +02:00
etc fix: lokale Docker build werkend krijgen 2026-05-02 19:18:35 +02:00
.env.deploy.example feat(deploy): bin/deploy-to-nas.sh voor één-commando redeploy 2026-05-06 01:30:22 +02:00
.env.example fix: cross-host compat — idempotent groupadd, quoted env value, ignore tarballs 2026-05-03 20:28:16 +02:00
.gitattributes fix: lokale Docker build werkend krijgen 2026-05-02 19:18:35 +02:00
.gitignore feat(deploy): bin/deploy-to-nas.sh voor één-commando redeploy 2026-05-06 01:30:22 +02:00
CLAUDE.md feat(M13): worker-loop pre-flight quota-check 2026-05-06 04:36:43 +02:00
docker-compose.yml chore(deploy): docker resources sizing for Apple M2 host 2026-05-06 08:05:54 +02:00
Dockerfile fix: cross-host compat — idempotent groupadd, quoted env value, ignore tarballs 2026-05-03 20:28:16 +02:00
mcp-config.json initial: NAS agent runner setup 2026-05-02 15:43:59 +02:00
package.json Merge pull request #7 from madhura68/fix/qnap-port-and-deploy-docs 2026-05-05 19:55:58 +02:00
README.md feat(deploy): bin/deploy-to-nas.sh voor één-commando redeploy 2026-05-06 01:30:22 +02:00

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, github.com, je Neon-host, registry.npmjs.org.

Verifieer vóór je deployt dat /share/Agent echt op disk staat:

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

# 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/<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):

# 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). Dat is de meest voorkomende val.

1. Tokens en .env op je Mac

Zelfde tokens als in 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

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:

docker image inspect scrum4me-agent-runner:local --format '{{.Architecture}}'
# verwacht: amd64

3. Image naar tarball

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

# .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:

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

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:

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:

docker compose up -d --force-recreate agent

Verifieer daarna dat /var/cache op de NAS-overlay staat en niet op tmpfs:

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:

{
  "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/<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 (voor producten met auto_pr=true) gebruikt dezelfde GH_TOKEN via de gh CLI's standaard env-detect.

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:

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:

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:

ssh admin@<nas> "source /etc/profile && docker ps"

yaml: control characters are not allowed

Symptoom:

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:

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 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.