Merge pull request #105 from madhura68/feat/pbi-37-skipped-job-status

feat(PBI-37): SKIPPED-status voor ClaudeJob + worker-idempotency runbook
This commit is contained in:
Janpeter Visser 2026-05-05 23:20:50 +02:00 committed by GitHub
commit 30955462e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 632 additions and 8 deletions

View file

@ -115,3 +115,5 @@ PBI (niet: Feature/Epic) · Story (niet: Ticket) · Sprint Goal (niet: Objective
```bash
npm run lint && npm test && npm run build
```
Worker job-status protocol (wanneer `DONE` / `SKIPPED` / `FAILED`): zie [docs/runbooks/worker-idempotency.md](./docs/runbooks/worker-idempotency.md).

View file

@ -41,7 +41,7 @@ describe('POST /api/cron/cleanup-agent-artifacts', () => {
expect(mockPrisma.claudeJob.deleteMany).not.toHaveBeenCalled()
})
it('200 met juiste secret + deleteMany aangeroepen voor FAILED/CANCELLED ouder dan 7 dagen', async () => {
it('200 met juiste secret + deleteMany aangeroepen voor FAILED/CANCELLED/SKIPPED ouder dan 7 dagen', async () => {
mockPrisma.claudeJob.deleteMany.mockResolvedValue({ count: 5 })
const res = await POST(makeReq({ authorization: 'Bearer ' + SECRET }))
@ -51,7 +51,7 @@ describe('POST /api/cron/cleanup-agent-artifacts', () => {
expect(body.ran_at).toMatch(/^\d{4}-\d{2}-\d{2}T/)
const arg = mockPrisma.claudeJob.deleteMany.mock.calls[0][0]
expect(arg.where.status).toEqual({ in: ['FAILED', 'CANCELLED'] })
expect(arg.where.status).toEqual({ in: ['FAILED', 'CANCELLED', 'SKIPPED'] })
expect(arg.where.finished_at.lt).toBeInstanceOf(Date)
// cutoff should be approximately 7 days ago

View file

@ -19,7 +19,7 @@ export async function cancelJobAction(jobId: string) {
})
if (!job) throw new Error('Job niet gevonden')
if (job.status === 'DONE' || job.status === 'FAILED' || job.status === 'CANCELLED') {
if (job.status === 'DONE' || job.status === 'FAILED' || job.status === 'CANCELLED' || job.status === 'SKIPPED') {
throw new Error('Job is al in eindstatus')
}

View file

@ -15,7 +15,7 @@ export async function POST(request: Request) {
const { count: deleted } = await prisma.claudeJob.deleteMany({
where: {
status: { in: ['FAILED', 'CANCELLED'] },
status: { in: ['FAILED', 'CANCELLED', 'SKIPPED'] },
finished_at: { lt: cutoff },
},
})

View file

@ -32,6 +32,7 @@ const STATUS_CLASS: Record<string, string> = {
DONE: 'bg-status-done text-white border-transparent',
FAILED: 'bg-priority-high text-white border-transparent',
CANCELLED: 'bg-muted text-muted-foreground',
SKIPPED: 'bg-muted/60 text-muted-foreground italic border-transparent',
}
const KIND_LABEL: Record<string, string> = {

View file

@ -7,6 +7,7 @@ export const JOB_STATUS_LABELS: Record<ClaudeJobStatusApi, string> = {
done: 'Klaar',
failed: 'Mislukt',
cancelled: 'Geannuleerd',
skipped: 'Overgeslagen',
}
export const JOB_STATUS_COLORS: Record<ClaudeJobStatusApi, string> = {
@ -16,6 +17,7 @@ export const JOB_STATUS_COLORS: Record<ClaudeJobStatusApi, string> = {
done: 'bg-status-done/15 text-status-done border-status-done/30',
failed: 'bg-status-blocked/15 text-status-blocked border-status-blocked/30',
cancelled: 'bg-muted text-muted-foreground border-border',
skipped: 'bg-muted/50 text-muted-foreground border-border italic',
}
export const JOB_STATUS_ACTIVE = new Set<ClaudeJobStatusApi>(['queued', 'claimed', 'running'])

View file

@ -2,7 +2,7 @@
# Documentation Index
Auto-generated on 2026-05-04 from front-matter and headings.
Auto-generated on 2026-05-05 from front-matter and headings.
## Architecture Decision Records
@ -39,6 +39,7 @@ Auto-generated on 2026-05-04 from front-matter and headings.
| Title | Status | Updated |
|---|---|---|
| [Plan — Auto-PR + selectieve deploy-controle + sync-zicht (end-to-end batch flow)](./plans/auto-pr-deploy-sync.md) | — | — |
| [Docs-restructuur — geoptimaliseerd voor AI-lookup](./plans/docs-restructure-ai-lookup.md) | proposal | 2026-05-02 |
| [PBI Bulk-Create Spec — Docs-Restructure for AI-Optimized Lookup](./plans/docs-restructure-pbi-spec.md) | done | 2026-05-03 |
| [Landing v2 — lokaal & veilig + architectuurdiagram](./plans/landing-local-first.md) | active | 2026-05-03 |
@ -113,6 +114,7 @@ Auto-generated on 2026-05-04 from front-matter and headings.
| [Vercel Deployment](./runbooks/deploy-vercel.md) | `runbooks/deploy-vercel.md` | active | 2026-05-03 |
| [MCP Integration — Scrum4Me Tools](./runbooks/mcp-integration.md) | `runbooks/mcp-integration.md` | active | 2026-05-03 |
| [v1.0 Smoke Test Checklist](./runbooks/v1-smoke-test.md) | `runbooks/v1-smoke-test.md` | active | 2026-05-04 |
| [Worker idempotency & job-status protocol](./runbooks/worker-idempotency.md) | `runbooks/worker-idempotency.md` | active | 2026-05-05 |
| [StoryDialog Profiel](./story-dialog.md) | `story-dialog.md` | active | 2026-05-03 |
| [TaskDialog Profiel](./task-dialog.md) | `task-dialog.md` | active | 2026-05-03 |
| [Scrum4Me — API Test Plan](./test-plan.md) | `test-plan.md` | active | 2026-05-03 |

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 667 KiB

After

Width:  |  Height:  |  Size: 708 KiB

Before After
Before After

View file

@ -0,0 +1,486 @@
# Plan — Auto-PR + selectieve deploy-controle + sync-zicht (end-to-end batch flow)
> Bij merge: dit plan verplaatsen naar `docs/plans/auto-pr-deploy-sync.md`
> conform feedback-memory (plans in `docs/plans/`).
## Context
Drie samenhangende problemen rond de "idee → uitvoeren"-keten:
1. **Worker stopt bij `commit`.** De Scrum4Me NAS-worker werkt lokaal:
commits blijven op de machine staan totdat de gebruiker zelf pusht en
een PR aanmaakt. Voor batch-uitvoer van story-jobs is dit een harde
menselijke gate.
2. **Deploy is alles-of-niets.** `.github/workflows/ci.yml` deployt nu
**elke** push naar `main` automatisch naar productie en **elke** PR
naar preview. `vercel.json` heeft geen `git.deploymentEnabled: false`,
dus Vercel's eigen Git-integratie deployt waarschijnlijk parallel mee
→ dubbele deploys en geen selectieve controle.
3. **Geen zicht op voortgang per Idea/PBI.** Concreet getest geval:
PBI-33 wordt nu de eerste sprint-batch — er is **geen git-voetafdruk**
(geen branch/commit/PR met "PBI-33"), **geen activiteitenlog-entry**,
en geen UI-pagina die per Story toont of er een ClaudeJob loopt, een
commit gepusht is, of een PR open/merged is. De data zit in
`Story.status`, `ClaudeJob.pushed_at/branch/pr_url`,
`Pbi.pr_url/pr_merged_at` — er is alleen geen view die het joint.
Doel: de complete keten **plan → job → commit → push → PR → auto-merge →
deploy** in één coherent ontwerp leggen, met (a) selectieve
deploy-controle als veiligheidsklep en (b) een sync-tab die per Idea
laat zien wat er werkelijk in git/PR-land gebeurd is.
## Vastgelegde keuzes
### Deploy-controle
1. **Mechanisme**: PR-labels (B) + path-filter (C) gecombineerd.
2. **Eigenaar**: GitHub Actions-workflow (A). Vercel Git-integratie uit.
3. **Defaults**: PR → preview, push naar `main` → productie.
4. **Override-richtingen**:
- `skip-deploy` label: voorkomt preview-deploy op een PR.
- `force-deploy` label: forceert deploy ook als path-filter doc-only
zegt.
### Auto-PR (uit IDEA-007-grill)
5. **Triggers in worker**: na elke succesvolle `update_job_status('done')`
pusht de worker; na laatste story van een PBI maakt de worker een PR
aan en activeert auto-merge (SQUASH).
6. **Auth**: `GITHUB_TOKEN` als omgevingsvariabele op de worker; geen UI
of GitHub App in v1.
7. **Foutafhandeling**: push/PR-aanmaak-fail → `update_job_status('failed',
error: …)`; geen force-push, geen automatische retry.
### Interactie tussen beide
8. **Worker-PRs gebruiken hetzelfde labelsysteem als alle andere PRs.**
Default = preview deploy, auto-merge wacht op CI groen, na merge
prod-deploy (mits path-filter zegt "code"). De worker zet **geen**
labels automatisch — als je batch-output zonder preview wilt mergen
moet je `skip-deploy` zelf toevoegen, of preview later uitzetten via
een product-instelling (out-of-scope v1).
9. **Implementatievolgorde**: eerst deploy-controle (infra,
onafhankelijk), daarna auto-PR (afhankelijk van stabiele deploy-flow).
## Architectuur in één plaat
```
auto-merge wacht op
[story-job DONE] ─push branch─┐ deploy-preview groen
▼ │
[laatste story?]──ja──[PR + auto-merge]──CI──┴──merge naar main
[job: ci] altijd
[paths-filter]
├ PR → deploy-preview
│ if code && !skip-deploy
│ || force-deploy
└ push → deploy-production
if code
```
---
## Deel A — Deploy-controle
### A.1 `vercel.json` — Vercel Git-deploy uitzetten
```json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"git": { "deploymentEnabled": false },
"crons": [
{ "path": "/api/cron/expire-questions", "schedule": "0 4 * * *" },
{ "path": "/api/cron/cleanup-agent-artifacts", "schedule": "0 3 * * *" }
]
}
```
Effect: Vercel deployt niet meer automatisch op git-events. Alleen
`vercel deploy` vanuit de workflow (met `VERCEL_TOKEN`) maakt nog
deployments.
### A.2 `.github/workflows/ci.yml` — path-filter + label-checks
Triggers uitbreiden met `workflow_dispatch`:
```yaml
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
target:
type: choice
description: Deploy target
options: [preview, production]
default: preview
```
Nieuwe job vóór de deploy-jobs:
```yaml
changes:
name: Detect deploy-relevant changes
runs-on: ubuntu-latest
needs: ci
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
code:
- 'app/**'
- 'components/**'
- 'lib/**'
- 'actions/**'
- 'stores/**'
- 'prisma/**'
- 'public/**'
- 'package.json'
- 'package-lock.json'
- 'next.config.ts'
- 'tsconfig.json'
- 'vercel.json'
- 'proxy.ts'
- 'middleware.ts'
- '.github/workflows/**'
```
`deploy-preview` if-conditie aanpassen:
```yaml
deploy-preview:
needs: [ci, changes]
if: |
github.event_name == 'pull_request' && (
(needs.changes.outputs.code == 'true'
&& !contains(github.event.pull_request.labels.*.name, 'skip-deploy'))
|| contains(github.event.pull_request.labels.*.name, 'force-deploy')
)
```
`deploy-production` if-conditie aanpassen:
```yaml
deploy-production:
needs: [ci, changes]
if: |
github.ref == 'refs/heads/main'
&& github.event_name == 'push'
&& needs.changes.outputs.code == 'true'
```
Nieuwe `deploy-manual` job voor `workflow_dispatch` met `inputs.target`
`vercel deploy` of `vercel deploy --prod`.
### A.3 GitHub-labels aanmaken
```bash
gh label create skip-deploy --color BFBFBF --description "Preview-deploy overslaan"
gh label create force-deploy --color 0E8A16 --description "Forceer deploy ondanks path-filter"
```
### A.4 Documentatie
`docs/runbooks/deploy-control.md` — triggers, labels, path-filter,
voorbeelden. `CLAUDE.md` § Deployment-regel verwijst naar runbook.
---
## Deel B — Auto-PR (worker → GitHub)
### B.1 Acceptatiecriteria (uit IDEA-007)
- **AC 1 — Push per story**: Na succesvolle `update_job_status('done')`
pusht de worker via HTTPS (`https://$GITHUB_TOKEN@github.com/…`) naar
origin. Push-timestamp via nieuwe MCP-call in `ClaudeJob.pushed_at`.
- **AC 2 — Detectie laatste story**: Nieuwe MCP-call `check_pbi_complete`
retourneert `{ complete: boolean, pbi_id }`.
- **AC 3 — PR aanmaken**: Op `complete: true` POST naar
`/repos/{owner}/{repo}/pulls`; titel/body uit PBI-naam + voltooide
stories; PR-URL via `set_pbi_pr`.
- **AC 4 — Auto-merge activeren**: Direct na PR-aanmaak GraphQL
`enablePullRequestAutoMerge` (SQUASH).
- **AC 5 — Foutafhandeling**: push/PR-fail →
`update_job_status('failed', error)`; PR-URL blijft bewaard voor
handmatige inspectie.
### B.2 Server-side wijzigingen (Scrum4Me-repo)
Velden bestaan al in schema:
- `Product.auto_pr Boolean @default(false)` (regel 176)
- `Pbi.pr_url String?` + `Pbi.pr_merged_at DateTime?` (regel 207208)
- `ClaudeJob.pushed_at DateTime?` + `ClaudeJob.pr_url String?` +
`ClaudeJob.branch String?` (regel 335, 338, 339)
Geen migratie nodig.
Server actions / REST: bestaande `set_pbi_pr` en `mark_pbi_pr_merged`
MCP-tools blijven. Nieuwe action:
- `actions/jobs.ts``recordJobPushedAtAction(jobId)` voor
`pushed_at`-write (als die nog niet via MCP gaat).
### B.3 MCP-laag (`scrum4me-mcp`-repo)
Nieuwe tool:
- `check_pbi_complete(pbi_id) → { complete: boolean, pbi_id }`. Leest
alle ClaudeJobs gelinkt aan PBI; aggregeert status. `complete = true`
als **alle** story-jobs status DONE hebben.
Uitbreiding bestaande tool:
- `update_job_status`: bij `status: 'done'` ook `pushed_at` accepteren
(worker geeft timestamp door).
- `set_pbi_pr`: ongewijzigd, bestaat al.
Schema-drift watchdog (`docs/runbooks/mcp-integration.md`) moet groen
voor merge.
### B.4 Worker-laag (lokaal Claude-CLI worker)
Nieuwe stappen na elke story:
```
1. update_job_status('done', pushed_at: null) ← bestaand
2. git push https://$GITHUB_TOKEN@github.com/$OWNER/$REPO.git $BRANCH
3. record_pushed_at(job_id, now) ← nieuwe MCP-call
4. { complete } = check_pbi_complete(pbi_id)
5. if complete:
prNumber = POST /repos/.../pulls
set_pbi_pr(pbi_id, pr_url)
enablePullRequestAutoMerge(prNumber, MERGE_METHOD: SQUASH)
6. on any HTTP/git failure → update_job_status('failed', error)
```
GITHUB_TOKEN-scope: `repo` voor private, `public_repo` voor public.
Documenteer in worker-readme.
### B.5 Repo-instellingen (handmatig, one-time)
- GitHub repo Settings → General → "Allow auto-merge" → **aanvinken**.
- Branch protection op `main`: required CI checks = `ci`,
`deploy-preview` is **niet** required (kan skipped zijn door label).
---
## Deel C — Interactie & demo-policy
### C.1 Interactie deploy-controle ↔ auto-PR
| Scenario | Preview-deploy | Prod-deploy bij merge |
|--------------------------------------------------|----------------|------------------------|
| Worker maakt PR met code-changes (default) | ✅ runt | ✅ runt |
| Worker maakt PR met `skip-deploy` (manueel toegevoegd) | ❌ skipped | ✅ runt |
| Worker maakt PR met enkel docs-changes (path-filter) | ❌ skipped | ❌ skipped |
| User voegt `force-deploy` toe aan doc-only PR | ✅ runt | ✅ runt (path-filter) of ❌ (doc-only push) |
Auto-merge wacht op required CI checks. `deploy-preview` mag skipped
zijn — branch protection markeert hem niet als required.
### C.2 Demo-policy
Auto-PR-flow draait op de worker, niet vanuit de webapp. Geen
demo-sessie kan deze code triggeren — geen extra proxy.ts of
`session.isDemo`-guards nodig. Wel: `check_pbi_complete` MCP-call moet
`requireWriteAccess` doen (consistent met andere write-MCP-tools), zodat
demo-tokens hem niet kunnen aanroepen.
---
---
## Deel D — Sync-tab op Idea-detail (zicht op voortgang)
### D.1 Wat bestaat al
- `model StoryLog` (`prisma/schema.prisma:251`) met types
`IMPLEMENTATION_PLAN | TEST_RESULT | COMMIT`, plus `commit_hash`,
`commit_message`, `metadata`. **Dit is de activiteitenlog.**
- MCP-tools `log_implementation`, `log_commit`, `log_test_result`
schrijven naar deze tabel.
- UI-component `components/shared/story-log.tsx` rendert
`StoryLogEntry[]` met type-styling.
- `Story.status`, `ClaudeJob.pushed_at/branch/pr_url`,
`Pbi.pr_url/pr_merged_at` zijn al gevuld door bestaande flows.
Geen nieuwe tabellen, geen migraties.
### D.2 Nieuwe tab op `/ideas/[id]`
Voeg vijfde tab **Sync** toe (naast Idee · Grill · Plan · Timeline) op
Idea-detail-page. Alleen zichtbaar als `Idea.status === 'PLANNED'` en
`pbi_id` gevuld.
Layout per tab-content:
- Header: PBI-link + `pr_url` + `pr_merged_at` als badge.
- Per Story (volgorde uit PBI): collapsible card met:
- **Story-header**: code · titel · status-badge.
- **Job-rij**: voor elke `ClaudeJob` (kind=TASK_IMPLEMENTATION) gelinkt
aan een Task van deze Story → status, `branch`, `pushed_at`,
`pr_url`. Toont "geen job" als nog niets gequeued.
- **Activity-log**: `<StoryLog logs={logs} repoUrl={product.repo_url} />`
— bestaande component, ongewijzigd.
### D.3 Server-laag
Nieuwe loader in `app/(app)/ideas/[id]/page.tsx` (of nieuw
`sync-tab-server.ts`):
```ts
async function loadIdeaSyncData(ideaId: string, userId: string) {
// Auth-scope: idea.user_id === userId (M12-keuze 2)
return prisma.idea.findFirst({
where: { id: ideaId, user_id: userId },
include: {
pbi: {
include: {
stories: {
orderBy: { sort_order: 'asc' },
include: {
tasks: { include: { claude_jobs: true } },
logs: { orderBy: { created_at: 'desc' } },
},
},
},
},
},
})
}
```
Server-only. Nooit importeren in client component (zie hardstop
`*-server.ts` regel).
### D.4 Realtime refresh
Sync-tab abonneert op bestaande SSE-streams:
- `app/api/realtime/solo/route.ts``JobPayload` voor job-status-updates
(al uitgebreid met `kind` en `idea_id` per Deel B).
- `app/api/realtime/notifications/route.ts` — voor StoryLog-inserts; als
story_logs nog geen pg_notify-trigger heeft, voeg er een toe (nieuwe
migratie, payload `{op: 'INSERT', entity: 'story_log', id, story_id}`).
Op event → `router.refresh()` of `revalidate` van Sync-tab data.
### D.5 PBI-33 als live testgeval
PBI-33 is **nu** in TODO + gequeued als ClaudeJobs (gebruiker bevestigt:
"taken op TODO gezet en claude-job aangemaakt"). Verwacht gedrag zodra
deze sprint live is:
| Moment | Sync-tab toont |
|----------------------------|-----------------------------------------------|
| Job QUEUED | "Wachtend op worker" |
| Job RUNNING | Status RUNNING + log-entry IMPLEMENTATION_PLAN|
| Worker commit | log-entry COMMIT (hash + message) |
| Worker test | log-entry TEST_RESULT (status) |
| Worker push (Deel B AC 1) | `branch` + `pushed_at` zichtbaar |
| Laatste story → PR | PBI.`pr_url` zichtbaar |
| Auto-merge | PBI.`pr_merged_at` zichtbaar |
Als één van deze niet verschijnt: bug in MCP-tool of worker (niet in
sync-tab zelf).
---
## Bestanden
| Wijziging | Pad |
|-------------------|--------------------------------------------------|
| Edit | `vercel.json` |
| Edit | `.github/workflows/ci.yml` |
| Nieuw | `docs/runbooks/deploy-control.md` |
| Edit | `CLAUDE.md` (verwijzing toevoegen) |
| Nieuw (mcp-repo) | `src/tools/check-pbi-complete.ts` |
| Edit (mcp-repo) | `src/tools/update-job-status.ts` (pushed_at) |
| Edit | `actions/jobs.ts` (optioneel: record-pushed-at) |
| Edit | Worker-script (post-story-hook + PR-aanmaak) |
| Doc | `docs/runbooks/auto-pr-flow.md` (worker-flow) |
| Nieuw | `app/(app)/ideas/[id]/sync-tab-server.ts` |
| Nieuw | `components/ideas/idea-sync-tab.tsx` |
| Edit | `app/(app)/ideas/[id]/page.tsx` (5e tab toevoegen) |
| Migratie | `prisma/migrations/<ts>_story_logs_notify/migration.sql` (pg_notify-trigger op story_logs) |
| Edit | `app/api/realtime/notifications/route.ts` (story_log-payload doorlaten) |
| GitHub (extern) | Labels `skip-deploy`, `force-deploy` aanmaken |
| GitHub (extern) | Repo Settings → "Allow auto-merge" aan |
| Vercel-dashboard | `git.deploymentEnabled: false` actief verifiëren |
## Implementatievolgorde
1. **Deel A — Deploy-controle**
1. `vercel.json` aanpassen
2. `ci.yml` uitbreiden (path-filter, labels, dispatch)
3. Labels op GitHub aanmaken
4. Runbook + CLAUDE.md-verwijzing
5. Test-PR voor elk scenario (zie Verificatie)
2. **Deel D — Sync-tab** (kan parallel met B; alleen DB-reads + UI)
1. `loadIdeaSyncData` server-loader
2. `idea-sync-tab.tsx` component met `<StoryLog>`-hergebruik
3. 5e tab in `app/(app)/ideas/[id]/page.tsx`
4. pg_notify-trigger op `story_logs` + SSE-route uitbreiden
5. **Live test op PBI-33** (sprint loopt al — check of activity
verschijnt zodra worker logs schrijft)
3. **Deel B — Auto-PR**
1. MCP `check_pbi_complete` + `update_job_status(pushed_at)` PR
(parallel-repo, schema-drift-watchdog groen)
2. Worker-hook: push na done, PR + auto-merge bij complete
3. Repo-instelling "Allow auto-merge" aan
4. End-to-end smoke met één test-PBI
## Verificatie
Lokaal:
```bash
npm run lint && npm test && npm run build
```
Workflow-syntax:
```bash
gh workflow view ci.yml
```
End-to-end deploy-controle:
1. **Doc-only PR**`deploy-preview` skipped.
2. **Doc-only PR + `force-deploy`**`deploy-preview` runt.
3. **Code-PR + `skip-deploy`**`deploy-preview` skipped.
4. **Code-PR zonder labels**`deploy-preview` runt.
5. **Push naar `main` met code-change**`deploy-production` runt.
6. **Push naar `main` doc-only**`deploy-production` skipped.
7. **`workflow_dispatch` target=production** → manuele prod.
8. **Vercel dashboard** → geen auto-deploy bij geforceerde test-push.
End-to-end auto-PR:
9. Maak een test-PBI met 1 story + 1 task.
10. Worker draait → na `done`: `pushed_at` gevuld, branch op origin
zichtbaar.
11. `check_pbi_complete``complete: true`.
12. PR verschijnt op GitHub met titel = PBI-naam, body = story-list.
13. Auto-merge actief; CI groen → squash-merge.
14. `mark_pbi_pr_merged` getriggerd door `pull_request: closed`-webhook
(al bestaand) → `Pbi.pr_merged_at` gevuld.
15. Push-event op `main``deploy-production` runt (path-filter ja).
16. **Failure-test**: revoke GITHUB_TOKEN tijdelijk → push faalt →
`update_job_status('failed')` met error; geen PR aangemaakt.
## Out-of-scope (v1)
- UI-toggle voor `auto_pr` per product (veld bestaat, geen UI-wiring).
- GitHub App-installatie (per-repo tokens, scopes-finetuning).
- Multi-repo PBI's (huidig ontwerp: één `repo_url` per PBI).
- Force-push / non-fast-forward retry-flow.
- Notificaties (Slack, e-mail) bij merge of CI-failure.
- Rollback-flow bij gemergende regressie.
- Migratie naar `vercel.ts` (knowledge-update beveelt het aan; later).
- Auto-skip preview-deploy specifiek voor worker-PRs op basis van
product-instelling.

View file

@ -0,0 +1,122 @@
---
title: "Worker idempotency & job-status protocol"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-05
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.
---
## 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/plans/auto-pr-deploy-sync.md` Deel D
(Sync-tab toont per-Story job-status incl. SKIPPED)

View file

@ -59,7 +59,7 @@ export async function getJobsPerDay(
SELECT
COUNT(*) FILTER (WHERE DATE(created_at) = CURRENT_DATE) AS today_count,
COUNT(*) FILTER (WHERE status = 'DONE' AND created_at > NOW() - INTERVAL '7 days') AS done_7d,
COUNT(*) FILTER (WHERE status IN ('DONE','FAILED','CANCELLED') AND created_at > NOW() - INTERVAL '7 days') AS terminal_7d,
COUNT(*) FILTER (WHERE status IN ('DONE','FAILED','CANCELLED','SKIPPED') AND created_at > NOW() - INTERVAL '7 days') AS terminal_7d,
AVG(EXTRACT(EPOCH FROM (finished_at - claimed_at))) FILTER (WHERE status = 'DONE' AND created_at > NOW() - INTERVAL '7 days') AS avg_seconds
FROM claude_jobs
WHERE user_id = ${userId}
@ -69,7 +69,7 @@ export async function getJobsPerDay(
SELECT
COUNT(*) FILTER (WHERE DATE(created_at) = CURRENT_DATE) AS today_count,
COUNT(*) FILTER (WHERE status = 'DONE' AND created_at > NOW() - INTERVAL '7 days') AS done_7d,
COUNT(*) FILTER (WHERE status IN ('DONE','FAILED','CANCELLED') AND created_at > NOW() - INTERVAL '7 days') AS terminal_7d,
COUNT(*) FILTER (WHERE status IN ('DONE','FAILED','CANCELLED','SKIPPED') AND created_at > NOW() - INTERVAL '7 days') AS terminal_7d,
AVG(EXTRACT(EPOCH FROM (finished_at - claimed_at))) FILTER (WHERE status = 'DONE' AND created_at > NOW() - INTERVAL '7 days') AS avg_seconds
FROM claude_jobs
WHERE user_id = ${userId}

View file

@ -7,6 +7,7 @@ const JOB_DB_TO_API = {
DONE: 'done',
FAILED: 'failed',
CANCELLED: 'cancelled',
SKIPPED: 'skipped',
} as const satisfies Record<ClaudeJobStatus, string>
const JOB_API_TO_DB: Record<string, ClaudeJobStatus> = {
@ -16,6 +17,7 @@ const JOB_API_TO_DB: Record<string, ClaudeJobStatus> = {
done: 'DONE',
failed: 'FAILED',
cancelled: 'CANCELLED',
skipped: 'SKIPPED',
}
export type ClaudeJobStatusApi = typeof JOB_DB_TO_API[ClaudeJobStatus]

View file

@ -0,0 +1,6 @@
-- Add SKIPPED to ClaudeJobStatus enum.
-- Used for jobs where the worker correctly detects that the work was already
-- merged before the job ran (verify=EMPTY/DIVERGENT with no net diff).
-- Distinct from FAILED (genuine errors) and DONE (new commit produced).
ALTER TYPE "ClaudeJobStatus" ADD VALUE 'SKIPPED';

View file

@ -37,6 +37,7 @@ enum ClaudeJobStatus {
DONE
FAILED
CANCELLED
SKIPPED
}
enum VerifyResult {