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…