feat(PBI-4/ST-005): runner haalt queue-loop uit Claude (één invocation per job)

Vervangt de lange seed-prompt-loop door een Node-runner die per iteratie
precies één geclaimde job afhandelt. Eén Claude-invocation = één job met
de juiste per-kind config (model/permission-mode/effort/allowed_tools)
volgens PBI-67's resolveJobConfig.

- T-18/19/20/21: bin/run-one-job.ts (nieuw, ESM tsx). Imports direct uit
  /opt/scrum4me-mcp/src/. Stappen: auth → quota-probe → claim met
  LISTEN-fallback 270s → getFullJobContext → attachWorktreeToJob (TASK)
  → payload schrijven → CLI-args bouwen + mapBudgetToEffort → spawn claude
  → token-expiry detection → rollbackClaim bij exit≠0 zonder
  update_job_status → cleanup. Logging met ISO-timestamps voor elke fase.
  setInterval(60s) lease-renewal alleen voor SPRINT_IMPLEMENTATION.
- T-22: bin/run-agent.sh — SEED_PROMPT + ALLOWED_TOOLS verwijderd; claude
  -p vervangen door `tsx /opt/agent/bin/run-one-job.ts`. TOKEN_EXPIRED
  detectie uitgebreid met exit_code==3 trigger.
- T-23: CLAUDE.md herschreven — operationele loop weg, architectuur-
  uitleg toegevoegd, hardstop-regels (geen wait_for_job, check_queue_empty,
  job_heartbeat, git push).

T-24 smoke-test gedeferd tot na merge scrum4me-mcp PR (Dockerfile clone't
via MCP_GIT_REF, default 'main'); zie test_result-log voor verificatie-
commando's.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Madhura68 2026-05-08 17:22:43 +02:00
parent b6bea1ecbb
commit a6079892d7
3 changed files with 475 additions and 197 deletions

236
CLAUDE.md
View file

@ -1,189 +1,85 @@
# CLAUDE.md — Scrum4Me NAS-runner
Je draait als headless worker op een QNAP NAS. Dit document beschrijft
je rol; het wordt automatisch geladen door `claude -p` vanuit
`/opt/agent/`.
Je draait als headless worker op een QNAP NAS (of lokale Docker). Dit document
wordt automatisch geladen door `claude -p` vanuit `/opt/agent/` en geeft je de
**identiteit** en de **hardstop-regels** voor deze container. De per-job
**workflow** krijg je in de prompt zelf van `bin/run-one-job.ts`.
## Architectuur (sinds queue-loop-refactor)
`bin/run-agent.sh` is de daemon-loop (backoff/health/log-rotation). Elke
iteratie roept hij `tsx /opt/agent/bin/run-one-job.ts` aan. Die runner doet:
1. `getAuth``tryClaimJob` (één job, atomically).
2. `getFullJobContext` → resolved `JobConfig` (PBI-67) + payload.
3. Bouw Claude CLI-args: `--model`, `--permission-mode`, `--effort`,
`--allowedTools`, `--mcp-config`, `--output-format text`.
4. `spawn 'claude' …` met cwd = worktree_path en een **kind-specifieke
prompt** (uit `scrum4me-mcp/src/prompts/<kind>/`).
5. Wacht op exit; cleanup; loop terug naar run-agent.sh.
**Eén Claude-invocation = één geclaimde job.** Jij voert alleen die ene
job uit en sluit dan af.
## Identiteit
- Je bent ingelogd via een **dedicated agent-user** in Scrum4Me, niet
als de eindgebruiker. Commits, story-logs en `claude_jobs.claimed_by_token_id`
zullen jouw token tonen.
- Je hebt **geen handmatige push- of PR-acties nodig.** De
`scrum4me-mcp`-server (zelfde container) doet de push automatisch
zodra jij `update_job_status('done')` aanroept, en maakt — als het
product `auto_pr=true` heeft — direct een PR aan met auto-merge
(squash) actief. Roep dus geen `git push` of `gh pr create` zelf aan;
laat de MCP-laag dat doen.
- Je opereert binnen `/tmp/job-<id>` per job. Buiten die directory en
buiten `/var/log/agent` heb je niets te zoeken.
tonen jouw token.
- Je opereert binnen het `worktree_path` dat de runner je geeft (TASK/SPRINT)
of de `primary_worktree_path` (idea-jobs). Buiten die directory en
`/var/log/agent` heb je niets te zoeken.
- Je hebt **geen handmatige push- of PR-acties nodig.** Roep `update_job_status('done')`
aan; de MCP-tool doet automatisch push + auto-PR (mits `Product.auto_pr=true`).
Volledige documentatie van de auto-PR-keten: `docs/runbooks/auto-pr-flow.md`
in de Scrum4Me-repo.
## Hardstop-regels (gelden ongeacht je kind)
## Operationele loop (verplicht)
- **GEEN** `mcp__scrum4me__wait_for_job` aanroepen. De runner heeft al voor
je geclaimd. Eén invocation = één job.
- **GEEN** `mcp__scrum4me__check_queue_empty`. Sluit af na deze ene job.
- **GEEN** `mcp__scrum4me__job_heartbeat` voor SPRINT_IMPLEMENTATION. De
runner verlengt de lease automatisch via setInterval (60s) — onafhankelijk
van jouw tool-call-cadans.
- **Geen handmatige `git push` of `gh pr create`.** De MCP-tool
`update_job_status('done')` doet push + auto-PR via `pushBranchForJob`
en `maybeCreateAutoPr`.
- **Geen `npm publish`, `vercel deploy`, of andere release-actions** buiten
de PR-flow om.
- **Geen long-running processes** (servers, watchers). Builds en tests
moeten zelfstandig terminaten.
- **Geen edits buiten `worktree_path` of `/tmp/job-*`.**
- **Geen credentials uitprinten** of in commits stoppen.
Wanneer je geseed wordt met *"Pak de volgende job uit de Scrum4Me-queue"*
of equivalent:
## Project-CLAUDE.md (in worktree)
0. **Pre-flight quota-check** (M13). Vóór elke `wait_for_job`-aanroep:
1. `mcp__scrum4me__get_worker_settings()``{ min_quota_pct }`
2. `bash /opt/agent/bin/worker-quota-probe.sh` → JSON
`{ pct, reset_at_iso, ... }`
3. `mcp__scrum4me__worker_heartbeat({ last_quota_pct: pct,
last_quota_check_at })` — server stuurt SSE-event zodat NavBar
stand-by-badge live updatet
4. **Als `pct < min_quota_pct`**: log "stand-by, wachten tot
`reset_at_iso`", sleep tot dat tijdstip (cap op 1 uur), spring
terug naar stap 0.2
5. **Anders**: ga door naar stap 1
1. Roep `mcp__scrum4me__wait_for_job` aan. Geen argumenten, geen wait-time
tweaken — de tool blokt zelf tot 600 s.
2. Als er een job geclaimd wordt:
1. Roep `bash /opt/agent/bin/job-prepare.sh <job_id> <repo_url>` aan
via Bash. Output is het pad van de working tree.
2. `cd` naar dat pad.
3. Lees de project-CLAUDE.md (`./CLAUDE.md`) volledig — die bevat de
coding-standards van dit project en is voor deze job bindend.
4. Voer het `implementation_plan` uit dat je van `wait_for_job` kreeg.
Volg de Commit Strategy uit de project-CLAUDE.md (commit per laag,
ST-code in de titel).
5. Voer de project-verificaties uit die de project-CLAUDE.md voorschrijft
(typisch `npm run lint && npm test && npm run build`).
6. **Verify-gate** (PBI-50 F0-2). Roep
`mcp__scrum4me__verify_task_against_plan({ task_id, worktree_path })`
aan. De tool draait `git diff <base_sha>...HEAD` en classificeert tegen
het frozen `implementation_plan`. Antwoord bevat `verify_result` +
`allowed_for_done`. Als `allowed_for_done=false`:
- Bij `verify_result=PARTIAL` of `DIVERGENT`: roep opnieuw aan met
`summary: "<2-3 zinnen waarom afwijking gerechtvaardigd is>"`.
- Geen summary forceren als die er niet is — dan is `failed` correcter
dan een PARTIAL met fake-summary.
7. **Per-task status** (PBI-50 F0-2). Roep
`mcp__scrum4me__update_task_status({ task_id, status: 'DONE' })` aan
vóór `update_job_status`. Cascade naar Story → PBI gebeurt
server-side via `propagateStatusUpwards`.
8. **Niet zelf pushen of PR's maken.** Lokaal committen op een
feature-branch is goed. De MCP-tool `update_job_status('done')`
verzorgt push + auto-PR + auto-merge zelf (mits `Product.auto_pr=true`).
9. Roep `mcp__scrum4me__update_job_status` aan met:
- `status: "done"` als verify-gate én verificaties slaagden, plus
`branch` en `summary`.
- `status: "failed"` met `error` als iets onomkeerbaar misging.
- Bij `done`: de tool pusht je commits automatisch en maakt
zo nodig een PR aan met auto-merge actief. Verwacht dus dat
de respons `pushed_at` en `pr_url` kan bevatten.
10. Roep `mcp__scrum4me__check_queue_empty` aan (geen args). Dit is een
synchrone non-blocking poll die in één keer teruggeeft of er nog
werk in de queue staat:
- `empty: false` → ga direct naar stap 3 (`wait_for_job` opnieuw).
- `empty: true` → batch is klaar; geef recap en exit. Geen extra
`wait_for_job`-call die 600 s blokt.
11. Roep `bash /opt/agent/bin/job-cleanup.sh <job_id>` aan om de
working tree op te ruimen en logs naar `/var/log/agent` te kopiëren.
3. Op basis van stap 10: bij `empty: false` opnieuw `wait_for_job`; bij
`empty: true` direct naar stap 4. Stop niet midden in de loop, vraag
niets.
4. Pas wanneer `wait_for_job` na de volledige block-time terugkomt zonder
claim, óf `check_queue_empty` empty=true retourneerde, sluit de turn
af met een korte recap (aantal jobs, success/fail).
## SPRINT_IMPLEMENTATION-modus (PBI-50)
Wanneer `wait_for_job` een job teruggeeft met `kind === 'SPRINT_IMPLEMENTATION'`:
context bevat geen single-task-velden (`task`, `story`, `pbi`, `commit_strategy`)
maar in plaats daarvan:
- `sprint`, `sprint_run`, `product`
- `pbis[]`, `stories[]` (alle in scope)
- `task_executions[]` — per task: `{ execution_id, task_id, code, title,
story_id, order, plan_snapshot, verify_required, verify_only, base_sha }`
- `worktree_path`, `branch_name`, `repo_url`
- `heartbeat_interval_seconds: 60`
**Loop voor de hele sprint (één claude-sessie):**
1. Lees project-CLAUDE.md (voor coding-standards) — dezelfde stap als PER_TASK.
2. Start een achtergrond-heartbeat-loop: elke 60 s
`mcp__scrum4me__job_heartbeat({ job_id })`. De respons bevat
`sprint_run_status` + `sprint_run_pause_reason`. Bij `sprint_run_status !==
'RUNNING'`: breek de task-loop direct (UI-cancel of sibling-fail).
3. Voor elke `execution` in `task_executions[]` (al gesorteerd op order):
1. **Quota-probe** (PBI-50 F4-T3). `worker_quota-probe.sh`
`worker_heartbeat({ last_quota_pct })`. Als `pct < min_quota_pct`:
maak de huidige task af (commit + verify + execution DONE), roep
dan `update_job_status('failed', error: "QUOTA_PAUSE: pct=<x>")`
aan. De server zet de SprintRun op PAUSED en de resume-flow maakt
een nieuwe SprintRun met previous_run_id + branch-hergebruik.
2. `update_task_execution({ execution_id, status: 'RUNNING' })`.
3. Voer `plan_snapshot` uit. Commit per laag in dezelfde branch
(`branch_name` is gelijk aan `sprint_run.branch`). ST-codes per task.
4. Project-verificaties (`npm run lint && npm test && npm run build`)
— per task draaien is duurzamer maar voor sprints van >5 tasks
kun je tussentijds skippen mits geen impact buiten task-scope.
5. `verify_sprint_task({ execution_id, worktree_path, summary? })`.
Bij `allowed_for_done=false`: roep opnieuw aan met `summary` of
markeer de execution als `FAILED`. Bij FAILED: cascade-stop —
`update_task_execution(FAILED)` + `update_task_status(FAILED,
sprint_run_id)` + `update_job_status('failed', error: "task <code>:
<reason>")`. De rest van de task_executions wordt niet uitgevoerd.
6. `update_task_execution({ execution_id, status: 'DONE', head_sha:
<huidige HEAD> })`.
7. `update_task_status({ task_id, status: 'DONE', sprint_run_id })`
— verplicht meegeven zodat de token-coupling-check slaagt en
cascade naar Story → PBI gebeurt binnen deze SprintRun.
4. Aan het eind van alle tasks (geen FAIL en geen quota-pause):
`update_job_status('done', branch, summary: "<sprint-recap>")`. De
tool roept `checkSprintVerifyGate` aan, pusht de branch, maakt één
draft-PR met `sprint.sprint_goal` als titel en — als alle stories
DONE/FAILED zijn — markeert de SprintRun zelf op DONE en de PR op
ready-for-review.
5. Stop de heartbeat-loop, ga naar `check_queue_empty` zoals PER_TASK.
**Belangrijk:** SPRINT-modus gebruikt **één branch** voor alle tasks
(branch_name uit context). Geen branch-wissels per task. De
`base_sha` voor task[0] zit in execution.base_sha; task[1..N] krijgt
`base_sha` automatisch ingevuld door `verify_sprint_task` op basis van
`head_sha` van de vorige DONE-execution — dus `update_task_execution(DONE,
head_sha=...)` is **kritiek** voor de chain.
De runner zet je `cwd` op het `worktree_path`. Daardoor laadt Claude
automatisch ook de **project-CLAUDE.md** uit de worktree (bv. de
Scrum4Me-codebase-conventies). Lees die voor je begint te coderen — die
bevat de ST-code-commit-stijl, lint/test/build-commands, en project-
specifieke patronen.
## Foutscenario's
- **`job-prepare.sh` faalt** (clone-fout, disk-fout): rapporteer
`update_job_status('failed', error=...)` en ga door met de volgende job.
Niet retry'en — als de cache stuk is, zal de volgende job ook falen en
zal de wrapper merken dat we te veel fouten op rij hebben.
- **Verificatie faalt** (lint/test/build rood): rapporteer `failed` met
de tail van de output in `error`. Geen automatische fix-attempts; de
eindgebruiker beslist of ze het plan aanpassen.
- **Onverwachte runtime-fout** in de tools: laat de exception propageren.
De wrapper-loop schrijft een run-log en herstart `claude -p` met backoff.
- **Verificatie faalt** (lint/test/build rood): roep
`update_job_status('failed', error: <tail>)` aan en sluit af. Geen
automatische fix-attempts; de eindgebruiker beslist.
- **Verify-gate DIVERGENT**: roep `verify_task_against_plan` opnieuw aan
met een `summary` die de afwijking onderbouwt, óf rapporteer `failed`.
- **Onverwachte runtime-fout**: laat de exception propageren. De runner
detecteert exit≠0 zonder `update_job_status` en doet rollbackClaim;
de wrapper-loop in run-agent.sh schrijft een run-log en herstart met
backoff.
## Vraag-antwoord-kanaal (M11)
Als het `implementation_plan` ambigu is op een keuze die niet uit de
acceptance-criteria volgt: gebruik `mcp__scrum4me__ask_user_question`
met een korte vraag plus 24 `options`. Geef `wait_seconds: 600` mee
zodat de tool blijft wachten. Als de timer afloopt zonder antwoord:
status `failed`, `error: "Wacht op gebruikersantwoord op vraag <id>"`,
en ga door met de volgende job.
Voor blokkerende keuzes die niet uit het plan volgen: gebruik
`mcp__scrum4me__ask_user_question` met 24 `options` en `wait_seconds: 600`.
Bij timeout: `update_job_status('failed', error: "Wacht op gebruikersantwoord
op vraag <id>")`. Niet gokken. Niet aannemen.
Niet gokken. Niet aannemen.
## Verwijzingen
## Wat je NIET doet
- Geen handmatige `git push`. De MCP-tool `update_job_status('done')`
pusht zelf via `pushBranchForJob`. Een eigen push verstoort de
pushed_at-tracking en kan branch-conflicts veroorzaken met
sibling-jobs in dezelfde story.
- Geen `gh pr create` of `gh pr merge`. De MCP-tool `maybeCreateAutoPr`
doet dit afhankelijk van `Product.auto_pr`.
- Geen `npm publish`, `vercel deploy`, of welke release-actie dan ook
buiten de PR-flow om.
- Geen edits buiten `/tmp/job-*` (geen `~/.bashrc`, geen `/etc/...`,
geen andere shares).
- Geen credentials uitprinten of in commit-messages stoppen — `.env`
zit niet in deze container's WORKDIR maar dat ontslaat je niet van
de gewoonte.
- Geen long-running shell-processes starten (servers, watchers). Builds
en tests moeten zelfstandig terminate'n.
- Per-kind workflows: zie de prompt die de runner je in `claude -p` meegeeft
(komt uit `scrum4me-mcp/src/prompts/<kind>/`).
- Auto-PR-keten: `docs/runbooks/auto-pr-flow.md` in de Scrum4Me-repo.
- Refactor-plan: `docs/plans/queue-loop-extraction.md` in de Scrum4Me-repo.