Operant Studio
OPER-160

OPER: admission unknown_feature_id should tell the filer which feature_ids are valid

Band
SMALL
Suggested
Justin Cooke

Body

---
feature_id: FEAT-studio-planning-validation
filing_template_version: v1
decision_memo: docs/pdt/2026.09.09-observation-blind-feature-id.md
blocked_by:
---

## Context

On 2026.09.09 the auto-update-behind gate ticket was filed with `feature_id: FEAT-fleet-gate-auto-update-behind`. Admission rejected it with:

```
Admission rejected (unknown_feature_id): feature_id=FEAT-fleet-gate-auto-update-behind is not declared in the feature-map.md of any admission-enabled product owned by this team
```

The rejection is correct policy: admission requires the feature_id to be declared in a `products/*/feature-map.md`. The failure was **discoverability**. The rejection body names the missing id, but not the id set that would have been accepted. The filer (human or agent) has to shell out to `gh api search/code?q=repo:...+filename:feature-map.md` and grep for `FEAT-` to construct a valid guess, then re-file.

The filing agent, mid-run, cannot do this without extra tool calls. On this session it produced two rejected tickets before landing on `FEAT-studio-dispatcher`.

## Root cause

The admission gate at `apps/web/src/lib/admission/*` (per OPER-D2 lineage) knows the set of admission-enabled products and their declared feature_ids because it enumerates them to check membership. That enumerated set is thrown away instead of surfaced in the rejection.

## Acceptance Criteria

1. When admission rejects with `unknown_feature_id`, the rejection body includes the top N (bounded at 20) feature_ids the requesting team owns across admission-enabled products, grouped by product. Example format:

   ```
   Admission rejected (unknown_feature_id): feature_id=FEAT-x-y-z is not declared.
   Valid feature_ids for team operant (studio):
     FEAT-studio-agent-workers
     FEAT-studio-audit
     FEAT-studio-dispatcher
     ...
   ```

2. If the team owns 0 admission-enabled products, the rejection body says so explicitly rather than showing an empty list.
3. If the team owns >20 feature_ids, the body shows the first 20 plus a `... and N more; see products/<product>/feature-map.md` line.
4. `ticket-filing.ts` (OPER-146 output) reads the admission rejection body and, on `unknown_feature_id`, surfaces the valid set to the caller so an agent can pick and retry without a second round trip to the code-search API.
5. Self-test: filing a ticket with `feature_id: FEAT-clearly-fake-id` returns the enumerated list, and re-filing with the first id in that list is accepted.

## Verification

- `bash scripts/admission-unknown-feature-id-selftest.sh` passes.
- Manual repro: `curl -X POST /api/studio/operant/tasks -d '{... feature_id: FEAT-fake ...}'` returns 200 with the ticket flagged admission-rejected AND the response body contains the enumerated valid list.

## Notes

- Class: `Blind-Feature-ID-Guess`. Pair with OPER-146 (ticket-filing helper) so the helper consumes the improved rejection body.
- Do not weaken the feature_id requirement while fixing this; a rejection is still a rejection, we are only making it self-correcting.


---
<!-- 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…