Operant Studio
OPER-161

OPER: PATCH to a frontmatter-blocker field should re-run admission, not just POST

Band
SMALL
Suggested
Justin Cooke

Body

---
feature_id: FEAT-studio-planning-validation
filing_template_version: v1
decision_memo: docs/pdt/2026.09.09-observation-patch-frontmatter-readmit.md
blocked_by:
---

## Context

On 2026.09.09 OPER-155 was filed with prose "Depends on OPER-148" in a Related section. Admission's blocker parser read that prose, saw OPER-148 was not DONE, and rejected the ticket with `blocked_by_not_done`. The prose was rewritten to "Sequenced after" and a proper `blocked_by: OPER-148` frontmatter field was added via PATCH -- but admission did not re-run, so the ticket stayed AWAITING_HUMAN with the `admission-rejected` label even though the ticket now conformed to policy.

The hole is `apps/web/src/app/api/studio/[team]/tasks/[id]/route.ts` PATCH -- it did not call `assertAdmissible`. OPER-151 (merged as PR #441) closes half of that hole: admission now runs on PATCH at all. It does NOT yet special-case the frontmatter-blocker fields, so a PATCH that fixes the exact rejection reason still leaves the `admission-rejected` label in place until the ticket is re-POSTed or manually promoted.

## Root cause

OPER-151's admission-on-PATCH fires for every PATCH, which is correct as a floor. But it does not clear the `admission-rejected` label when the new body would pass admission, and it does not re-emit a passing admission trailer to the body. Downstream planners then see a mixed signal: body is compliant, but the label says "do not touch".

## Acceptance Criteria

1. When PATCH runs admission and the resulting decision is `ok=true`:
   a. Remove the `admission-rejected` label from the ticket if present.
   b. Replace the trailing `<!-- admission-rejected -->` marker (and its associated `**Admission rejected ...**` line) with `<!-- admission-passed-on-patch -->` and a one-line `**Admission re-checked on PATCH: PASS** -- <reason>` note.
   c. Transition the ticket status from `AWAITING_HUMAN` to `BACKLOG` so the planner picks it up on the next tick.
2. When PATCH runs admission and the resulting decision is `ok=false`:
   a. Preserve the `admission-rejected` label.
   b. Append (not replace) a new dated `<!-- admission-rejected -->` marker with the current failure reason, so the trailer history reads as a mini-changelog.
3. Self-test `scripts/patch-readmit-selftest.sh` (KNUCK-A032 pattern) proves: (a) blocker-not-done -> PATCH with correct frontmatter -> label cleared, status BACKLOG; (b) blocker-not-done -> PATCH with still-wrong body -> label kept, new marker appended; (c) benign PATCH of an unrelated field -> label state unchanged.
4. `docs/task-state-machine.md` records the AWAITING_HUMAN -> BACKLOG re-admission transition alongside the existing manual reclassification path.

## Verification

- `bash scripts/patch-readmit-selftest.sh` passes locally and in CI.
- Manual repro on OPER-155 (if still AWAITING_HUMAN): PATCH the body, watch the label clear and status flip to BACKLOG within one API call.

## Notes

- Class: `Stale-Admission-Label`. Depends on OPER-151 having landed (it has, as of 2026.09.09 PR #441).
- Do not weaken admission while fixing this; the fix is about label/status hygiene, not lenience.


---
<!-- admission-rejected -->
**Admission rejected (map_drift):** feature-map.md content sha256 does not match Product.featureMapChecksum for slug=studio; DB and repo have drifted

Attachments

Loading attachments…

Comments

Loading comments…