scrum4me-docker/README.md
janpeter visser c090e6c349 feat(bootstrap): GH_TOKEN-based clone of Scrum4Me + scrum4me-mcp
Fixes the 'no GitHub credentials' deadlock observed in the first
NAS-Docker batch run (2 May 2026): scrum4me-mcp's `wait_for_job`
expects a local clone at `~/Projects/<repo-name>` (convention-fallback
in resolveRepoRoot) but the container had no credentials and no clone.
Agent asked the user how to proceed; turn closed without claim.

Changes:
- `.env.example`: GH_TOKEN (fine-grained PAT, repo+PR scope) and
  GH_PRECLONE_REPOS (comma-separated owner/name list, default covers
  Scrum4Me + scrum4me-mcp).
- `bin/repo-bootstrap.sh` (new): runs as agent-user; configures git
  credential-helper with HTTPS oauth2 token, then clones-or-fetches
  each entry in GH_PRECLONE_REPOS into ~/Projects/<name>. Idempotent.
- `bin/entrypoint.sh`: hooks repo-bootstrap before run-agent.sh.
- `Dockerfile`:
  - installs `gh` CLI (used for auto_pr `gh pr create`; reads GH_TOKEN
    from env directly).
  - pre-creates `~agent/Projects` and `~agent/.scrum4me-agent-worktrees`
    so directory-ownership is right from the first boot.
- `README.md`: 'Repo bootstrap (clone-on-start)' section + GH_TOKEN
  step in the deploy checklist; corrects the obsolete 'no push
  credentials' note (agent now pushes feature-branches, gh creates PRs).

Same token covers clone, push and PR-creation — one secret to rotate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:48:57 +02:00

174 lines
8.1 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) │ │
│ │ └─ 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 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/<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: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.
## Health-endpoint
`GET http://<nas>: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/<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.
## 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.