OPER-23
Serialize schema-touching tickets in the dispatcher (writeSets policy)
Body
# OPER-23 — Schema-serialization dispatcher policy
## Problem
The dispatcher''s ordering primitive is `blocked_by`, evaluated at claim time in `packages/dispatcher/src/backend.ts` (`evaluateBlockedBy` → `claim()`). The `Product.dependency-map.md` gate at admission (`apps/web/src/lib/ticket-admission.ts`) ensures every ticket''s declared blockers exist and form a valid edge in the product''s dependency graph.
Both of these are **feature-graph correct** but **file-graph blind**. Two features can be orthogonal in the dependency map yet touch the same `packages/database/prisma/schema/*.prisma` model. When N agents run in parallel and both edit e.g. `client.prisma`, their PRs go DIRTY on each other as soon as the first one merges to `main`.
Empirically today (2026-07-17), of 65 open agent PRs:
- 16 are DIRTY
- 17 have already escalated to `awaiting-human` via the OPER-21 auto-rebase loop (3-strike cap)
- Every escalated PR''s blocking comment is a conflict on a `.prisma` file in `packages/database/prisma/schema/`
The auto-rebase-dirty loop (OPER-21) handles textual conflicts but is not, and should not become, a semantic Prisma merger. The right fix is upstream: **stop dispatching schema-changing tickets in parallel**.
## Non-goals
- **Not** splitting the database. Distributed transactions, cross-DB FKs, and cross-domain joins are strictly worse than the current problem.
- **Not** building a Prisma-aware three-way git merge driver. That''s a valid future optimization (OPER-24 candidate).
- **Not** changing the feature dependency map or admission logic. Those solve ordering; this solves write-set overlap.
## Proposal
Introduce a `SCHEMA` write-set flag at the ticket layer. Serialize dispatch of `SCHEMA`-flagged tickets at `maxInFlightSchema=1` per team. All other tickets continue to flow up to `maxInFlight`.
### 1. Detect schema tickets at planning time
At planning-validation, set `Task.writeSets = [''SCHEMA'']` when any predicted file matches:
```
packages/database/prisma/schema/**/*.prisma
packages/database/prisma/schema/migrations/**
```
**DB change:** add `writeSets String[]` (default `[]`) to `Task`, plus a partial index `WHERE ''SCHEMA'' = ANY(writeSets)`.
### 2. Add per-write-set concurrency limits
Extend `DispatcherState`:
```prisma
model DispatcherState {
maxInFlight Int @default(3)
maxInFlightSchema Int @default(1) // NEW
}
```
### 3. Enforce the limit in `dispatcherTick`
In `packages/dispatcher/src/tick.ts` around line 140:
- Compute `inFlightSchema = countInFlight(teamId, writeSet=''SCHEMA'')`
- If the candidate task has `''SCHEMA'' in writeSets` and `inFlightSchema >= maxInFlightSchema` → skip and add to `skippedIds`, record a `dispatcher:schema-serialization-hold` refusal comment, continue peeking.
- Non-SCHEMA tickets ignore the schema counter.
Add `countInFlight` overload to `DispatcherBackend`:
```ts
countInFlight(teamId: string, opts?: { writeSet?: string }): Promise<number>;
```
### 4. Refusal comment surface
Include the SCHEMA task currently holding the slot, queue depth waiting, link to this spec.
## Acceptance criteria
- [ ] Admission predicts + stamps `Task.writeSets = [''SCHEMA'']` when planning identifies `packages/database/prisma/schema/**` files.
- [ ] Backfill script re-scans IN_PROGRESS and PLANNING_VALIDATED tickets.
- [ ] `DispatcherState.maxInFlightSchema` exists, defaults to 1, per-team.
- [ ] Dispatcher refuses SCHEMA promotions past the limit with de-duped refusal comment.
- [ ] Non-SCHEMA tickets unaffected; synthetic wave of 5 non-schema + 5 schema promotes 5 non-schema in one tick, drips schema one at a time.
- [ ] `dispatcher.schema_in_flight` gauge added to pipeline-health cron.
- [ ] Zero net new Prisma conflicts on `main` for 72h after deploy.
## Rollout
1. Merge admission-side prediction (safe no-op).
2. Backfill script.
3. Enable `maxInFlightSchema=1` via env `DISPATCHER_SCHEMA_SERIALIZATION_ENABLED`.
4. Observe 24h.
5. Delete feature flag.
## Escape hatch
Raise `maxInFlightSchema` via one-row UPDATE on `DispatcherState`. No code change needed.
## Files this will touch
- `packages/database/prisma/schema/tasks.prisma` (new column + partial index)
- `packages/database/prisma/schema/dispatcher.prisma` (new `maxInFlightSchema`)
- `packages/database/prisma/schema/migrations/YYYYMMDDHHMMSS_oper23_schema_serialization/` **must be in schema/ subfolder — POR-450 regression pattern**
- `apps/web/src/lib/ticket-admission.ts`
- `packages/dispatcher/src/backend.ts`
- `packages/dispatcher/src/tick.ts`
- `scripts/backfill-oper23-writesets.ts`
- Tests: extend `apps/web/src/lib/dispatcher-tick-*.test.ts`
**PR title:** `feat(dispatcher): serialize schema-touching tickets (OPER-23)`
## DOC review required before build
This changes dispatcher promotion policy — DOC (DevOps Council) sign-off required before an agent starts coding.Attachments
Loading attachments…
Comments
Loading comments…