feat(IDEA-213): managed queue dispatch — opslag, guards en herstel (ST-1590) #251

Merged
janpeter merged 45 commits from feat/idea-213-dispatch into main 2026-09-21 19:25:44 +02:00
Owner

Draft — niet mergen. Geopend om de nieuwe CI-job queue-dispatch te 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-dispatch staan in scrum4me-mcp, scrum4me-shared, scrum4me-workers, scrum4me-docker, s4m-queue en Ops-dashboard.

Review-fixes (code-review 2026-09-20)

  • T-1835 98ca282a — Task.dispatch_request_id ontbrak in schema.prisma; migrate diff stelde een DROP van kolom, index en FK voor. Na regenereren geen dispatch-drift meer (gemeten tegen postgres:17).
  • T-1837 2ff30bf7 5171db91 1c117d6d ebd45bdf — 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.
  • T-1836 5f8fcaeb — de deploy-workflow migreert niet meer (Neon is geen migratiedoel); rollen-preconditie vastgelegd in db-access-policy.md.

Verificatie

  • Lokaal, vers geprovisioneerd: npm run test:dispatch 9 files / 20 tests groen; npm run verify 2808 tests groen.
  • Niet bewezen: de CI-job queue-dispatch zelf — 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

Draft — niet mergen. Geopend om de nieuwe CI-job `queue-dispatch` te 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-dispatch` staan in scrum4me-mcp, scrum4me-shared, scrum4me-workers, scrum4me-docker, s4m-queue en Ops-dashboard. ## Review-fixes (code-review 2026-09-20) - **T-1835** `98ca282a` — `Task.dispatch_request_id` ontbrak in `schema.prisma`; `migrate diff` stelde een DROP van kolom, index en FK voor. Na regenereren geen dispatch-drift meer (gemeten tegen postgres:17). - **T-1837** `2ff30bf7` `5171db91` `1c117d6d` `ebd45bdf` — 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. - **T-1836** `5f8fcaeb` — de deploy-workflow migreert niet meer (Neon is geen migratiedoel); rollen-preconditie vastgelegd in `db-access-policy.md`. ## Verificatie - Lokaal, vers geprovisioneerd: `npm run test:dispatch` 9 files / 20 tests groen; `npm run verify` 2808 tests groen. - **Niet bewezen:** de CI-job `queue-dispatch` zelf — 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](https://claude.com/claude-code)
The task-binding migration added tasks.dispatch_request_id, its unique
index and foreign key, but the submodule bump in 1a52046a never
regenerated prisma/schema.prisma. The next migrate diff would have
proposed dropping the column that the occupancy triggers read.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
s4m_dispatch_observe_managed_worker is the only SECURITY DEFINER
function in the dispatch schema and had no test. Cover the role
boundary, observation validation, staleness, and every binding
refusal against real roles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The IP-09 guards had no test: a stopped attempt must stay stopped, and
a claimed Task may only be released after a terminal result, matching
stop evidence and settled publications.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A previously claimed request may move to another pool slot only under
an unconsumed retry authorization and with stop evidence for the prior
attempt. Each refusal differs from the accepted case by one fact.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The dispatch integration tests are excluded from npm test and had no CI
step, so the SQL guards were only ever exercised by hand. Provision the
full migration history plus the policy adoption against postgres:17 and
gate deploys on the result.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ci(IDEA-213): stop migrating from the deploy workflow
All checks were successful
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 4m14s
CI / DB Access Policy Gate (pull_request) Successful in 1m35s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m28s
CI / DB Access Production Sentinel (pull_request) Successful in 1m12s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
5f8fcaeb4e
Neon is no longer a migration target. The dispatch migrations fail
closed without the dispatch roles, and a failed attempt leaves a row in
_prisma_migrations that blocks every later deploy. Migrations run only
through the ops-agent DB-access operator; document the role
precondition there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fix(IDEA-213): let the adoption precheck resume a half-applied migration set
All checks were successful
CI / DB Access Policy Gate (pull_request) Successful in 2m1s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m58s
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 4m27s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m38s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
c860ba23bb
The precheck reported 'before' only when no manifest migration had a
ledger row and sent everything else to the after-verification, which
demands all of them. A deploy that stopped between two migrations could
therefore never be resumed.

Classify the ledger instead: a finished, never rolled back prefix is
'partial' and resumable, a failed row is refused until it is resolved
as rolled back, and an out-of-order or re-checksummed row stays fatal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fix(IDEA-213): answer a hub cancel of a managed dispatch reply with a conflict
Some checks failed
CI / DB Access Policy Gate (pull_request) Failing after 1m36s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m44s
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 4m29s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m34s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
5ac3fc8175
The hub's cancel has no type filter, so a pending managed reply
addressed to mac:jp could be cancelled from the phone or the watch. The
row guard refuses that transition and the caller got a 500. Reading and
acknowledging the answer stay allowed; cancelling now returns the
existing 409 conflict with the current row, before any write.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fix(IDEA-213): read the dispatch role without requiring the column
All checks were successful
CI / DB Access Policy Gate (pull_request) Successful in 1m59s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m58s
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 4m24s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m22s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
87e6eee9f5
The hub's row lock selected dispatch_role directly, so claim, complete
and cancel would all fail with 42703 on a database where the dispatch
migrations are not adopted yet; the hub watch gate, whose schema has no
such column, caught it. Reading the key through to_jsonb yields NULL
there and the role afterwards.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
IP-13, Scrum4Me. Bump the scrum4me-shared gitlink from 6d72c8f to the final
IDEA-213 commit 5209199, which adds `lib/queue-dispatch-projection.ts`, and
regenerate `prisma/schema.prisma` through `scripts/gen-schema.sh`. The
generated schema is byte-identical: 5209199 carries no schema change, and the
model/enum inventory (119 entries) is unchanged before and after, so no model
disappeared behind a silent submodule drift.

The new shared module uses a BigInt literal for the bigint version bound. This
repo's tsconfig still targeted ES2017, so `tsc --noEmit` refused it with
TS2737; the target moves to ES2020. `lib` is explicitly listed and unchanged,
and Next transpiles through SWC/browserslist rather than this target, so the
move only widens the syntax tsc accepts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
test(IDEA-213): integrate dispatch consumers and failure contracts
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m44s
CI / Queue Dispatch Guard Gate (pull_request) Has been cancelled
CI / DB Access Production Sentinel (pull_request) Has been cancelled
CI / Detect deploy-relevant changes (pull_request) Has been cancelled
CI / Deploy Preview (PR) (pull_request) Has been cancelled
CI / Deploy Production (main) (pull_request) Has been cancelled
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been cancelled
CI / Lint, Typecheck, Test & Build (pull_request) Has been cancelled
98664ad485
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>
chore(IDEA-213): document the web dispatch configuration and drop dead helper
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m42s
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 3m39s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m57s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m16s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
793d8b9867
IP-13, Scrum4Me. `S4M_DISPATCH_URL` and `DISPATCH_WEB_ASSERTION_KEY` are the
main app's only dispatch configuration, and the key is deliberately a separate
one from workers' — the service pairs issuer and key, and `scrum4me-web` is
authorized for read and cancel only.

Also removes `isDispatchConfigured`: nothing calls it. The one read path this
task added reads the plain `dispatch_request_id` column on `claude_jobs`, so it
never touches the client and needs no feature probe to degrade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add the immutable release manifest contract for the coordinated seven-repo
delivery and a read-only checker for it. check-release.ts measures everything
this checkout can prove (own commit, shared gitlink, package versions, dispatch
migration ids and checksums, adoption manifest hash, six-role policy hash,
protocol version, artifact limits) and compares host facts only against a
supplied evidence file, including the db-access-install-evidence/v1 document
that the Ops capture script prints. It opens no network or database connection
and prints no DSN or secret. Exit 3 marks a run whose local half is green but
whose remote facts are unmeasured, so that is never mistaken for a pass.

Extend queueDispatchSchemaPreflight to the plan's activation preflight: database
and cluster identity, runtime role flags for all six contract roles instead of
only the two new ones, refusal of membership of the dispatch, projector or
migrator/owner roles by inheritance or SET ROLE, trigger and
session_replication_role bypass refusal, an unknown login role blocking
activation, schema invariants, migration ids and the adopted object contracts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs(IDEA-213): document dispatch rollout recovery and acceptance
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m45s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m36s
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 5m15s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m15s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
59f8a3794d
Add the queue-dispatch rollout runbook and the fillable acceptance evidence
form. The runbook starts with the feature off and states that executing any
deployment, host or database change needs JP's corresponding instruction. It
describes the binding ordering with the all-instance inventory over mac,
scrum4me-srv and max2, per-service provisioning with secret ownership and no
values, the key rotation rule, the all-consumer database preflight, ISS-1 as an
open activation dependency without claiming a new measurement, host slot
preparation with the confinement test, forward-safe rollback, the operator
procedures that have no entry point today, and where the plan text and the code
disagree. Rollout status is kept strictly separate, with the four gates and only
the implementation gate carrying evidence.

The acceptance form keeps every boolean false and records per proof what
measurable evidence may flip it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs(IDEA-213): align dispatch runbook with the health and operator routes
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m37s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m38s
CI / DB Access Production Sentinel (pull_request) Successful in 1m21s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 5m5s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
e0ae0f57e9
The mcp and docker work landed the routes this runbook described as missing.
Correct it against mcp 365e29d and docker c190be7, verified in code:

- GET /healthz exists, outside /dispatch/v1, unauthenticated, one-second cache,
  four fields, and always HTTP 200 — so a probe must test schema_ready and
  role_ready, not the status code.
- Queue-restore redelivery is POST /dispatch/v1/outbox/republish with a global
  ADMIN bearer, at most 500 requests per call, repeated with a new action_id
  until the receipt reports zero; the raw outbox UPDATE is gone.
- The unknown-publication attestation has a route, recovery authority and no
  client method, so operators use curl.
- Forgotten reply repair and terminal retention are tick maintenance stages on
  DISPATCH_MAINTENANCE_INTERVAL_MS; retention deletes hot queue rows and is
  opt-in through DISPATCH_RETENTION_DAYS.
- Record the per-host runtime gate, the compose profile that an empty render
  hides, and the supervisor entrypoint that still refuses activation with
  DISPATCH_RUNTIME_WIRING_REQUIRED_IP13, plus unproven Linux confinement, as
  unmet gates for the host slots.

Still open and left as such: no dispatch NOTIFY producer, and GET /artifacts/:id
does not honour the bound-attempt proof.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs(IDEA-213): align dispatch runbook with the assembled supervisor surface
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m42s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m39s
CI / DB Access Production Sentinel (pull_request) Successful in 1m20s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 5m7s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
0e9aaf453d
Correct the runbook against mcp ee898fe and docker 4f930b4, verified in code.

The docker supervisor entrypoint no longer refuses with
DISPATCH_RUNTIME_WIRING_REQUIRED_IP13: it reads operator config, registers and
polls, with the job route running its own claim loop and the host route serving
a listener socket that still replays the claim on its own bound session. Rewrite
the slot preparation section around the template's actual operator variables,
the pinned profile file whose digest is derived from those exact bytes, the
removal of SCRUM4ME_WORKER_INSTANCE_ID from this service, and the refusal of any
repo_write or source-mount profile before registration, so only read-only
artifact-publishing profiles can be armed today.

Record the supervisor and child surface that mcp now serves: the collected-output
route after the supervisor's own stop, the non-launch recovery routes, the result
receipt that carries the canonical result except on publication_unknown, and the
two /agent routes that exist only under DISPATCH_AGENT_OUTPUT_KEY, resolve no
dispatch identity and refuse a bearer alongside. Warn that the docker README
still carries the superseded claim about that receipt.

List the cross-repo contract gate under the manual gates, with what its one full
attempt does and does not prove: a fake runtime, no container, image, model or
egress, never through the compose template and never against a live service.
Replace the old §6.2 entry with the gates that are actually open: read-only
attempt root on Linux, agent_token into a child, a producer for code output, the
recovery REST from docker, and unproven Linux confinement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs(IDEA-213): align dispatch runbook with prepared sources and tick notifications
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m37s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m33s
CI / DB Access Production Sentinel (pull_request) Successful in 1m14s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 4m57s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
0e2b21b03c
Correct the runbook against mcp b24f35b and docker 5d7f76d, verified in code.

Prepared sources (T-1861). Record the design position instead of the old gap
list: the supervisor fetches the sources through the signed source manifest, the
child deliberately holds no credential at all and its source route serves review
documents only, and code is collected post-stop by the supervisor with base and
branch from the pinned request. A repo_write or source-mount profile now needs an
armed producer — the writable prepared-sources root plus the service's public
Ed25519 permit key, all or nothing, with a private key refused outright — and
stays refused without it. Add the two new service routes: the signed source
manifest, and the bound-attempt read on GET /artifacts/:id, which retires the
row claiming that route ignores the attempt proof.

Workspace and notifications (T-1862). Document DISPATCH_WORKSPACE_ROOT, whose
absence makes intake answer 404 to every repository-pinning request without
writing a row, with the rollout consequence that it must be set before
DISPATCH_ENABLED=1 where repo_write is served, and DISPATCH_GIT_PROTOCOLS, whose
file protocol is fixture-only while publication stays https. Replace the "no
NOTIFY producer" statements with the dispatch_tick channel: ids-only payload,
emitted inside the service's own transactions, consumed on a dedicated listener
connection, coalesced, with the interval remaining the guarantee, so a stuck
request is never diagnosed as a lost notification. It needs no grant and adds no
trigger, so nothing changes for the access policy or the adoption.

Retarget the open-gate list and the status section: the cross-repo gate now runs
two full attempts, one of them repo_write into a local bare repository, so the
https forge path, Forgejo conflict handling and draft-PR creation are unmeasured,
checks has no producer, and the docker code-artifact implementation is a second
one guarded only by that gate. Drop the note about the superseded docker README
claim, which has been corrected upstream.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs(IDEA-213): document the broker socket group, stuck-request recovery and the remaining raw-code paths
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m36s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m44s
CI / DB Access Production Sentinel (pull_request) Successful in 1m13s
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 4m49s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
11456a2145
Correct the runbook against docker 026d9eb and mcp ac4789b, verified in code.

Broker socket group (docker 026d9eb, m3). The operator-managed runtime broker
requires a numeric socketGroup >= 1 in its own root-owned JSON config: after
listen it chowns the socket to that group and sets mode 0660, and refuses with
DISPATCH_BROKER_SOCKET_GROUP_REFUSED (and does not serve) on a missing, non-integer
or zero value. Document it as a broker-config field, not a compose variable, and
state that the group is named explicitly rather than inherited from a setgid bit.

Stuck CLAIMED/RUNNING request (docker 026d9eb, T-1863). Add a troubleshooting
subsection for the two situations: a scope the broker created but never started is
now closed by the supervisor itself to FAILED with one canonical result and a
bounded DISPATCH_* summary, no operator action needed; but a refusal before any
scope exists (typically DISPATCH_PREPARED_SOURCES_REFUSED) cannot be closed by the
supervisor, because the service accepts no result without stop evidence and stop
evidence binds to a scope id that does not exist, so the request stays CLAIMED and
needs a cancel or a close-failed recover once UNCERTAIN. Recover authority verified
against mcp auth.ts. Also state that a result summary is always a bounded
DISPATCH_* code and that raw text, a path or anything credential-shaped in a
summary is a bug to report. Cross-reference docker's own operator doc.

Remaining raw DISPATCH_MANAGED_ROW paths (mcp ac4789b, m14(c)). m14 translated the
ordinary task enqueue path's 42501 + DISPATCH_MANAGED_ROW into a friendly message,
but idea-jobs.ts and the handleUpdateTaskStatus tool still surface the raw code.
Record both as known open items, cosmetic and fail-closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review MINOR m5 (ST-1590.29) sub-finding (a). A same-slot re-claim or a request
returned to RESERVED with a generation bump passes no SQL guard: state, generation
and retry_authorization_event_id are mutable and no DB code demands a retry_consumed
event. Per JP, no SQL request-state-transition guard is added -- such a guard would
be written by the same scrum4me_dispatch role that performs the transition and could
only prove self-consistency, not authority. Retry authority is enforced in the
application dispatch service (scrum4me-mcp); its own refusal and red test live there,
out of this repository's scope. This characterization test pins the DB boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(IDEA-213): scope the task-job guard and require read-committed
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m39s
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 3m44s
CI / DB Access Production Sentinel (pull_request) Successful in 1m15s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m52s
CI / Detect deploy-relevant changes (pull_request) Has been cancelled
CI / Deploy Preview (PR) (pull_request) Has been cancelled
CI / Deploy Production (main) (pull_request) Has been cancelled
8044532888
Review MINOR m5 (ST-1590.29) sub-findings (b) and (c), shipped as the ninth dispatch
migration 20260921090000_queue_dispatch_guard_scope (never edits a pinned migration).

(b) queue_dispatch_sync_task_binding now raises DISPATCH_TASK_BINDING_ISOLATION
(SQLSTATE 25000) unless the transaction is READ COMMITTED. Its FOR UPDATE re-read and
occupancy EXISTS checks are only race-safe under READ COMMITTED; under REPEATABLE READ
or SERIALIZABLE they run against a stale snapshot and could seat a second occupant
without a serialization error. Fail closed on the wrong isolation level.

(c) queue_dispatch_guard_task_job is narrowed to BEFORE INSERT OR UPDATE OF status,
task_id. As an all-column trigger it fired on every update of an active claude_jobs
row and took FOR UPDATE on the Task row each time, widening the worker(job->task)/
web(task->job) deadlock window. Occupancy-relevant transitions still fire; unrelated
updates no longer take the Task lock. Behaviour is otherwise unchanged.

Re-pins the adoption manifest (new migration id + checksum, bumped migrationId and
recaptured after_schema for queue_dispatch_sync_task_binding and claude_jobs),
scripts/db-access/profiles/scrum4me.json, dispatchMigrationIds, DB_ACCESS_HASH_FILES
and the policy-bundle required set. These files are hashed inputs, so the policy hash
changes (b6b511bb -> ad031b95) and the bundle must be rebuilt and re-approved before
adoption. The release manifest now pins nine migrations. Tests under __tests__/
queue-dispatch cover both findings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chore(IDEA-213): pin shared ee7fe1a
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m41s
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 5m0s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m39s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m38s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
8153831ac3
Bump vendor/scrum4me-shared 5209199 -> ee7fe1a (feat/idea-213-dispatch).
Shared contract tightening m9a/m9b/m9c/m10; lib-only, no prisma change.
Regenerated prisma/schema.prisma is byte-identical (72 models / 47 enums).
verify 2882 passed/6 skipped; test:dispatch 13 files/34 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(IDEA-213): bind the idempotency key into the dispatch assertion
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m23s
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 3m29s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m57s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m12s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
8ff353ee6c
The web dispatch client signed the assertion over method/path/body but
left any Idempotency-Key in an unsigned header. The web surface is read
and cancel only and sends no such header, but the signed material must
still match the verifier's new `idem` claim, so sign the empty string it
checks against. This keeps the web client's assertion valid against the
scrum4me-mcp verifier that now binds the Idempotency-Key on submit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(IDEA-213): correct the managed-row leak notes after T-1865
Some checks failed
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 3m35s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m39s
CI / DB Access Policy Gate (pull_request) Successful in 1m47s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m40s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
15781c2b71
§10.2 listed two raw-DISPATCH_MANAGED_ROW paths as open; that premise was
asserted without measurement and is half wrong. Correct it against mcp ae748f5.

The update-task-status path was a real leak and is now fixed (ae748f5):
assertUnmanagedTask in managed-job.ts throws the Prisma-7 driver shape (SQLSTATE
42501 on .cause) and handleUpdateTaskStatus translates it with isManagedTaskRefusal
to a Dutch message, fail-closed with no mutation.

The idea-jobs path is not a leak and never was: an idea-job (claudeJob.create with
idea_id, no task_id, no dispatch markers) cannot trigger the guards —
queue_dispatch_guard_task_work returns early on task_id IS NULL and
queue_dispatch_guard_job only raises for a row carrying
dispatch_request_id/dispatch_candidate_id. Measured empirically as
scrum4me_web_runtime against the disposable DB (T-1865).

The m14(c) open item is closed; the "a raw DISPATCH_* code in a user-facing
message is a bug to report" guidance stays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(IDEA-213): the supervisor now closes a pre-scope refusal itself
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m40s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m45s
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 4m58s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m22s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
7423142870
§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>
chore(IDEA-213): pin shared 9c3ce16
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m42s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m51s
CI / Lint, Typecheck, Test & Build (pull_request) Failing after 3m45s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m23s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
9fefc7d026
Bump vendored scrum4me-shared to 9c3ce16 (feat/idea-213-dispatch), which
adds the DispatchState value UNCERTAIN_UNSTARTED and narrows the state
table (CLAIMED:lease_lost -> UNCERTAIN_UNSTARTED -> resume_same_attempt ->
CLAIMED). Generated prisma/schema.prisma is byte-identical (72 models, 47
enums); no consumer edits required in this repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(IDEA-213): keep the dispatch-bound refusal constant module-private
All checks were successful
CI / DB Access Policy Gate (pull_request) Successful in 1m32s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m37s
CI / DB Access Production Sentinel (pull_request) Successful in 1m20s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 6m18s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
252e452690
'use server' files may only export async functions; the exported
DISPATCH_BOUND_DELETE_REFUSED string broke next build (webpack).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bumps vendor/scrum4me-shared to 925af45 (start-permit key-id keyset + manifest
key id). No main code touches the permit/manifest signer or verifier, so this is
unaffected: the generated Prisma schema is byte-identical (no model change) and the
full verify + dispatch suites are green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(IDEA-213): document dispatch key rotation
All checks were successful
CI / DB Access Policy Gate (pull_request) Successful in 1m31s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m39s
CI / DB Access Production Sentinel (pull_request) Successful in 1m17s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 6m16s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
b9ffbeb22f
Documents the zero-downtime Ed25519 rotation for the dispatch start permit and the
prepared-sources manifest in docs/runbooks/queue-dispatch.md: add the new key to the
keyset on both hosts, flip the signer to the new kid, wait out everything signed
under the old kid (permits 5s, a manifest one attempt), then retire the old key. Adds
the new env vars with ownership (DISPATCH_START_PERMIT_KEY_ID,
DISPATCH_SOURCE_MANIFEST_PRIVATE_KEY, DISPATCH_SOURCE_MANIFEST_KEY_ID,
DISPATCH_PERMIT_PUBLIC_KEYS) and notes it changes no DB_ACCESS_HASH_FILES / policy hash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
janpeter changed title from 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) 2026-09-21 18:00:10 +02:00
s4m-codex-reviewer left a comment

Verdict: COMMENT

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • minor — scripts/queue-dispatch/test-db.mjs:4: requiredUrls mist DISPATCH_TEST_PROJECTOR_URL, terwijl __tests__/queue-dispatch/harness.ts wel altijd een projector-pool opent via die env var. Daardoor kan npm run test:dispatch de 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 aan requiredUrls, 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 deploy uit 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 - minor — `scripts/queue-dispatch/test-db.mjs:4`: `requiredUrls` mist `DISPATCH_TEST_PROJECTOR_URL`, terwijl `__tests__/queue-dispatch/harness.ts` wel altijd een projector-pool opent via die env var. Daardoor kan `npm run test:dispatch` de 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 aan `requiredUrls`, 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 deploy` uit de GitHub Actions deploy-jobs is in de aangepaste db-access runbook expliciet gekoppeld aan de operator-route/T-1836 en lijkt daarmee intentioneel.
fix(IDEA-213): add projector URL to dispatch preflight guard
Some checks failed
CI / DB Access Policy Gate (pull_request) Successful in 1m43s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m39s
CI / Detect deploy-relevant changes (pull_request) Has been cancelled
CI / Deploy Preview (PR) (pull_request) Has been cancelled
CI / Deploy Production (main) (pull_request) Has been cancelled
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been cancelled
CI / DB Access Production Sentinel (pull_request) Has been cancelled
CI / Lint, Typecheck, Test & Build (pull_request) Has been cancelled
0624e5e793
test-db.mjs check() validated only admin/dispatch/queue/web URLs, but the
harness always opens a projector pool via DISPATCH_TEST_PROJECTOR_URL. A
missing or wrong projector DSN slipped past the fail-fast preflight and only
surfaced later in the harness. Add DISPATCH_TEST_PROJECTOR_URL to requiredUrls
so the same host/port and /s4m_dispatch_test pathname checks cover it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
s4m-codex-reviewer left a comment

Verdict: COMMENT

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • INFO — vendor/scrum4me-shared:1 — De PR wijzigt de submodule-pin van 759c1889… naar 925af453…, 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.
  • INFO — 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: COMMENT Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - **INFO** — `vendor/scrum4me-shared:1` — De PR wijzigt de submodule-pin van `759c1889…` naar `925af453…`, 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. - **INFO** — `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.
chore(IDEA-213): re-pin shared c65f5a8 (F-S1)
All checks were successful
CI / DB Access Policy Gate (pull_request) Successful in 1m25s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m24s
CI / DB Access Production Sentinel (pull_request) Successful in 57s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 7m3s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
ce633bfd68
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
s4m-codex-reviewer requested changes 2026-09-21 18:36:54 +02:00
Dismissed
s4m-codex-reviewer left a comment

Verdict: REQUEST_CHANGES

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • ERROR — actions/admin/jobs.ts:52 / lib/queue-dispatch-client-server.ts:26: de admin-cancelroute voor managed jobs roept cancelManagedDispatch() aan, maar die client tekent altijd als de huidige web-user en documenteert zelf dat authorizeRequestRead een 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.

# Verdict: REQUEST_CHANGES Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - **ERROR** — `actions/admin/jobs.ts:52` / `lib/queue-dispatch-client-server.ts:26`: de admin-cancelroute voor managed jobs roept `cancelManagedDispatch()` aan, maar die client tekent altijd als de huidige web-user en documenteert zelf dat `authorizeRequestRead` een 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.
Author
Owner

Review afgehandeld — F-SC1 gerepareerd.

  • F-SC1 (minor) scripts/queue-dispatch/test-db.mjs: requiredUrls miste DISPATCH_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. Commit 0624e5e7, npm run test:dispatch 34 passed.

Herpind op shared c65f5a8 (ce633bf).

**Review afgehandeld — F-SC1 gerepareerd.** - **F-SC1 (minor)** `scripts/queue-dispatch/test-db.mjs`: `requiredUrls` miste `DISPATCH_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`. Commit `0624e5e7`, `npm run test:dispatch` 34 passed. Herpind op shared `c65f5a8` (`ce633bf`).
fix(IDEA-213): map non-requester managed-cancel refusal to clear NL message (T-1868 interim)
All checks were successful
CI / DB Access Policy Gate (pull_request) Successful in 2m3s
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 4m27s
CI / DB Access Production Sentinel (pull_request) Successful in 59s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m40s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
fb2a0c4f3d
Interim for the re-review ERROR on admin cancelJobAction: an admin cancelling
ANOTHER user's managed job is refused by the dispatch service
(`authorizeRequestRead` authorizes a web actor as the requester only), which
surfaces as DispatchClientError 403 DISPATCH_FORBIDDEN and previously bubbled up
as a raw service error.

Catch that one refusal in the managed branch and re-throw a clear operator
message; every other error is re-thrown untouched, and admin-cancel of one's OWN
managed job still works. The auth/assertion contract is unchanged — the real
cross-repo fix (admin-authority on the service) is tracked as T-1868.

Tests: add a non-requester refusal case mapping to the clear message, plus a
pass-through case for other DispatchClientError codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Owner

Re-review-finding afgehandeld — interim + follow-up-task. Terecht: cancelJobAction cancelt 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 (DispatchClientError code DISPATCH_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 verify 2884 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.

**Re-review-finding afgehandeld — interim + follow-up-task.** Terecht: `cancelJobAction` cancelt 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 (`DispatchClientError` code `DISPATCH_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 verify` 2884 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.
s4m-codex-reviewer left a comment

Verdict: REQUEST_CHANGES

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • error — lib/task-status-decisions.ts:3 — decidePbiStatus geeft nu altijd BLOCKED terug zodra de huidige PBI-status BLOCKED is, nog vóór states.includes('FAILED') wordt gecontroleerd. Dat wijzigt het bestaande gedrag uit lib/tasks-status-update.ts, waar een gefaalde child-story altijd de PBI naar FAILED trok. 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.

# Verdict: REQUEST_CHANGES Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - **error** — `lib/task-status-decisions.ts:3` — `decidePbiStatus` geeft nu altijd `BLOCKED` terug zodra de huidige PBI-status `BLOCKED` is, nog vóór `states.includes('FAILED')` wordt gecontroleerd. Dat wijzigt het bestaande gedrag uit `lib/tasks-status-update.ts`, waar een gefaalde child-story altijd de PBI naar `FAILED` trok. 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.
docs(IDEA-213): pin BLOCKED-over-FAILED intent in decidePbiStatus (re-review)
All checks were successful
CI / DB Access Policy Gate (pull_request) Successful in 1m50s
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 4m25s
CI / Queue Dispatch Guard Gate (pull_request) Successful in 1m58s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / DB Access Production Sentinel (pull_request) Successful in 1m1s
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
32b8060b1e
Reviewer flagde dat decidePbiStatus BLOCKED teruggeeft voor de FAILED-check.
Gemeten: dit is bestaand, intentioneel gedrag, geen regressie. De pre-refactor
helper (tasks-status-update.ts, 644f1726~1) sloeg de PBI-herevaluatie over met
`if (pbi.status !== 'BLOCKED')` — comment: "BLOCKED is handmatig en wordt niet
overschreven door deze helper". decidePbiStatus behoudt dat exact.

Tweede reviewer-optie (onderbouwen + testen), geen gedragswijziging:
- comment bij decidePbiStatus die de intentie en de historische bron vastlegt
- nieuw __tests__/lib/task-status-decisions.test.ts dat het gedrag pint:
  decidePbiStatus(['FAILED'],'BLOCKED')==='BLOCKED' (BLOCKED maskeert FAILED),
  (['FAILED'],'READY')==='FAILED', (['DONE'],'READY')==='DONE', READY-geval,
  plus dekking voor decideStoryStatus/decideSprintStatus.

Logica ongewijzigd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Owner

Re-review-finding afgehandeld via de tweede optie (onderbouwen + testen) — geen gedragswijziging. De finding vermoedde een regressie: dat decidePbiStatus BLOCKED vóór FAILED teruggeeft 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 met if (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 bij decidePbiStatus legt de bewuste precedentie + de historische bron vast, en een nieuw testbestand pint decidePbiStatus(['FAILED'],'BLOCKED')==='BLOCKED' (bewust) naast (['FAILED'],'READY')==='FAILED' en de DONE/READY-gevallen. Logica ongewijzigd. npm run verify 2893 passed | 6 skipped.

**Re-review-finding afgehandeld via de tweede optie (onderbouwen + testen) — geen gedragswijziging.** De finding vermoedde een regressie: dat `decidePbiStatus` `BLOCKED` vóór `FAILED` teruggeeft 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 met `if (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 bij `decidePbiStatus` legt de bewuste precedentie + de historische bron vast, en een nieuw testbestand pint `decidePbiStatus(['FAILED'],'BLOCKED')==='BLOCKED'` (bewust) naast `(['FAILED'],'READY')==='FAILED'` en de DONE/READY-gevallen. Logica ongewijzigd. `npm run verify` 2893 passed | 6 skipped.
s4m-codex-reviewer left a comment

Verdict: COMMENT

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • info — 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 geen APPROVED maar COMMENT.

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.

# Verdict: COMMENT Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - info — `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 geen `APPROVED` maar `COMMENT`. ## 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.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
janpeter/Scrum4Me!251
No description provided.