run-one-job.ts spawnde Claude met een hardcoded --output-format text, dus de run-log bevatte alleen Claude's eind-samenvatting — geen zicht op het werk tijdens een job (~6-10 min stilte, dan ineens de samenvatting). - --output-format komt nu uit AGENT_CLAUDE_OUTPUT_FORMAT (default 'stream-json'). stream-json streamt elke tool-call / elk bericht live naar de run-log; --verbose wordt automatisch toegevoegd want print-mode vereist dat bij stream-json. - Zet AGENT_CLAUDE_OUTPUT_FORMAT=text terug voor de oude terse output. - .env.example: nieuwe var gedocumenteerd. stdoutBuf wordt alleen voor de TOKEN_EXPIRED-regexscan gebruikt; de auth-error-strings staan ook binnen de JSON-events, dus detectie werkt ongewijzigd. Niets parseert de output als job-resultaat. Gevolg: de run-log (en de jobs/<job_id>.log symlink uit IDEA-063) wordt JSONL i.p.v. plain text — gebruik jq of een viewer. Log-grootte groeit; rotate-logs.sh dekt dat al af. node --check + type-strip schoon. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| bin | ||
| etc | ||
| .env.deploy.example | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| mcp-config.json | ||
| package.json | ||
| README.md | ||
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)
Agentals QTS Shared Folder op een echte volume (bv.CACHEDEV1_DATA). Niet eenmkdir /share/Agent—/sharezelf 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/Agentecht 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 opcachedev1/cachedev2. Als je hiertmpfs 16Mziet, is de share geen geregistreerde QTS Shared Folder en zal elke transfer16 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 viaAGENT_HEALTH_PORT_HOSTin.envals 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:
docker buildx build --platform linux/amd64 --loaddocker save | gzip → scrum4me-agent-runner-amd64.tar.gzscpvan tarball +docker-compose.yml+ (eerste keer).envnaar NASsshop NAS:docker load+ sanity-check op.env+docker compose up -d --force-recreatedocker 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/profileis verplicht in non-interactieve ssh op QNAP. Zonder die regel staatdockerniet op$PATH(Container Station's binary zit onder/share/CACHEDEV*_DATA/.qpkg/container-station/...) en faalt elkdocker-commando metcommand 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 restartherstart 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:
- Configureert git's credential-helper met
GH_TOKENzodatgit clone/pushnaarhttps://github.com/...zonder prompt werkt. - Voor elke repo in
GH_PRECLONE_REPOS(komma-gescheiden owner/name):- Bestaat
~/Projects/<name>/.gital? →git fetch origin --prune - Anders → fresh
git clone
- Bestaat
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/Agentis veilig zolang/share/Agentnog tmpfs is — alle content stond in RAM en zou de volgende reboot toch verdwenen zijn. Maar verifieer eerst metdf -h /share/Agentdat '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_TOKENkunnen draaien —wait_for_jobgebruiktFOR UPDATE SKIP LOCKEDdus dat is veilig op DB-niveau, maar dan moet je jenode_modules-cache per container scheiden. - OAuth-token: 1 jaar geldig. Bij verloop schrijft de wrapper een
TOKEN_EXPIRED-marker en wordt de containerunhealthy. Geen auto-rotatie. npm installper 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_modulesper repo als dat een knelpunt wordt.