OPER-193
Admission auto-reheal on PATCH that resolves rejection
Suggested
Justin Cooke
Body
--- feature_id: FEAT-studio-planning-validation --- ## Context Every session that files new tickets through the Ledger POST API hits the same admission-workflow friction: when initial admission rejects (missing feature_id, unknown feature_id, missing required body section, etc.), a subsequent PATCH that *resolves* the rejection reason does NOT re-run admission. The ticket keeps the sticky `admission-rejected` label and `AWAITING_HUMAN` status until a human (or a follow-up PATCH) explicitly sets `status: READY_FOR_PLANNING` and `labels: []`. Observed this session (2026.09.10) on OPER-184 through OPER-191: eight tickets filed for D-29 + D-30 scope. All eight initially failed admission on `missing_feature_id` because the body used `## Feature` prose instead of frontmatter `feature_id:`. PATCH added valid frontmatter (200 OK, body persisted correctly) but admission did not re-evaluate. Required a second manual PATCH to `status: READY_FOR_PLANNING, labels: []` before the auto-planner would consider the ticket. Prior session (2026.09.08) hit the same pattern: NEEDS_RESCOPE required a human PATCH + status reset even after size/module/writeSets were backfilled correctly. This is a systematic gate quality issue: admission is a computed function of ticket body + row fields, but its result is only latched on POST, not re-computed on PATCH. ## What Extend the PATCH route at `apps/web/src/app/api/studio/[team]/tasks/[id]/route.ts` (and any bulk-PATCH equivalent under `.../tasks/bulk/`) to re-run `assertAdmissible` (from `@/lib/ticket-admission`) whenever a PATCH modifies any admission-relevant field: - `body` (frontmatter or body-section changes) - `size`, `module`, `writeSets`, `band`, `riskTier`, `phiInScope`, `hasMigration` - Any other field admission reads Re-admission logic: 1. If the ticket currently has `admission-rejected` label AND status is `AWAITING_HUMAN`, run admission. 2. On admission success: - Remove `admission-rejected` label - Transition status to `READY_FOR_PLANNING` - Log an audit event `TASK_ADMISSION_HEALED` with prior + current rejection reasons (null on current) 3. On admission failure with the same reason: no-op (keep sticky state, don't spam audit). 4. On admission failure with a different reason: update the label/audit trailer with the new reason. If a PATCH would re-trigger admission and the caller passes `?skipReadmission=true`, honor it (for platform maintenance where a human wants to edit body without re-admitting). ## Acceptance Criteria - [ ] PATCH to any admission-relevant field re-runs `assertAdmissible` on tickets currently in `AWAITING_HUMAN` with `admission-rejected` label - [ ] Successful re-admission clears the label and transitions status to `READY_FOR_PLANNING` - [ ] Successful re-admission emits `TASK_ADMISSION_HEALED` audit event with old rejection reason - [ ] Failed re-admission with same reason is a no-op (no duplicate audit rows) - [ ] Failed re-admission with different reason updates the admission-rejected trailer in body - [ ] `?skipReadmission=true` query param honored for platform maintenance edits - [ ] Unit test in `apps/web/src/app/api/studio/[team]/tasks/[id]/route.test.ts` covering: heal path, no-op path, changed-reason path, skip path - [ ] Integration test asserting that filing a ticket + PATCHing to resolve rejection yields a ready ticket without a second status-reset PATCH ## Blocked-by None. ## Size / Band S / assurance (single-route change, well-scoped, existing admission code is the source of truth). ## Discovered by Repeated ticket-filing sessions (2026.09.08 and 2026.09.10). Systematic issue flagged under the standing agent-fleet-controller quality rule.
Attachments
Loading attachments…
Comments
Loading comments…