From b31d29bc2c1393e2f4c45ccb70f22d54699ba4ac Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Wed, 29 Apr 2026 19:09:02 +0200 Subject: [PATCH] docs(ST-1111.9): document Claude job queue architecture and agent flow Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 6 ++- docs/erd.svg | 2 +- docs/plans/ST-1111-claude-job-trigger.md | 69 ++++++++++++++++++++++++ docs/scrum4me-architecture.md | 50 +++++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 docs/plans/ST-1111-claude-job-trigger.md diff --git a/CLAUDE.md b/CLAUDE.md index 7dad5ee..442ad68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -262,7 +262,7 @@ docs(ST-XXX): document profile feature Scrum4Me heeft een eigen MCP-server in repo [`madhura68/scrum4me-mcp`](https://github.com/madhura68/scrum4me-mcp) die de REST-API als native tools voor Claude Code aanbiedt. Schema's worden gedeeld via een git submodule (`vendor/scrum4me`), niet gedupliceerd. -### Tools beschikbaar in Claude Code (16) +### Tools beschikbaar in Claude Code (18) **Read / context:** - `mcp__scrum4me__health` — service + DB ping @@ -285,6 +285,10 @@ Scrum4Me heeft een eigen MCP-server in repo [`madhura68/scrum4me-mcp`](https://g - `mcp__scrum4me__list_open_questions` — eigen vragen, max 50, recente eerst - `mcp__scrum4me__cancel_question` — asker-only annulering van een eigen open vraag +**Job queue — agent worker mode (M13):** +- `mcp__scrum4me__wait_for_job` — blokkeert ≤600s, claimt atomisch een QUEUED-job via FOR UPDATE SKIP LOCKED; retourneert volledige task-context (implementation_plan, story, pbi, sprint, repo_url). Zet stale CLAIMED-jobs (>30min) eerst terug naar QUEUED. +- `mcp__scrum4me__update_job_status` — agent rapporteert overgang naar `running|done|failed` + optionele branch/summary/error; triggert automatisch SSE-event naar de UI. Auth: Bearer-token moet matchen `claimed_by_token_id`. + ### Prompt - `implement_next_story` (arg: `product_id`) — end-to-end workflow diff --git a/docs/erd.svg b/docs/erd.svg index ec45d14..76d6985 100644 --- a/docs/erd.svg +++ b/docs/erd.svg @@ -1 +1 @@ -

active_product

user

enum:role

user

user

product

enum:status

pbi

product

sprint

assignee

enum:status

story

enum:type

enum:status

product

enum:status

story

sprint

enum:status

product

user

user

product

user

story

task

product

asker

answerer

Role

PRODUCT_OWNER

PRODUCT_OWNER

SCRUM_MASTER

SCRUM_MASTER

DEVELOPER

DEVELOPER

StoryStatus

OPEN

OPEN

IN_SPRINT

IN_SPRINT

DONE

DONE

PbiStatus

READY

READY

BLOCKED

BLOCKED

DONE

DONE

TaskStatus

TO_DO

TO_DO

IN_PROGRESS

IN_PROGRESS

REVIEW

REVIEW

DONE

DONE

LogType

IMPLEMENTATION_PLAN

IMPLEMENTATION_PLAN

TEST_RESULT

TEST_RESULT

COMMIT

COMMIT

TestStatus

PASSED

PASSED

FAILED

FAILED

SprintStatus

ACTIVE

ACTIVE

COMPLETED

COMPLETED

users

String

id

🗝️

String

username

String

email

String

password_hash

Boolean

is_demo

String

bio

String

bio_detail

Bytes

avatar_data

DateTime

created_at

DateTime

updated_at

user_roles

String

id

🗝️

Role

role

api_tokens

String

id

🗝️

String

token_hash

String

label

DateTime

created_at

DateTime

revoked_at

products

String

id

🗝️

String

name

String

code

String

description

String

repo_url

String

definition_of_done

Boolean

archived

DateTime

created_at

DateTime

updated_at

pbis

String

id

🗝️

String

code

String

title

String

description

Int

priority

Float

sort_order

PbiStatus

status

DateTime

created_at

DateTime

updated_at

stories

String

id

🗝️

String

code

String

title

String

description

String

acceptance_criteria

Int

priority

Float

sort_order

StoryStatus

status

DateTime

created_at

DateTime

updated_at

story_logs

String

id

🗝️

LogType

type

String

content

TestStatus

status

String

commit_hash

String

commit_message

Json

metadata

DateTime

created_at

sprints

String

id

🗝️

String

sprint_goal

SprintStatus

status

DateTime

created_at

DateTime

completed_at

tasks

String

id

🗝️

String

title

String

description

String

implementation_plan

Int

priority

Float

sort_order

TaskStatus

status

DateTime

created_at

DateTime

updated_at

product_members

String

id

🗝️

DateTime

created_at

todos

String

id

🗝️

String

title

String

description

Boolean

done

Boolean

archived

DateTime

created_at

DateTime

updated_at

login_pairings

String

id

🗝️

String

secret_hash

String

desktop_token_hash

String

status

String

desktop_ua

String

desktop_ip

DateTime

created_at

DateTime

expires_at

DateTime

approved_at

DateTime

consumed_at

claude_questions

String

id

🗝️

String

question

Json

options

String

status

String

answer

DateTime

answered_at

DateTime

created_at

DateTime

expires_at

\ No newline at end of file +

active_product

user

enum:role

user

user

product

enum:status

pbi

product

sprint

assignee

enum:status

story

enum:type

enum:status

product

enum:status

story

sprint

enum:status

user

product

task

enum:status

claimed_by_token

product

user

user

product

user

story

task

product

asker

answerer

Role

PRODUCT_OWNER

PRODUCT_OWNER

SCRUM_MASTER

SCRUM_MASTER

DEVELOPER

DEVELOPER

StoryStatus

OPEN

OPEN

IN_SPRINT

IN_SPRINT

DONE

DONE

PbiStatus

READY

READY

BLOCKED

BLOCKED

DONE

DONE

ClaudeJobStatus

QUEUED

QUEUED

CLAIMED

CLAIMED

RUNNING

RUNNING

DONE

DONE

FAILED

FAILED

CANCELLED

CANCELLED

TaskStatus

TO_DO

TO_DO

IN_PROGRESS

IN_PROGRESS

REVIEW

REVIEW

DONE

DONE

LogType

IMPLEMENTATION_PLAN

IMPLEMENTATION_PLAN

TEST_RESULT

TEST_RESULT

COMMIT

COMMIT

TestStatus

PASSED

PASSED

FAILED

FAILED

SprintStatus

ACTIVE

ACTIVE

COMPLETED

COMPLETED

users

String

id

🗝️

String

username

String

email

String

password_hash

Boolean

is_demo

String

bio

String

bio_detail

Bytes

avatar_data

DateTime

created_at

DateTime

updated_at

user_roles

String

id

🗝️

Role

role

api_tokens

String

id

🗝️

String

token_hash

String

label

DateTime

created_at

DateTime

revoked_at

products

String

id

🗝️

String

name

String

code

String

description

String

repo_url

String

definition_of_done

Boolean

archived

DateTime

created_at

DateTime

updated_at

pbis

String

id

🗝️

String

code

String

title

String

description

Int

priority

Float

sort_order

PbiStatus

status

DateTime

created_at

DateTime

updated_at

stories

String

id

🗝️

String

code

String

title

String

description

String

acceptance_criteria

Int

priority

Float

sort_order

StoryStatus

status

DateTime

created_at

DateTime

updated_at

story_logs

String

id

🗝️

LogType

type

String

content

TestStatus

status

String

commit_hash

String

commit_message

Json

metadata

DateTime

created_at

sprints

String

id

🗝️

String

sprint_goal

SprintStatus

status

DateTime

created_at

DateTime

completed_at

tasks

String

id

🗝️

String

title

String

description

String

implementation_plan

Int

priority

Float

sort_order

TaskStatus

status

DateTime

created_at

DateTime

updated_at

claude_jobs

String

id

🗝️

ClaudeJobStatus

status

DateTime

claimed_at

DateTime

started_at

DateTime

finished_at

String

branch

String

summary

String

error

DateTime

created_at

DateTime

updated_at

product_members

String

id

🗝️

DateTime

created_at

todos

String

id

🗝️

String

title

String

description

Boolean

done

Boolean

archived

DateTime

created_at

DateTime

updated_at

login_pairings

String

id

🗝️

String

secret_hash

String

desktop_token_hash

String

status

String

desktop_ua

String

desktop_ip

DateTime

created_at

DateTime

expires_at

DateTime

approved_at

DateTime

consumed_at

claude_questions

String

id

🗝️

String

question

Json

options

String

status

String

answer

DateTime

answered_at

DateTime

created_at

DateTime

expires_at

\ No newline at end of file diff --git a/docs/plans/ST-1111-claude-job-trigger.md b/docs/plans/ST-1111-claude-job-trigger.md new file mode 100644 index 0000000..1e8d0ab --- /dev/null +++ b/docs/plans/ST-1111-claude-job-trigger.md @@ -0,0 +1,69 @@ +# ST-1111 — 'Voer uit'-knop met Claude Code job queue + +**Story:** Als developer wil ik op het solo-scherm per task een 'Voer uit'-knop, zodat ik mijn lokale Claude Code-sessie kan inschakelen om de taak uit te voeren. + +**Branch:** `feat/M13-claude-job-queue` + +--- + +## Sub-tasks en commits + +| Task | Commit | +|---|---| +| ST-1111.1 DB: ClaudeJob model + enum + migration | `5274e1e` | +| ST-1111.2 API: ClaudeJob status mappers | `a1b1f69` | +| ST-1111.3 Server actions: enqueue + cancel | `9d9fb4b` | +| ST-1111.4 SSE: ClaudeJob events op solo-stream + initial state | `ece0aa9` | +| ST-1111.5 MCP-tools (scrum4me-mcp repo — aparte PR) | — | +| ST-1111.6 UI: 'Voer uit' + cancel in TaskDetailDialog | `b9c65eb` | +| ST-1111.7 UI: status-pill op SoloTaskCard | `dace427` | +| ST-1111.8 Tests: mappers + actions | `2c2a246` | +| ST-1111.9 Docs | dit bestand | + +--- + +## Architectuur + +### State machine + +``` +QUEUED → CLAIMED → RUNNING → DONE + → FAILED + → CANCELLED (cancel-knop of server action) +CLAIMED → QUEUED (stale cleanup, >30min, via wait_for_job) +``` + +### NOTIFY-pijplijn + +Omdat `claude_jobs` geen row-trigger heeft (zoals `tasks` en `stories`), stuurt de **server action** zelf `pg_notify` via `prisma.$executeRaw`: + +```ts +await prisma.$executeRaw`SELECT pg_notify('scrum4me_changes', ${JSON.stringify(payload)}::text)` +``` + +Voordeel: expliciete controle over het payload-shape (met `type` i.p.v. `entity`). Nadeel: MCP-tools in de `scrum4me-mcp`-repo moeten hun eigen NOTIFY-aanroep hebben bij `update_job_status`. + +### SSE-routing + +De bestaande `/api/realtime/solo`-route herkent nu twee payload-shapes: +- `entity: 'task'|'story'` — bestaande trigger-events +- `type: 'claude_job_enqueued'|'claude_job_status'` — nieuwe job-events + +Job-events worden gefilterd op `user_id + product_id`. Bij connect stuurt de route een `claude_jobs_initial`-event met alle actieve + recente (vandaag) jobs. + +### Idempotency + +`enqueueClaudeJobAction` weigert als `claude_jobs WHERE task_id=X AND status IN (QUEUED, CLAIMED, RUNNING)` bestaat. De client ontvangt `{ error, jobId }` zodat de UI naar de actieve job kan linken in plaats van een nieuw venstertje te openen. + +--- + +## Beslissingen + +**Waarom geen DB-trigger voor NOTIFY?** +De MCP-server claimt jobs via raw SQL (FOR UPDATE SKIP LOCKED); die schrijft ook direct naar de DB. Een trigger zou clean zijn, maar de MCP-tools moeten hoe dan ook hun eigen NOTIFY-payload bouwen voor `update_job_status`. Applicatie-NOTIFY houdt de payloads consistent en expliciet. + +**Waarom `cancelled` verwijderd uit de store?** +Geannuleerde jobs zijn terminaal; het pill-element zou "Geannuleerd" tonen tot de gebruiker een refresh doet. In plaats daarvan wist `handleJobEvent` de entry bij `status === 'cancelled'` zodat de kaart teruggaat naar de "Voer uit"-staat. + +**Auto-clear DONE/FAILED?** +Niet geïmplementeerd in v1. De pill blijft staan totdat de SSE-connectie herstart (refresh, tab-hidden+visible). Acceptabel voor de eerste iteratie. diff --git a/docs/scrum4me-architecture.md b/docs/scrum4me-architecture.md index 2d2182d..01cd734 100644 --- a/docs/scrum4me-architecture.md +++ b/docs/scrum4me-architecture.md @@ -1001,6 +1001,56 @@ Iron-session cookie of Bearer-token (demo). De auth-check loopt éénmalig bij d --- +## Claude job queue (M13 — ST-1111) + +Developers kunnen vanuit de Task Detail Dialog een lokale Claude Code-sessie inschakelen. De job queue zorgt voor coördinatie en realtime-status. + +### State machine + +``` +QUEUED → CLAIMED → RUNNING → DONE + → FAILED + → CANCELLED (door user) +CLAIMED → QUEUED (stale claim cleanup, >30min) +``` + +### ClaudeJob model + +``` +claude_jobs + id, user_id, product_id, task_id + status: ClaudeJobStatus (QUEUED|CLAIMED|RUNNING|DONE|FAILED|CANCELLED) + claimed_by_token_id (FK → api_tokens, nullable) + claimed_at, started_at, finished_at + branch, summary, error + @@index([user_id, status]) + @@index([task_id, status]) + @@index([status, claimed_at]) — voor stale-claim cleanup +``` + +### NOTIFY/LISTEN flow + +``` +UI klikt 'Voer uit' + → enqueueClaudeJobAction() Server Action + → prisma.claudeJob.create(QUEUED) + → prisma.$executeRaw pg_notify('scrum4me_changes', {type:'claude_job_enqueued',...}) + → /api/realtime/solo SSE server-side filter: user_id + product_id + → EventSource.onmessage browser: handleJobEvent() + → useSoloStore.claudeJobsByTaskId map + → SoloTaskCard pill + dialog-footer update +``` + +### Idempotency + +`enqueueClaudeJobAction` weigert een tweede enqueue als er al een job bestaat met `status IN (QUEUED, CLAIMED, RUNNING)`. Teruggestuurde fout bevat het bestaande `jobId` zodat de UI ernaar kan linken. + +### Hybride-ready + +De huidige implementatie verwacht een lokale Claude Code-sessie die `wait_for_job` aanroept vanuit `madhura68/scrum4me-mcp`. Toekomstige uitbreiding naar Vercel Sandbox (serverless agent) vereist alleen een nieuw claim-endpoint — het datamodel en SSE-flow zijn ongewijzigd. + +--- + ## Environment variables | Variabele | Doel | Waar te vinden |