---
name: routine-delivery-loop
type: routine
description: >-
  Driver routine for the full Claude Code delivery loop. Sequences exec → qa →
  route → merge for each eligible ticket in one session, with a 2-strike QA bounce
  cap. Loaded by the Cloud Routine config via a verbatim read. Replaces the three
  separate timer-polling routines (exec, qa, merge timers). Trigger it whenever
  asked to run the delivery loop or babysit the queue.
---

# routine-delivery-loop

The driver routine of the Claude Code delivery loop. Sequences the exec, qa, and merge skills in one session, draining the eligible ticket queue. Replaces three separate timer-polling routines and removes inter-leg latency at each handoff.

## Trigger

Timer-based Cloud Routine. This file is the canon; the Cloud Routine config is a thin loader that reads it (`Read .claude/skills/routine-delivery-loop/SKILL.md and follow it verbatim`). Behaviour changes are made here, never in the routine config.

**Concurrency guardrail — with a staleness escape.** Before running, check for any ticket currently In Progress with label `F0-RGE` — the active-exec-leg signal. If one exists, never run two legs concurrently in the same repo; but first distinguish a genuinely active run from a stalled handoff, so a single stuck leg cannot block every future run indefinitely:

- **Genuinely active** — In Progress under `F0-RGE`, carrying no attached open PR yet, or having entered In Progress recently (within the staleness threshold, default ~3 hours). Another exec leg is mid-run: **skip the run entirely**, as before.
- **Likely-stalled handoff** — In Progress under `F0-RGE` for longer than the threshold *and* already carrying an attached open PR. This is exec having opened the PR but never completed the handoff (never moved to In Review, so the label was never evicted to `PR-1SM` and the assignee was never cleared) — a crashed or interrupted leg, not an active one. **Do not skip silently:** surface it as a likely-stalled handoff for the operator — name the ticket, its open PR, and how long it has sat In Progress — and recommend either resuming the handoff (move to In Review, set `PR-1SM`, clear the assignee) or clearing it back to Todo so the loop can proceed. Do not auto-resume or auto-clear it; the operator decides.

In Progress is the active-run signal only while the run is genuinely active: a leg that opened its PR and then stalled past the threshold is reported, not treated as live. This is distinct from the duplicate-PR / race guards on *opening* a PR (APP-206/APP-208) — it concerns a run that already has its PR and never completed the handoff. (Captured 2026-07-04, delivery-loop run: A1-130 sat In Progress under `F0-RGE` for over 24 hours with an open PR and a stale assignee, skipping every run.)

## Behaviour

A run is a loop. Repeat the following cycle for one eligible ticket at a time, highest priority first, until no eligible ticket remains or the run nears its time budget. Always finish the in-flight ticket's full cycle before ending the run.

**Out-of-band approval sweep — run first, every run, before the eligibility scan.** Sweep In Review for tickets carrying `agent:R3-LAY` — the merge-approval signal `skill-merge` treats as "approved". The loop's own clean-pass routing sets this label in-session and merges immediately, but Aled also approves tickets **out of band** between runs (setting `agent:R3-LAY` on an In Review ticket by hand, per convention-linear), and nothing else ships those: skill-triage and skill-coordinate never touch In Review to merge, so an out-of-band approval sits unmerged indefinitely (worked case: A1-146 approved 2026-07-06 20:00, still In Review after the 01:13 loop run — APP-454). So each run, before draining the Todo queue: list In Review tickets in delivery projects carrying `agent:R3-LAY`, and for each with an open PR, run the `skill-merge` skill (it applies its own CI-clean / conflict fail-safe). This is the same merge leg, fired for approvals set outside the in-session flow. Then proceed to the eligibility scan. Merge stays in this loop rather than moving to the Linear-only Relay tasks (triage / coordinate) because merge needs GitHub repo access, which those do not carry.

**Eligibility scan** (same rules as exec): a ticket is eligible if it is a leaf ticket (never `type:epic`), has label `F0-RGE`, is in **Todo**, has no open blocked-by relation (blocker not Done or Canceled), and carries no unmet dependency named in a PM comment. Scan delivery projects only — never the Pipeline team. If the top candidate is ineligible, move to the next by priority. End the run cleanly if no eligible ticket exists — and on that empty-queue exit, emit the zero-eligible diagnostic (see *End-of-run report*).

For each eligible ticket, execute the following cycle in full before moving to the next:

**Spike-ticket variant — no PR, no QA/merge.** Some `F0-RGE` Todo tickets are **architecture spikes**, not builds: their acceptance criteria are "an ADR is attached, no PR with code" (the `skill-architecture-spike` shape), typically carrying a `spike` label or a spike-shaped title and AC. These are eligible by the letter of the scan but do not fit the exec → PR → QA → merge cycle, which assumes every `F0-RGE` Todo ticket produces a PR. When the top eligible ticket is a spike, run this variant instead of the standard cycle: claim it (In Progress, clear assignee), run the `skill-architecture-spike` skill (which attaches the ADR and never opens a code PR), then route it to **In Review**, set label `human`, assign **Aled**, and post a note that the ADR is ready for his read. **Skip the QA and merge legs** — there is no PR for them to act on. Then continue to the next eligible ticket. (Worked case: A1-87 carried `F0-RGE` + Todo with an "ADR attached, no PR" AC and had to be handled ad hoc — APP-450.)

### 1. Exec leg

Run the `skill-exec` skill for this ticket. Full exec behaviour applies — follow the exec skill for details. In summary:

- Sync to `origin/main`, cut a fresh `claude/<ticket-id>` branch.
- Claim the ticket (In Progress, clear assignee).
- Read the ticket, acceptance criteria (Pattern A), and any PM comment.
- Build, run CI (formatter → lint → format-check → type-check → build, as the repo requires), open a PR only when CI is green.
- Check for a duplicate open PR before opening; take the Blocked path if one exists on a different branch.
- Rebase onto `origin/main`, re-run CI, force-push the updated branch.
- Move ticket to In Review, set label `PR-1SM` (evicts `F0-RGE`), leave assignee clear.

If exec hits an unresolvable blocker, follow exec's Blocked fail-safe: move to Blocked, set priority Urgent, set label `human`, assign Aled, comment the blocker. Skip to the next eligible ticket.

### 2. QA leg

Run the `skill-qa` skill on the handoff. Full qa behaviour applies — follow that skill for details. In summary, working from isolated inputs only (ticket, embedded criteria, diff):

- Check the dedup key (`[qa] HEAD: <sha7>`) — skip if a verdict for the current head commit already exists.
- Assess each acceptance criterion adversarially; cite what was run and observed.
- Post the verdict comment on both the Linear ticket and the GitHub PR, beginning with `[qa] HEAD: <sha7>`.
- Do **not** convert draft PRs to ready — the verdict comment is the handoff signal.

### 3. Route per verdict

Read the verdict outcome and the ticket's human-gate flag (`Human gate: required — <reason>` in the ticket body; absent = none).

- **Unflagged + clean pass** → set label `R3-LAY` (evicts `PR-1SM`), keep In Review, leave assignee clear. This routing is the merge approval signal for unflagged work (see *merge leg*). Run the `skill-merge` skill. Continue to the next eligible ticket.
- **Flagged + clean pass** → assign Aled, set label `human` (evicts `PR-1SM`), set priority **High**, keep In Review, post the verdict with the flagged item leading. Stop on this ticket; continue to the next eligible ticket.
- **Operator-verify-only clean pass** (QA clean except one AC that only Aled's own eyes or judgement can verify — no environment can automate it, e.g. "looks right in /studio") → keep **In Review**, set label `human` (evicts `PR-1SM`), assign Aled, set priority **High** (age to **Urgent** if it lingers), post the verdict noting the one AC awaiting his review. **Not Blocked** — nothing external blocks it, only his sign-off is outstanding (convention-linear *Issue states*; skill-qa; worked case A1-129, APP-441). Stop on this ticket; continue to the next eligible ticket.
- **Block (a genuine external blocker QA cannot resolve without separate work — a missing credential, an environment only CI or a device can provide, an unanswered dependency)** → move to Blocked, set label `human` (evicts `PR-1SM`), assign Aled, @mention with what is needed. Reserve Blocked for a dependency that must clear before the ticket can progress; an AC that merely needs Aled's own review is the operator-verify-only route above, not this. Stop on this ticket; continue to the next eligible ticket.
- **Changes needed** → check the bounce count before routing (see *Bounce cap* below).

### merge leg

When the route is unflagged + clean, run the `skill-merge` skill on the ticket's PR. The loop's clean-pass routing to `R3-LAY` is itself the approval signal — for unflagged, clean, unblocked work the merge proceeds without a separate `@relay` comment. Full merge behaviour applies. If merge hits a CI failure or conflict, it takes its own fail-safe (Blocked, Urgent, human, assigned Aled). Continue to the next eligible ticket regardless.

## Bounce cap

The cap is **two strikes**: one auto-bounce to exec, escalation to Aled on the second "changes needed" verdict for the same ticket.

**Before routing a "changes needed" verdict**, count prior QA bounce verdicts on the ticket:

1. Scan the ticket's Linear comment thread (`orderBy: createdAt`, sufficient limit — see convention-linear *Comment ordering gotcha*).
2. Count comments that begin with `[qa] HEAD:` and contain "changes needed" (case-insensitive) anywhere in their body. This is the number of prior bounces.

**Strike 1 (count = 0):** Auto-bounce to exec — move ticket to **Todo**, set label `F0-RGE` (evicts `PR-1SM`), clear assignee, add a brief Linear comment summarising the fix needed. Re-run the exec and QA legs for this ticket (re-enter from step 1 of this cycle).

**Strike 2 (count ≥ 1):** Cap hit — assign Aled, set label `human` (evicts `PR-1SM`), keep In Review. Comment: "Second QA changes-needed — bounce cap reached. Flagged for Aled's review." Stop on this ticket; continue to the next eligible ticket.

The count is derived from Linear, not session state — a crash or a loop spanning multiple runs reads the correct strike count from the comment thread, so no chain state is lost.

## End-of-run report

Close every run with a short note of what moved (tickets merged, bounced, blocked, flagged). On a run that **ends with no eligible ticket**, distinguish "no work queued" from "work waiting on a gate" with a **zero-eligible diagnostic**: list any `F0-RGE` tickets that exist but are in a non-Todo state (Refinement, Backlog, Blocked, In Review) with their state, so the operator can see at a glance whether the queue is genuinely empty or whether promotion / unblocking is what is missing. A fully empty `F0-RGE` set is reported as exactly that. This keeps a clean zero-eligible run informative rather than silent (captured 2026-06-28, delivery-loop run). A likely-stalled handoff detected by the concurrency guardrail (see *Trigger*) is surfaced here too, so a run skipped or narrowed by a stuck leg is never silent.

## Guardrails

- Merges unflagged, clean, unblocked work autonomously: the merge leg runs `skill-merge`, and the loop's clean-pass routing to `agent:R3-LAY` is the approval signal — no separate `@relay` comment is required (Option A, decision 2026-06-25). Main branch stays protected by the routing gate that precedes merge: only unflagged, clean, unblocked tickets are ever routed to `R3-LAY`, so only they are ever merged.
- **Ships out-of-band approvals too.** `agent:R3-LAY` on an In Review ticket is the approval signal whether the loop set it in-session or Aled set it by hand between runs. The out-of-band approval sweep at the top of each run (see *Behaviour*) is what closes the merge-coverage hole for the latter — otherwise a manually-approved ticket never re-enters the flow and sits unshipped (APP-454). The sweep merges only tickets already carrying `R3-LAY`, so it never approves anything itself.
- Aled is in the path for: flagged work, bounce-cap escalations, QA blocks, and merge failures. He is out of the path only for unflagged, clean, unblocked work — those are merged without him, by design.
- The concurrency guardrail skips only for a genuinely active leg, never for a stalled one: a leg past the staleness threshold with an open PR is surfaced for the operator, not silently allowed to block every future run.
- The driver calls existing skills; it does not duplicate their logic. Updates to exec, qa, or merge skills are picked up automatically.
- Serial by design — one ticket at a time, one PR open at once. One-agent-per-repo preserved.
- Crash-safe: all state lives in Linear (status, labels, PR links, verdict comments). A crash mid-cycle leaves a valid intermediate state; the next run resumes from Linear.
- Delivery projects only. Never touches the Pipeline team (Network / Roles / Advisory / Pitches).

## Cutover note — operator action required

When this driver goes live, **retire the three standalone Cloud Routines** (exec timer, qa timer, merge timer) so there is one writer per repo. Running this driver alongside the standalone routines causes double-writes and competing state transitions. The retirement is a scheduling step Aled performs in the Claude Code Routines settings — it is not automated.

## Setup

- Claude Code Desktop → Schedule → New remote task.
- Connectors: Linear + GitHub (same combined access as exec and qa: Contents read/write, Pull requests read/write).
- Routine prompt (the loader): `Read .claude/skills/routine-delivery-loop/SKILL.md and follow it verbatim.`
- Load MCP tool schemas via ToolSearch as needed for each leg (same schemas exec and qa load individually).

On finish, run skill-ops-retro capture on this run's friction: file a FRICTION note to the Pulse queue for the drain pass to assess.
