Operant Studio
OPER-148

Provenance guard for merged-PR precheck (OPER-D1 hardening) — D-26 next action 1

Band
LARGE
Suggested
Justin Cooke

Body

feature_id: FEAT-studio-dispatcher
decision_memo: docs/pdt/[REDACTED-DOB]-d26-premature-done.md
filing_template_version: 1

## Context

OPER-119 and OPER-120 were both marked DONE at 2026-09-09T00:24:21.271Z — an identical timestamp for two different tickets, with prOpenedAt / checksGreenAt / firstReadyAt all NULL for both, and 4 hours BEFORE either ticket's real Portico PR (#1697 / #1698) was opened.

Root cause pinned in PDT memo D-26 (`docs/pdt/[REDACTED-DOB]-d26-premature-done.md`): `packages/dispatcher/src/merged-pr-precheck.ts` queries `search/issues?q=repo:${owner}/${repo} type:pr in:title ${identifier}` for the team's target repo (`getPullRequestsForIdentifier`, `packages/dispatcher/src/github-pr-details.ts:224-251`) and, on any merged hit, unconditionally flips `status → DONE` (line 137).

The operant team's target repo per `team-repo-routing.ts:23` is `operant-studio` — but the actual code work for many OPER-* tickets lands in `portico`. To reconcile the two, the fleet opens a NOOP bookkeeping PR on the studio repo whose title contains the identifier + a pointer to the portico PR (evidence: studio PR #287 for OPER-120 merged 00:22:42Z, studio PR #288 for OPER-119 merged 00:17:30Z — both preceded the DONE flip). The precheck saw those merges, matched the identifier, and flipped DONE — even though the real work was still on a Portico branch, unmerged.

D-26 recommendation is Option A (provenance check). Precheck must load the ticket's most recent `DispatchRun`, extract its branch (or PR number if the release path recorded one), and only flip DONE when the merged PR's branch/number MATCHES that run. If no DispatchRun exists, or the merged PR's `mergedAt` predates the DispatchRun's `startedAt`, precheck skips reconciliation and lets dispatch proceed.

Explicitly REJECTS title-only matches — a noop bookkeeping PR titled with the identifier cannot flip DONE unless its branch matches the DispatchRun's branch. Preserves the OPER-D1 double-dispatch guard for the same-repo dropped-webhook case (DispatchRun DID open the PR, so provenance matches).

Kira's DISSENT mitigation is a MUST-INCLUDE: add a covering index `DispatchRun(taskId, startedAt DESC) INCLUDE (branch, prNumber)` in the same migration; verify with `EXPLAIN ANALYZE` on a representative row count before landing.

## Acceptance Criteria

- `packages/dispatcher/src/merged-pr-precheck.ts` reads the most recent DispatchRun for the ticket before considering any DONE flip.
- A merged PR is considered provenance-matched ONLY when: (a) its head branch equals the DispatchRun's branch, OR (b) its PR number equals a PR number the DispatchRun recorded at release. Otherwise precheck returns `{ skip: false, reconciled: false }` even when a matching-identifier merged PR exists.
- New migration adds covering index on `DispatchRun(taskId, startedAt DESC) INCLUDE (branch, prNumber)`. Migration is verified locally with `prisma migrate diff` and `EXPLAIN ANALYZE` shows index use on the precheck's DispatchRun lookup.
- Unit tests in `merged-pr-precheck.test.ts` cover: (1) same-repo real merge (webhook dropped) → DONE fires; (2) cross-repo noop bookkeeping PR merged in studio with matching identifier but no DispatchRun-branch match → no flip; (3) no DispatchRun → no flip; (4) DispatchRun exists but merged-PR mergedAt < DispatchRun startedAt → no flip; (5) DispatchRun with no branch recorded → no flip (fail-safe).
- Retroactive audit: query `Comment WHERE authorLabel='dispatcher:merged-pr-precheck'` for the last 30 days; produce a list of any tickets whose linked PR was NOT authored by that ticket's DispatchRun. Attach the audit output as a comment on this ticket.

## Non-goals

- Fleet noop-PR convention itself (open question in D-26; separate memo if pursued).
- Nightly drift reconciler (D-26 next action 2 — separate ticket).
- Repo-scoped identifier disambiguation (D-26 Option C, explicitly rejected as blast-radius too high).

## Related

- Blocks D-25 retroactive PATCH work on OPER-117/119/120/121 — the audit here may reveal those tickets are only marked DONE because of this bug, not because their work truly landed. Do not manually flip those tickets back until this audit runs.
- Depends on nothing.

Attachments

Loading attachments…

Comments

Loading comments…