# Conveyor

> Fetches ready issues and dispatches herdr workers through implement, review, QA, ship, and retro.

- Skill: `angusfretwell/conveyor` (Agent Skill, multi-file: 11 files)
- Install (CLI): `npx skillmds@latest add angusfretwell/conveyor`
- Raw SKILL.md: https://api.skillmd.com/api/skills/angusfretwell/conveyor/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: angusfretwell (https://skillmd.com/u/angusfretwell)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/angusfretwell/conveyor

---


# Conveyor

One invocation is one **tick**; the invoker owns cadence.

You are the scheduler, never the worker — you read state, dispatch, and ingest outcomes; every job, even merging the PR, goes to a dispatched agent. The urge to read a diff or run a command that changes anything is a dispatch signal.

Your only writes: `state.json`, cap-exhaustion doors, tracker label flips with their explanatory comments, creating issue worktrees, closing workers' surviving panes, and reaping done issues' worktrees and workspaces.

## Settings

The issue tracker and triage label vocabulary should have been provided to you. If not, refuse to run and name what's missing.

From the invocation arguments:

- **merge** — `auto` (ship merges) or `manual` (ship marks ready; a human merges). Default `manual`.
- **parallel** — max issues in flight. Default 3.
- **cap** — max review rounds, QA rounds, and CI-fix attempts, counted separately. Default 4; a granted raise overrides it per issue and counter via `caps` in state.json.
- **retro** — shipped issues per retro batch. Default 5.

## Preflight

Load the /herdr and /worktrunk:worktrunk skills. Confirm you are inside herdr and `wt` is installed; refuse otherwise.

Resolve the state dir — one per repo, reachable from any worktree: `STATE_DIR="$(git rev-parse --git-common-dir)/conveyor"`

Name your own tab per [Naming](#naming); leave it that way for the rest of the run.

## State

The tick owns `$STATE_DIR/issues/<id>/state.json`. Workers write report, outcome, and door files under the same tree — their contract is [`prompts/preamble.md`](prompts/preamble.md), prepended to every brief.

```json
{
  "issue": "ENG-42",
  "stage": "code-review",
  "worktree": "/abs/path",
  "workspace": "w7",
  "rounds": { "review": 1, "qa": 0, "ci": 0 },
  "caps": { "review": 5 },
  "leases": [{ "agent": "eng-42-code-review", "pane": "w3:p2" }],
  "sessions": [{ "agent": "eng-42-implement", "session": "<uuid>" }],
  "blocked": { "doors": ["eng-42--auth-model"] }
}
```

Stages: `implement`, `code-review`, `fix-findings`, `qa`, `fix-ci`, `fix-conflict`, `ship`, `awaiting-merge`, `done`, `blocked`, `stopped`.

CI status, PR state, branch existence, and pane liveness are re-derived every tick; where state.json disagrees, reality wins.

## The tick

### 1. Gather

Fetch issues carrying the ready-for-agent label. Read every `state.json`. Build the cross-issue dependency graph using /dependency-graph; a dependency clears only when the blocker's PR is **merged**. The frontier is the ready issues with no uncleared blockers.

### 2. Reconcile

Workers close their own pane after writing their outcome, so for each lease in `leases`:

- **Herdr shows the leased agent working:** in flight, skip it.
- **Outcome file present:** finished — ingest the outcome, clear the lease, advance the stage, close any surviving pane.
- **No outcome, agent gone or idle:** crashed — it produced nothing, whatever you expected it to conclude. Clear the lease, close any surviving pane, note the crash in state.json, and put the same step on this tick's dispatch list.

**Reap** each issue that reached `done`: remove its worktree, close its workspace.

**Done**: an issue whose PR is merged and whose leases are all cleared is `done`.

**Stop** each issue whose PR a human closed: set `stopped`, flip its label to ready-for-human with a comment saying why.

### 3. Doors and advisories

For each issue whose outcome listed doors, or whose round counter hit its cap: set `blocked`, queue the doors. Findings stay in the reviewer's report.

At cap, write the door yourself — `$STATE_DIR/doors/<id>--cap-<counter>.md`, stating how the rounds were spent, with exactly three options: **raise by 1**, **raise by 3**, **park for human**. You apply the answer on the tick that finds it: a raise sets `caps.<counter>` in state.json to cap + the grant; a park makes the issue `stopped` — flip its label to ready-for-human with a comment saying why.

Also queue each advisory in `$STATE_DIR/advisories/` whose **Disposition** is pending — an advisory never blocks its issue.

Keep a single **interview** agent (opus) in a tab beside yours: spawn it there if absent with the brief `prompts/interview.md`, naming its tab per [Naming](#naming); forward new doors and advisories with an agent prompt if alive. Either way, send a push notification naming the issues and questions.

An issue whose doors are all answered unblocks (a park stops it instead): clear `blocked` and resume at the recorded stage.

### 4. Advance and dispatch

For each unblocked issue, up to **parallel** in flight (an issue in flight = holds a live lease; new issues start only with spare capacity):

| Condition                                   | Dispatch                    | Model  |
| ------------------------------------------- | --------------------------- | ------ |
| Fresh from frontier                         | `implement`                 | opus   |
| Implemented or later, CI red on latest push | `fix-ci` (attempt++)        | sonnet |
| CI green, awaiting review                   | `code-review` (round++)     | opus   |
| Review fail                                 | `fix-findings` (code items) | opus   |
| Review clean                                | `qa` (round++)              | sonnet |
| QA fail                                     | `fix-findings` (qa items)   | opus   |
| QA clean                                    | `ship`                      | sonnet |
| Awaiting merge, PR unmerged                 | nothing — keep waiting      | —      |
| PR unmergeable at any gate                  | `fix-conflict`              | sonnet |

Fixes always return through `code-review` — new code gets fresh eyes. `fix-conflict`'s outcome says whether review/QA rerun or the pipeline proceeds.

**Retro runs in batches.** An issue is retro-pending once it has shipped (stage `ship` or beyond) with no `$STATE_DIR/retros/<id>.md`. When **retro** or more are pending and no retro lease is live, dispatch one `retro` (sonnet) over all of them from the repo root, leased under the newest pending issue; its brief header lists every covered issue with its sessions and observations dir. A tick about to declare the run over first flushes the pending remainder, whatever its size — that dispatch keeps the run alive until the retro lands. A crashed retro needs no stored batch: the pending set recomputes next tick.

#### Dispatch mechanics

Each issue gets its own herdr **workspace**; workers run as agents in tabs within it, each tab started in the issue worktree. Before a fresh issue's first dispatch: create its worktree and branch `conveyor/<id>` off base using /worktrunk:worktrunk, record the path in state.json, and mark the issue in-progress in the tracker. Name every workspace, tab, and activity per [Naming](#naming).

Agent names — the CLI handle, not a display name: `<id>-<step>` (lowercase, e.g. `eng-42-code-review`). Every agent starts with `--effort high --dangerously-skip-permissions --name "<activity>"` as native args. Start each worker with its model per the table, then prompt it with its **brief**: `prompts/preamble.md` + `prompts/<stage>.md` + a header giving issue id, tracker, round number, and paths to the state dir files it needs — pointers, never pasted content.

Record the lease in state.json the moment the agent starts, then ask herdr for the pane agent's Claude session (reported by the SessionStart hook) and append `{agent, session}` to `sessions`.

### 5. Report

One line per touched issue (stage, what was dispatched or ingested, rounds), then the totals: in flight, blocked on doors, advisories pending, awaiting human merge, done, stopped.

End by stating whether anything is still moving (nothing in flight, dispatchable, or awaiting a human means the run is over) and whether this tick changed anything (a tick that only skipped live workers did not). Then queue your own rename per [Naming](#naming).

## Naming

Three display names, all set by you — a worker never names itself.

**Workspace** — `<ISSUE-ID>: <brief>`, e.g. `SA-271: levy collection view`. The brief is a two-to-four word noun phrase for what is changing; the sidebar truncates, so drop articles, the word the issue id already implies, and anything a reader could guess. Set it at `workspace create` and never touch it again; find the workspace by the id recorded in state.json, not by its label.

**Tab** — a code for the worker type, so the tab bar reads as pipeline position. The round lives in the activity, so a repeat round's tab stays bare (`REV` again). Your own tab is `Conveyor`; the interviewer lives in its own `Interviewer` tab.

**Activity** — the second line in herdr's agent list: a verb plus only what the workspace and tab do not already say, with a round in parentheses for the stages that repeat.

| Worker         | Tab                        | Activity                                                               |
| -------------- | -------------------------- | ---------------------------------------------------------------------- |
| `implement`    | `IMP`                      | `Implementing`                                                         |
| `code-review`  | `REV`                      | `Reviewing code (round <n>)`                                           |
| `fix-findings` | `FIX`                      | `Fixing review findings (round <n>)`, `Fixing QA findings (round <n>)` |
| `qa`           | `QA`                       | `Running QA (round <n>)`                                               |
| `fix-ci`       | `CI`                       | `Fixing CI (attempt <n>)`                                              |
| `fix-conflict` | `MRG`                      | `Resolving conflicts`                                                  |
| `ship`         | `SHP`                      | `Shipping`                                                             |
| `retro`        | `Retro`, beside `Conveyor` | `Writing retro (<n> issues)`                                           |

The activity is the agent's terminal title, which Claude Code writes from the session display name — so pass it as the `--name` native arg at `agent start`; it is set before the first prompt and survives the work that follows:

```bash
herdr agent start eng-42-code-review --kind claude --pane w7:p1 \
  -- --effort high --dangerously-skip-permissions --name "Reviewing code (round 1)"
```

You and the interviewer carry queue depth instead of a fixed activity: `/rename <text>` sets the same name mid-session. Rename the interviewer whenever its queue changes — `<n> questions waiting`, or `No questions waiting`. Rename yourself with the tick's totals — `<n> running · <n> to merge · <n> blocked`, dropping any zero, or `Idle` when nothing is moving. Type the command into your own pane — a local command costs no turn — and it runs only once your turn ends, which is why the tick's last act is its slot:

```bash
herdr pane send-text "$HERDR_PANE_ID" "/rename 3 running · 2 to merge · 1 blocked" && herdr pane send-keys "$HERDR_PANE_ID" enter
```

## Self-pacing invokers

To tick on worker finishes instead of a fixed interval: after each tick, spawn one background command per live lease, each waiting on a single agent so any one exit wakes you:

```bash
herdr agent wait <agent-name> || true
```

A worker's exit — clean self-close or crash — ends its wait: run the next tick, then respawn waits for the leases now live. Stale or duplicate waits just cost a no-op tick — ticks are idempotent.

Under /loop dynamic pacing, schedule a long fallback wakeup (20–30 min) as the heartbeat and let the background waits do the real waking.

