Operant Studio
OPER-24

Relocate autonomous-engine workflows from portico → operant-studio (reusable workflows pattern)

Body

# OPER-24 — Move autonomous-engine workflows to operant-studio

## The bug this fixes

2026-07-18 03:42 UTC: OPER-19 dispatched successfully at the DB layer (`agent/oper-19-...` branch created in `operant-studio`, `dispatchedAt` populated). The `agent-dispatch` workflow was then invoked — but the workflow lives in the `portico` repo, and the branch is in `operant-studio`. Step ''Checkout target branch'' failed with 404 in 15 seconds, and the task is now IN_PROGRESS with no PR forever.

This was not a one-off. Every OPER-* dispatch in the last 48h has failed the same way:

- 10 ghost IN_PROGRESS tickets (OPER-9, 12, 7B, 16A, 15A, 9A, 11, 8A, 10, 13) all had 0-commit branches in `operant-studio` that never became PRs.
- OPER-1 and OPER-19 just failed the same way at 03:43 UTC.

## Root cause

`agent-dispatch.yml` (and every other autonomous-engine workflow) only exists in `Operant-Health/portico`. The dispatcher (`packages/dispatcher/src/tick.ts`) hardcodes the repo when invoking `workflow_dispatch`.

But the pipeline itself is **Operant Studio infrastructure** — not a Portico feature. It should live at the studio level and be consumed by every product repo (portico, operant-studio itself, and any future product) as a reusable workflow.

## Proposal

### Phase 1 — Establish reusable-workflow contract in operant-studio

Create `.github/workflows/agent-dispatch-reusable.yml` in `Operant-Health/operant-studio`:

- Same steps as today''s `portico/.github/workflows/agent-dispatch.yml`
- Declared as `workflow_call` with inputs: `ticket_id`, `target_branch`, `target_repo`, `agent_context_b64`
- Uses `${{ inputs.target_repo }}` in the checkout step

Also relocate the related autonomous-engine workflows to `operant-studio`:

- `agent-dispatch.yml` → reusable
- `agent-rebase.yml` → reusable
- `agent-rebase-dirty.yml` (OPER-21) → stays as cron in each consumer OR moves to `operant-cron`
- `agent-pr-sweeper.yml` (OPER-22) → same

### Phase 2 — Product-repo thin wrappers

In `portico/.github/workflows/agent-dispatch.yml`, replace body with:

```yaml
name: Agent Dispatch
on:
  workflow_dispatch:
    inputs:
      ticket_id: {required: true, type: string}
      target_branch: {required: true, type: string}
      agent_context_b64: {required: true, type: string}
jobs:
  run:
    uses: Operant-Health/operant-studio/.github/workflows/agent-dispatch-reusable.yml@main
    with:
      ticket_id: ${{ inputs.ticket_id }}
      target_branch: ${{ inputs.target_branch }}
      target_repo: Operant-Health/portico
      agent_context_b64: ${{ inputs.agent_context_b64 }}
    secrets: inherit
```

Same wrapper added to `operant-studio/.github/workflows/agent-dispatch.yml` — which self-references the reusable workflow with `target_repo: Operant-Health/operant-studio`. This unblocks OPER-* ticket execution.

### Phase 3 — Dispatcher routing

Update `packages/dispatcher/src/tick.ts` to select target repo from the ticket:

- `teamId=Operant Studio` → `Operant-Health/operant-studio`
- `teamId=Portico` → `Operant-Health/portico`
- Extract to a `TeamRepoRouting` table if we add a third product repo.

### Phase 4 — Ghost cleanup

One-shot script to reset the 10 ghost IN_PROGRESS OPER-* tickets and delete their empty branches, then let the (now-fixed) dispatcher re-claim them.

## Why this is the right shape

- Reflects the actual ownership: pipeline = Studio, not Portico.
- Any new product repo gets the same 15-line wrapper and is instantly part of the autonomous engine.
- Reusable workflow updates propagate to every consumer on the next dispatch.
- Compatible with OPER-23 (schema serialization) — that lives in the dispatcher, not the workflow.

## Non-goals

- Not moving the workflows *into* `operant-cron`. Those are on-demand `workflow_dispatch`, not cron.
- Not building a monorepo. Product code stays in product repos.

## Acceptance criteria

- [ ] `Operant-Health/operant-studio/.github/workflows/agent-dispatch-reusable.yml` exists and passes `actionlint`.
- [ ] `portico/.github/workflows/agent-dispatch.yml` is a thin wrapper.
- [ ] `operant-studio/.github/workflows/agent-dispatch.yml` is a thin wrapper for self-dispatch.
- [ ] Dispatcher chooses target repo from `Task.teamId`.
- [ ] Manual test: file OPER-N test ticket → dispatcher opens PR on operant-studio.
- [ ] Manual test: file POR-N test ticket → dispatcher opens PR on portico.
- [ ] Ghost cleanup script run; 10 orphaned OPER-* tickets return to PLANNING_VALIDATED and re-dispatch successfully.

## Files this touches

- **operant-studio (new):** `.github/workflows/agent-dispatch-reusable.yml`, `.github/workflows/agent-rebase-reusable.yml`, `.github/workflows/agent-dispatch.yml` (self wrapper)
- **portico:** `.github/workflows/agent-dispatch.yml` (replace body), `.github/workflows/agent-rebase.yml` (replace body)
- **operant-studio:** `packages/dispatcher/src/tick.ts` (repo routing), `packages/dispatcher/src/backend.ts` (add `targetRepo` to dispatch payload)
- **scripts:** `scripts/oper-24-cleanup-ghost-in-progress.ts`

## Priority

**P0.** Zero OPER-* work can complete until this ships. Portico is still working because its workflow is co-located, but the entire Studio pipeline is broken.

## DOC review

Required — this is a repo-boundary + infrastructure change. Post to `#alerts-operant-pipeline`.

Attachments

Loading attachments…

Comments

Loading comments…