---
title: "Worker idempotency & job-status protocol"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-09
when_to_read: "Vóór het implementeren of debuggen van Claude-CLI-worker logica die `update_job_status` aanroept."
---

# Worker idempotency & job-status protocol

Beschrijft hoe de Scrum4Me-worker `ClaudeJob.status` moet zetten op basis
van `VerifyResult` × git-diff-staat × branch-staat. Doel: voorkom
status-divergentie zoals geconstateerd in de **PBI-33 batch (5-5-2026
22:22)** waarin werk dat al gemerged was via PR #102/#103/#104 leidde
tot inconsistente combinaties van `verify=EMPTY → FAILED` en
`verify=DIVERGENT → DONE`.

---

## Beslissingsboom

Aan het einde van een story-job, ná `verify`-pass:

| `verify_result` | netto diff t.o.v. `origin/main` | branch al gemerged | → `ClaudeJob.status` | `Task.status` |
|---|---|---|---|---|
| `ALIGNED` of `PARTIAL` | nieuwe commit aanwezig | n.v.t. | **`DONE`** | `DONE` |
| `EMPTY` | leeg (niets gewijzigd) | werk zit al op `origin/main` | **`SKIPPED`** | `DONE` |
| `EMPTY` | leeg, maar werk staat **niet** op origin | n.v.t. | **`FAILED`** (`error: "verify produced no output"`) | `IN_PROGRESS` (handmatig onderzoeken) |
| `DIVERGENT` | aanwezig, maar identiek aan al-gemergde branch | ja (PR closed/merged) | **`SKIPPED`** | `DONE` |
| `DIVERGENT` | aanwezig, niet matchend met main | nee | **`FAILED`** (`error: "verify divergent — handmatige review"`) | `IN_PROGRESS` |
| (compile-fail, test-fail, push-fail, exception) | n.v.t. | n.v.t. | **`FAILED`** met concrete `error` | `IN_PROGRESS` |
| (gebruiker drukt cancel) | n.v.t. | n.v.t. | **`CANCELLED`** | `TO_DO` |

### Vuistregels

- **`SKIPPED`** = "geen netto-output, maar geen fout" — werk was al
  gedaan vóór deze job draaide. Task mag op `DONE` omdat het beoogde
  resultaat in main aanwezig is.
- **`FAILED`** is gereserveerd voor échte fouten: code-fouten,
  test-failures, push-fouten, onverklaarde diff. Niet voor
  "implementatie was al gedaan".
- **`DONE`** alleen bij `ALIGNED`/`PARTIAL` mét nieuwe commit op de
  feature-branch. Een lege `DIVERGENT` op een al-gemergde branch is
  géén `DONE`.

---

## StoryLog-verplichting

Tijdens elke job moet de worker `story_logs`-entries schrijven via de
MCP-tools, anders is de Sync-tab leeg:

| Wanneer | MCP-tool | Inhoud |
|---|---|---|
| Bij claim | `log_implementation` | "Start implementatie van T-XXX. Branch X. Plan: …" |
| Per commit | `log_commit` | hash + message + samenvatting van wijzigingen |
| Na verify | `log_test_result` | status `PASSED` of `FAILED` + samenvatting van checks |

In **PBI-33 batch** zijn deze tools **niet** aangeroepen — `story_logs`
voor ST-1208/1209/1210 is leeg. Worker MAG geen job afronden zonder
minimaal één `log_implementation` (start) en één `log_test_result`
(eind).

---

## Idempotency-protocol (vóór schrijven)

Bij claim van een job:

1. Lees `Task.implementation_plan` — beschrijft expliciet welke files
   gewijzigd moeten worden.
2. Vergelijk de huidige `origin/main`-staat met die plan-instructies:
   - Bestaat het bestand al met de beoogde inhoud?
   - Bestaat de migratie al?
   - Bevat de relevante codepad de nieuwe symbolen/types?
3. Bij **volledige hit**: roep `log_implementation` met inhoud "Werk
   reeds aanwezig op origin/main vanaf commit X (Y)." Sla
   verify-stap over en zet `JobStatus.SKIPPED`. Task naar `DONE`.
4. Bij **gedeeltelijke hit**: log de bevindingen via
   `log_implementation` en doe alleen het resterende werk. Eindig met
   `DONE` (`ALIGNED` of `PARTIAL`) als je netto-output hebt.

Dit voorkomt dubbele commits op al-gemergde branches en houdt
`pushed_at` semantisch correct (alleen gevuld als er werkelijk
gepusht is).

---

## Case-study: PBI-33 (5-5-2026 22:22)

PBI-33 ("PLAN_CHAT — gebruikersvragen over plan") werd opnieuw aangemaakt
nadat de feature al via een eerdere batch was gemerged onder cuid-style
story-codes (`ST-bsjoqjnr`, `ST-p6d1odh0`, …). De worker draaide om
22:22 en zag:

- **T-533** (`ST-1208` schema-werk): diff = leeg → `verify=EMPTY` →
  `Job.FAILED` met error "Implementatie reeds voltooid en gemerged".
  Volgens het nieuwe protocol had dit **`SKIPPED`** moeten zijn.
- **T-534…538**: diff niet leeg op feature-branches `feat/story-7pl4dsb6`
  en `feat/story-0vtnydpi` (al-gemergde branches uit eerdere PR's) →
  `verify=DIVERGENT` → `Job.DONE` met `pushed_at=now()`. Volgens het
  nieuwe protocol had dit ook **`SKIPPED`** moeten zijn — branch was
  al closed/merged, geen nieuwe commit.
- **`story_logs` voor ST-1208/1209/1210 is leeg** — geen
  `log_implementation`, geen `log_commit`, geen `log_test_result`.

Drie protocol-overtredingen die we met deze runbook + de nieuwe
`SKIPPED`-status aanpakken.

---

## Config doorgeven aan Claude Code (PBI-67)

`wait_for_job` levert sinds PBI-67 een `config`-object mee in de
response. **De runner** (`scrum4me-docker/bin/run-one-job.ts`) leest deze
config en bouwt per geclaimde job de juiste Claude CLI-flags. Eén
Claude-invocation per job — niet één lange sessie die zelf claimt.

```bash
claude \
  -p "$PROMPT" \
  --model "$MODEL" \
  --permission-mode "$PERMISSION_MODE" \
  ${EFFORT:+--effort $EFFORT} \
  --allowedTools "$ALLOWED_TOOLS" \
  --mcp-config /opt/agent/mcp-config.json \
  --add-dir /opt/agent \
  --output-format text
```

Waar:

| Variabele | Bron in response | Voorbeeld |
|---|---|---|
| `PROMPT` | `getKindPromptText(kind)` met `$PAYLOAD_PATH` vervangen | (kind-prompt-md) |
| `MODEL` | `config.model` | `claude-sonnet-4-6` |
| `PERMISSION_MODE` | `config.permission_mode` | `bypassPermissions` |
| `EFFORT` | `mapBudgetToEffort(config.thinking_budget)` (null = vlag weg) | `medium` |
| `ALLOWED_TOOLS` | `config.allowed_tools.join(',')` | `Read,Edit,…,mcp__scrum4me__update_task_status,…` |

### Claude CLI 2.1.x flag-correctie

De Claude CLI 2.1.x heeft géén numerieke `--thinking-budget` en géén
`--max-turns`. Mapping:

| `config.thinking_budget` | CLI-flag |
|---|---|
| 0 | (geen `--effort` flag) |
| 1-6000 | `--effort medium` |
| 6001-12000 | `--effort high` |
| 12001-24000 | `--effort xhigh` |
| >24000 | `--effort max` |

`config.max_turns` blijft **audit-only** — wordt gesnapshot in
`ClaudeJob.requested_*` voor cost-attribution maar niet doorgegeven aan
Claude. De resolver in `lib/job-config.ts` exporteert
`mapBudgetToEffort(budget)` voor deze mapping.

### Verwachte CLI-aanroep per kind (defaults zonder overrides)

| Kind | Model | thinking_budget | --effort | permission_mode |
|---|---|---|---|---|
| `IDEA_GRILL` | sonnet-4-6 | 12000 | high | acceptEdits |
| `IDEA_MAKE_PLAN` | opus-4-7 | 24000 | xhigh | acceptEdits |
| `PLAN_CHAT` | sonnet-4-6 | 6000 | medium | acceptEdits |
| `TASK_IMPLEMENTATION` | sonnet-4-6 | 6000 | medium | bypassPermissions |
| `SPRINT_IMPLEMENTATION` | sonnet-4-6 | 6000 | medium | bypassPermissions |

### Wie doet wat in de runner-architectuur

| Component | Verantwoordelijkheid |
|---|---|
| `bin/run-agent.sh` | Daemon-loop, exponential backoff, UNHEALTHY-marker, log-rotation, TOKEN_EXPIRED-detectie via exit-code 3 of stdout-regex |
| `bin/run-one-job.ts` | **Claim** (tryClaimJob + LISTEN-fallback 270s), config-resolve (getFullJobContext), payload schrijven, **CLI-flags bouwen**, spawn `claude`, lease-renewal voor SPRINT (setInterval 60s), rollbackClaim bij Claude exit≠0 zonder update_job_status, cleanup |
| `claude` (per invocation) | Voert exclusief de geclaimde job uit. **Mag geen** `wait_for_job`, `check_queue_empty`, of `job_heartbeat` aanroepen — zit niet in `allowed_tools` |

Volledige resolver-uitleg + override-cascade staat in
[job-model-selection.md](./job-model-selection.md). Refactor-plan:
[queue-loop-extraction.md](../plans/queue-loop-extraction.md).

---

## Referenties

- Enum: `prisma/schema.prisma` → `enum ClaudeJobStatus`
- Mapping: `lib/job-status.ts` (DB↔API) en
  `components/shared/job-status.ts` (label + kleur)
- Status-data-cleanup: `app/api/cron/cleanup-agent-artifacts/route.ts`
- KPI-aggregatie: `lib/insights/agent-throughput.ts` (terminal_7d
  inclusief SKIPPED)
- Gerelateerd plan: `docs/old/plans/auto-pr-deploy-sync.md` Deel D
- PBI-67 resolver: `scrum4me-mcp/src/lib/job-config.ts` + `lib/job-config.ts`
  (Sync-tab toont per-Story job-status incl. SKIPPED)
