Operant Studio
OPER-59

Eliminate LEDGER_BEARER round-trip in auto-planner (in-process runPlanValidation)

Body

## Problem

The 2026-07-17 -> 2026-07-27 silent stall was caused by a rotated `LEDGER_BEARER_PORTICO` env var on Vercel. The auto-planner cron self-fetched its own HTTP `/api/studio/[team]/tasks/[id]/validate-plan` route with a per-team bearer, and when the value drifted between Vercel and its origin, every planner call returned 401 before writing a single row. OPER-58 shipped the SEV-1 detector (`auto_planner_silent`) so this class of failure surfaces within 30 minutes going forward. OPER-59 eliminates the failure MODE.

## Fix

Extract the full validate-plan transaction (policy check, PlanningValidation write, task-status transition, audit, event, points, auto-assignment) into a new shared function `runPlanValidation()` in `apps/web/src/lib/plan-validation-runner.ts`. Both callers now use it:

- HTTP route `POST /validate-plan`: auth -> parse -> call runner (unchanged external contract).
- `/api/cron/auto-planner`: iterates BACKLOG + READY_FOR_PLANNING tasks and calls the runner in-process with a synthesized bearer actor `dispatcher:hub-<slug>`. No self-fetch. No `LEDGER_BEARER_<slug>` env var needed for this path.

The env var still exists for external HTTP callers (agents, CI hooks) and remains unchanged for that path.

## Scope

- `apps/web/src/lib/plan-validation-runner.ts` (new): `runPlanValidation()` + `autoPlannerActor()` helper.
- `apps/web/src/lib/__tests__/plan-validation-runner.test.ts` (new): 7 tests covering pass, fail, BACKLOG transition, READY_FOR_PLANNING transition, ineligible status, missing task, auto-assign fan-out, actor synthesis.
- `apps/web/src/app/api/studio/[team]/tasks/[id]/validate-plan/route.ts`: slim wrapper around the runner.
- `apps/web/src/app/api/cron/auto-planner/route.ts`: replaces `fetch` + bearer with in-process runner call.
- `docs/operations/pipeline-runbook.md`: OPER-59 note added to `invariant-auto-planner-silent` section explaining the 401 mode is now structurally impossible for the cron path, plus post-OPER-59 remedy steps for when the invariant fires despite no bearer failure.

## Verification

- `cd apps/web && npx vitest run src/lib/__tests__/plan-validation-runner.test.ts` -> 7/7 pass.
- After merge: dispatcher backlog should keep draining even with `LEDGER_BEARER_PORTICO` unset / stale.
- OPER-58 `auto_planner_silent` invariant remains armed as belt-and-suspenders.

<!-- ledger:trailers
blocked_by: []
module: platform
phi_in_scope: false
verification_cmd: pnpm --filter web vitest run src/lib/__tests__/plan-validation-runner.test.ts
has_migration: false
-->

Attachments

Loading attachments…

Comments

Loading comments…