feat(IDEA-213): managed queue dispatch — opslag, guards en herstel (ST-1590) #251
No reviewers
Labels
No labels
severity/s3
severity/s4
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
janpeter/Scrum4Me!251
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/idea-213-dispatch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Draft — niet mergen. Geopend om de nieuwe CI-job
queue-dispatchte laten draaien; IP-10 t/m IP-14 van ST-1590 zijn nog niet uitgevoerd.Inhoud
Scrum4Me-deel van IDEA-213 (managed queue dispatch), IP-02/06/07/09: opslag, rollen, guards, pool-transfer, Task-binding, worker-observatie en geverifieerd herstel. Zusterbranches
feat/idea-213-dispatchstaan in scrum4me-mcp, scrum4me-shared, scrum4me-workers, scrum4me-docker, s4m-queue en Ops-dashboard.Review-fixes (code-review 2026-09-20)
98ca282a—Task.dispatch_request_idontbrak inschema.prisma;migrate diffstelde een DROP van kolom, index en FK voor. Na regenereren geen dispatch-drift meer (gemeten tegen postgres:17).2ff30bf75171db911c117d6debd45bdf— 8 nieuwe integratietests op echte SQL onder echte rollen (SECURITY DEFINER-rolgrens, stop-immutabiliteit, bewijs-gebonden Task-release, retry-geautoriseerde pool-transfer) en de CI-job die ze draait.5f8fcaeb— de deploy-workflow migreert niet meer (Neon is geen migratiedoel); rollen-preconditie vastgelegd indb-access-policy.md.Verificatie
npm run test:dispatch9 files / 20 tests groen;npm run verify2808 tests groen.queue-dispatchzelf — deze PR is de eerste run.Open
Nog 6 MAJOR-bevindingen in de andere repos (T-1838 t/m T-1843) en 20 MINORs.
🤖 Generated with Claude Code
IP-13, Scrum4Me. Inventory of every write to `claude_jobs` and `agent_message` in this repo, and a decision per path. New: `lib/queue-dispatch-client-server.ts`. Same wire shape as the workers, MCP and CLI clients — same root normalization, same paths, same bounded reading, same single transport retry — with the main app's own identity: a 30-second HMAC-SHA-256 assertion under issuer `scrum4me-web` and key `DISPATCH_WEB_ASSERTION_KEY`, claims pinning method, full path and the SHA-256 of the exact bytes. `sub` comes from this server's iron-session and the user is re-read from the database per call, so no caller-supplied user id can reach the wire; there is no parameter that could carry one. The service already accepts this issuer (`mcp/src/dispatch/assertions.ts`) and authorizes it for read and cancel only (`mcp/src/dispatch/auth.ts`), so the client exposes exactly those two calls. `claude_jobs`: * `cancelClaudeJobAction`, `actions/admin/jobs.ts#cancelJobAction` — a row bound to a dispatch request routes to the central cancel with the current authenticated user. No local status write and no local NOTIFY: the projector owns that state, and a second announcement would race it. * `restartClaudeJobAction` — refused. The central equivalent is recovery with stop evidence (§2.4), which a restart button cannot express; the refusal falls before the status check so the text stays actionable. * `deleteJobAction` — refused: the row is an attempt's execution record. * `cleanup-agent-artifacts` cron, `cancelSprintRunAction`, `propagateStatusUpwards` — managed rows excluded from the bulk `where`. The row guard aborts the whole statement rather than skipping a row, so without the exclusion one managed row silently stops the rest of the cleanup. * `cancelIdeaJobAction` — fenced. The kind CHECK keeps the managed kinds out of an idea lookup today, so this is a fence, not a live path; the alternative is a raw error halfway through the Idea status transaction. * Task parent delete (`deleteTask`, `deleteTaskAction`) — an active `Task.dispatch_request_id` gives "Bewaar gekoppelde uitvoering; verwijdering niet mogelijk" rather than a raw FK error or cascaded evidence. `agent_message`: the legacy root cancel already routes to central semantics (409 on a managed reply, read through `to_jsonb` so it runs pre-migration); result and read ack stay ordinary. `scrum4me-dispatch` joins `scrum4us-job` in the participant-facet exclusion: the model part is a request UUID, not someone you message. Existing `scrum4us-job` guarantees are asserted unchanged. Display: QUEUE_TASK/QUEUE_REVIEW get their own subject — the short request id plus the projector's summary — instead of falling through the ordinary target order, which would present a Task, Idea or Doc that happens to sit on the row as a managed job's subject. The admin list reads the plain `dispatch_request_id` column rather than joining the service's own tables. Every guard runs before the write, the NOTIFY and the revalidate. The database refuses these writes too (`queue_dispatch_guard_job_row`, DISPATCH_MANAGED_ROW, SQLSTATE 42501) but only as a raw error mid-transaction, which is not a refusal a user can act on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>IP-13, Scrum4Me. Pins the legacy queue mutation contract against managed dispatch rows at every level the plan names. * `queue-mutations.test.ts` — the injectable factory: cancel of a managed reply is a conflict before any UPDATE or NOTIFY, while claim and complete on that same row stay ordinary 200s. Reading and acknowledging a managed answer was never the thing being fenced. * `queue-mutations-route.test.ts` — the same two decisions at the HTTP layer: 409 with the current status, and a plain 200 for the ack. * `queue-mutations-alignment.test.ts` — that the guard reads `dispatch_role` through `to_jsonb(agent_message)` and that the locking query never names the column directly. A mock returns whatever it is told, so this property is only observable against the source; the spelling matters because the same query must run on a database where the dispatch migrations are not adopted and the key is simply absent. * `claude-jobs-kind-constraint.test.ts` — the two managed kinds: bound to a dispatch request and to no Task, Idea or sprint run, the request/candidate pair moving together, the re-issued kind-consistency check admitting them, and the three Messages markers staying all-or-nothing on both the live and the archive table. Mutation check: neutralising `row.dispatch_role != null` in `cancelMessage` turns 4 of these cases red across the four suites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>§9.2 described the pre-scope refusal as an operator-only gap ("servicewerk dat nog niet bestaat"). That is now shipped (mcp 379cbe4, docker 158a359, T-1863). The supervisor binds stop evidence to the CLAIM instead of to a scope: POST /dispatch/v1/attempts/claim-stop (mcp acceptClaimBoundStopInTransaction) with the bounded DISPATCH_* reason, then a failed result, so the request reaches FAILED with one canonical result and the slot is released — no operator action. This applies only to a reason that provably precedes the broker create; docker's PRE_SCOPE_REASONS is {DISPATCH_PREPARED_SOURCES_REFUSED}. A prepare timeout, a broker-create error or an unbounded error stays UNCERTAIN because a container may exist. Operator recovery (cancel, or recover with close_failed once UNCERTAIN, authority per §9.1) remains only for the genuinely uncertain case. The broker-created-but-never-started self-close and the "raw DISPATCH_* in a summary is a bug" guidance are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>WIP: feat(IDEA-213): managed queue dispatch — opslag, guards en herstel (ST-1590)to feat(IDEA-213): managed queue dispatch — opslag, guards en herstel (ST-1590)Verdict: COMMENT
Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.
Findings
scripts/queue-dispatch/test-db.mjs:4:requiredUrlsmistDISPATCH_TEST_PROJECTOR_URL, terwijl__tests__/queue-dispatch/harness.tswel altijd een projector-pool opent via die env var. Daardoor kannpm run test:dispatchde safety/preflight-check groen laten lijken met een ontbrekende of verkeerde projector-DSN, waarna de suite pas later in de harness faalt in plaats van fail-fast in de expliciete test-target guard. Voeg de projector-URL toe aanrequiredUrls, zodat alle rollen die de dispatch-integratietests gebruiken door dezelfde host/database/production-cluster checks gaan.Geen blokkerende findings gevonden in de diff. De verwijdering van
prisma migrate deployuit de GitHub Actions deploy-jobs is in de aangepaste db-access runbook expliciet gekoppeld aan de operator-route/T-1836 en lijkt daarmee intentioneel.Verdict: COMMENT
Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.
Findings
vendor/scrum4me-shared:1— De PR wijzigt de submodule-pin van759c1889…naar925af453…, maar de payload bevat alleen de gitlink-wijziging en niet de onderliggende submodule-diff. Daardoor kan ik de nieuwe shared-contracten niet inhoudelijk valideren tegen de Scrum4Me-kant van deze PR.prisma/migrations/20260915090100_queue_dispatch_storage/migration.sql:1— Grote, nieuwe queue-dispatch opslag/guard-surface met expliciete rol- en contractcontroles. De diff bevat bijbehorende integratietests en release/preflight-checks; ik heb in de aangeleverde unified diff geen concrete blokkerende inconsistentie gevonden.Samenvatting
De wijziging volgt zichtbaar de productstandaard rond expliciete DB-rechten, vaste migratiechecksums, geen mutatie van bestaande migraties en een aparte dispatch-testconfig. Omdat de review geen gekoppeld plan heeft en de submodule-inhoud niet in de payload zit, is dit een niet-blokkerende review-opmerking in plaats van een approval.
Verdict: REQUEST_CHANGES
Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.
Findings
actions/admin/jobs.ts:52/lib/queue-dispatch-client-server.ts:26: de admin-cancelroute voor managed jobs roeptcancelManagedDispatch()aan, maar die client tekent altijd als de huidige web-user en documenteert zelf datauthorizeRequestReadeen web actor weigert die niet de requester is. Het IDEA-213 REST-contract staat cancel toe voor “opdrachtgever of beheerder”; hiermee kan een admin via de admin jobs-pagina beheerde jobs van andere gebruikers niet annuleren, terwijl gewone lokale cancel writes terecht zijn uitgezet. Voeg een echte admin/beheerder-authority voor deze route toe of laat de adminactie niet via de requester-only webassertion lopen.Opmerkingen
De diff bevat uitgebreide guard-/policytests en documentatie rond de nieuwe dispatchrollen en migratie-adoptie. Die richting past bij de productdocs, maar bovenstaande autorisatiegat blokkeert acceptatie omdat het een bestaande admin-operatie breekt precies op de managed jobs waarvoor deze PR de lokale fallback verwijdert.
Review afgehandeld — F-SC1 gerepareerd.
scripts/queue-dispatch/test-db.mjs:requiredUrlsmisteDISPATCH_TEST_PROJECTOR_URL, terwijl de harness altijd een projector-pool via die var opent. Toegevoegd, zodat de projector-DSN dezelfde host/db/production-cluster-guard doorloopt vóór er verbindingen opengaan. Bevestigd dat de projector-URL dezelfde db/host:port heeft;test-db.mjs check→DISPATCH_TEST_TARGET_OK. Commit0624e5e7,npm run test:dispatch34 passed.Herpind op shared
c65f5a8(ce633bf).Re-review-finding afgehandeld — interim + follow-up-task. Terecht:
cancelJobActioncancelt een managed job via de requester-only web-assertie, dus een admin kan andermans managed job niet annuleren (authorizeRequestRead→ 403).De echte fix is cross-repo (admin-authority in de dispatchservice-assertie naast de requester) en is gepland als T-1868. Interim (
fb2a0c4f): de niet-requester-weigering (DispatchClientErrorcodeDISPATCH_FORBIDDEN/403) wordt afgevangen en als heldere melding getoond — "Alleen de opdrachtgever kan deze beheerde job annuleren; beheerder-annulering van andermans beheerde job wordt nog niet ondersteund (gepland: T-1868)" — i.p.v. een rauwe fout. Admin-cancel van de EIGEN managed job blijft werken; het auth-contract wijzigt niet. 2 tests toegevoegd (rood-eerst),npm run verify2884 passed | 6 skipped.Dit is bewust het kleinere alternatief conform je tweede suggestie ("laat de adminactie niet via de requester-only webassertion lopen"); de volledige admin-authority volgt in T-1868.
Verdict: REQUEST_CHANGES
Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.
Findings
lib/task-status-decisions.ts:3—decidePbiStatusgeeft nu altijdBLOCKEDterug zodra de huidige PBI-statusBLOCKEDis, nog vóórstates.includes('FAILED')wordt gecontroleerd. Dat wijzigt het bestaande gedrag uitlib/tasks-status-update.ts, waar een gefaalde child-story altijd de PBI naarFAILEDtrok. Daardoor kan een failure-signaal verborgen blijven onder een handmatige BLOCKED-status en propageren sprint-/joboverzichten niet meer correct naar failure. Zet de failure-check weer vóór de BLOCKED-preservatie, of onderbouw en test expliciet waarom BLOCKED voortaan failure mag maskeren.Reviewlog
De dispatch-/DB-guard wijzigingen zijn globaal consistent met IDEA-213 en de nieuwe operator-migratieroute is in docs/runbooks bijgewerkt. De blokkade hierboven is een losstaande runtime-regressie in statuspropagatie.
Re-review-finding afgehandeld via de tweede optie (onderbouwen + testen) — geen gedragswijziging. De finding vermoedde een regressie: dat
decidePbiStatusBLOCKEDvóórFAILEDteruggeeft en zo een gefaalde child maskeert. Gemeten: dit is bestaand, intentioneel gedrag. De pre-refactor helper (lib/tasks-status-update.ts, commit 644f1726~1) sloeg de PBI-herevaluatie juist over metif (pbi.status !== 'BLOCKED')en de comment "BLOCKED is handmatig en wordt niet overschreven door deze helper". De refactor behoudt dat exact — een handmatige BLOCKED heeft bewust voorrang op FAILED.Conform je tweede aangeboden optie is dit nu expliciet onderbouwd en getest (
32b8060b): een comment bijdecidePbiStatuslegt de bewuste precedentie + de historische bron vast, en een nieuw testbestand pintdecidePbiStatus(['FAILED'],'BLOCKED')==='BLOCKED'(bewust) naast(['FAILED'],'READY')==='FAILED'en de DONE/READY-gevallen. Logica ongewijzigd.npm run verify2893 passed | 6 skipped.Verdict: COMMENT
Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.
Findings
PR-breed:1— Geen blokkerende finding gevonden in de geïnspecteerde runtimepaden, DB-guards, CI-aanpassing, docs en tests. De diff is uitzonderlijk groot en er ontbreekt een gekoppeld plan/acceptatiecriteria in de payload; daarom safe-default geenAPPROVEDmaarCOMMENT.Reviewnotities
De wijziging lijkt de relevante productstandaarden bewust te volgen: managed dispatch-rows worden niet lokaal geschreven of verwijderd, server actions weigeren voor DB-guard/FK-fouten, bigint-versies blijven strings, en er is gerichte testdekking plus een aparte dispatch-guard CI-gate toegevoegd. Zonder gekoppeld plan heb ik geen formele plan-conformiteit kunnen vaststellen.