Operant Studio
OPER-56

Generalized reap-streak parker (guardrail)

Body

```yaml
ticket_id: OPER-56
size: M
has_migration: false
phi_in_scope: false
verification_cmd: "pnpm --filter dispatcher test -- workflow-reaper"
target_branch: main
module: dispatcher
```

## Agent directive

`@claude` — Implement the generalized reap-streak parker per the spec below. Open a PR titled `feat(dispatcher): generalized reap-streak parker (OPER-56)` against `main`. Behind feature flag; default off. Follow `CLAUDE.md`.

**Do not exceed 500 LOC total.**

---

## 1. Summary

Generalize the workflow-reaper streak parker to count any consecutive non-merge outcome (`failure` | `success_nopr` | `success_closed`) rather than only `success_nopr`. Parks the ticket at threshold 5 with a diagnostic label describing the dominant failure mode. Behind feature flag.

## 2. Motivation

On 2026-07-26 eleven Portico tickets (POR-321, POR-122, POR-124, POR-129, POR-133, POR-134, POR-202, POR-242, POR-245, POR-248, POR-251) accumulated 55–110 dispatch cycles each without merging. Zero of them tripped the current guardrail (`workflow-reaper.ts:78`, threshold 3, `success_nopr` only) because their failure modes were `success_closed` (size gate) or `failure`. This ticket closes that gap.

See `/home/user/workspace/decisions/cancelled-11-fix-plan-2026-07-26.md` and `/home/user/workspace/decisions/oper-56-guardrail-proposal-2026-07-26.md` for full context.

## 3. Acceptance criteria

- [ ] `packages/dispatcher/src/workflow-reaper-streak.ts` exports `getConsecutiveNonMergeCount(taskId)` reading from `DispatchRun.outcome` (if the enum is already granular enough — verify first).
- [ ] `parkTaskIfStreakExceeded(taskId)` flips status to `BOUNCED` and labels the ticket with a dominant-mode string when the count hits 5.
- [ ] Dominant-mode label is one of:
  - `reap:oversize-close-streak` — dominated by `success_closed`
  - `reap:no-pr-streak` — dominated by `success_nopr`
  - `reap:failure-streak` — dominated by `failure`
  - `reap:mixed-streak` — no single mode ≥3 of 5
- [ ] Fast path retained: 3 consecutive `success_nopr` still parks immediately (backward compatible with `DEFAULT_MAX_CONSECUTIVE_SUCCESS_NOPR`).
- [ ] Feature flag `reaper.generalized_streak` gates the new behavior. Default off.
- [ ] Only counts outcomes from the last 24h (older decay out).
- [ ] Slack notification to `#operant-studio-ops` on every park with the 5 outcome tags in order + workflow-run links.
- [ ] Backtest script `packages/dispatcher/scripts/backtest-reaper.ts` logs what would have been parked without actually parking.
- [ ] Vitest unit tests covering:
  - 5 consecutive `success_closed` → park with `reap:oversize-close-streak`.
  - 5 consecutive `failure` → park with `reap:failure-streak`.
  - 3 consecutive `success_nopr` → park with `reap:no-pr-streak` (fast path, threshold 3).
  - Merge in the middle resets the counter.
  - Non-merge outcomes older than 24h don't count.
  - Feature flag off → no park.
- [ ] `pnpm lint && pnpm typecheck && pnpm --filter dispatcher test -- workflow-reaper` green.

## 4. Files in scope

### Create
- `packages/dispatcher/src/workflow-reaper-streak.ts`
- `packages/dispatcher/src/workflow-reaper-streak.test.ts`
- `packages/dispatcher/scripts/backtest-reaper.ts`

### Modify
- `packages/dispatcher/src/workflow-reaper.ts` — integrate new streak logic; keep `DEFAULT_MAX_CONSECUTIVE_SUCCESS_NOPR = 3` fast path.

### Do NOT touch
- The DispatchRun schema unless the outcome enum is not granular enough (verify first; stop and comment if it isn't).

## 5. Constraints & stop conditions

- Do not exceed 500 LOC total.
- Do not park any ticket while the feature flag is off.
- Stop and comment if the `DispatchRun.outcome` enum doesn't already distinguish `success_nopr` / `failure` / `success_closed` / `success_merged` — the ticket depends on that granularity being present.
- Do not migrate or delete existing BOUNCED tickets.

## 6. Dependencies

- **Blocked by:** none (defense-in-depth; ship independently of POR-492/493/494).
- **Blocks:** OPER-57 (dependency validator, separate ticket) — not created yet.

## 7. Out of scope

- Fixing the underlying failure modes (planner underspec, phantom deps). Those are separate tickets.
- Changing dispatcher throughput (still 1 claim/tick).
- Deleting or migrating existing BOUNCED tickets.

## 8. PR Description

```
## Summary
Generalizes workflow-reaper streak parker to count any consecutive non-merge
outcome (failure | success_nopr | success_closed) rather than just success_nopr.
Parks at threshold 5 with a diagnostic dominant-mode label. Behind feature flag.

## Acceptance criteria status
- [x] getConsecutiveNonMergeCount / parkTaskIfStreakExceeded
- [x] Dominant-mode labels (4 variants)
- [x] Fast path 3x success_nopr retained
- [x] Feature flag reaper.generalized_streak (default off)
- [x] 24h decay window
- [x] Slack notification on park
- [x] Backtest script
- [x] Vitest coverage
- [x] pnpm lint && typecheck && test green

## Test output
<paste output>

## Risk level
low  (guarded by feature flag; default off; backtest before enable)

## Rollout note
Ship off. Backtest for 1 week. Enable in staging for 1 week. Enable prod.
Rollback if false-positive rate > 5% of merges per week.

## Out of scope
- Planner underspec fixes
- Dispatcher throughput
- Phantom-dependency validation (separate ticket)
```

Attachments

Loading attachments…

Comments

Loading comments…