---
name: skill-exec
type: skill
description: >-
  Execute a Claude Code ticket end to end and open a pull request for review, on
  behalf of the Forge role. Use this whenever running the exec routine, or when a
  ticket carries the label F0-RGE in Todo and no other F0-RGE ticket is In
  Progress — run the work against the connected repo, open a PR (never merge),
  then hand off to Prism and In Review. Apply this so the lock, handoff, blocked,
  and gate discipline stay consistent. Trigger it whenever the task is to
  implement, build, or fix a Linear-tracked ticket with Claude Code.
---

# exec

The execution role of the Claude Code loop. Runs as a polling Cloud Routine against a connected GitHub repo. Follows `convention-linear`, Pattern A, and the gate policy. **Never merges — merge is Aled's alone.**

## Trigger

Poll for issues carrying label `F0-RGE` in **Todo**, delivery projects only — never the Pipeline team. **Skip the run only if a `F0-RGE` ticket is already In Progress** — that means another exec run is active, and the rule is one Claude Code agent per repo. In Progress is the *active-run* signal: tickets parked in **In Review** (awaiting QA, PM, or Aled) are not In Progress and never hold off a run.

**Stale-claim recovery (run this before the skip guard).** A crashed run can leave a ticket In Progress under `F0-RGE` with no PR and no trace — a ghost claim — and the bare skip guard would then hold off every future run indefinitely. So before honouring the skip, check whether the In Progress `F0-RGE` ticket is a *live* run or a *ghost*. Treat it as a ghost if it has no open PR, no agent comment since it was claimed, and an In Progress timestamp older than one poll interval. A live run is recent and leaves a branch, PR, or comment within its cycle. On a ghost: do not skip — move the ticket back to **Todo**, keep `F0-RGE`, and comment that a crashed run left no PR and the state was reset. This empties In Progress so the leg unjams; the next eligibility scan re-evaluates the ticket on its merits (a still-open blocker will correctly hold it back). Only a genuinely active run holds off the leg.

A run is **not** a single ticket. It works the eligible `F0-RGE` queue **serially — one ticket fully finished before the next is claimed** — until no eligible ticket remains or the run nears its time budget (see Behaviour). Across a run only one ticket is In Progress at any instant; across the repo only one run is active at a time.

**Eligibility scan.** Before each claim — the first and every subsequent one in the run — scan candidates from highest priority downward:

- A ticket is **eligible** if it is a **leaf ticket** (never `type:epic` — epics are outcomes, closed by Aled when their children are done; see convention-linear *Structure*), has no open blocked-by relation (blocker ticket not Done or Canceled), and has no unmet dependency named in a PM comment.
- If the top ticket is ineligible, move down the priority list rather than ending the run.
- If no eligible ticket exists, end the run cleanly with no claim. Do not mark anything Blocked.

## Behaviour

A run is a loop. **Repeat the cycle below for one eligible ticket at a time, highest priority first, until no eligible ticket remains or the run nears its time budget.** Each ticket is carried fully through handoff before the next is claimed — strictly serial, so only one ticket is In Progress at any instant.

For each ticket:

1. **Sync** — fetch and check out latest `main`, then cut a fresh `claude/`-prefixed branch for *this* ticket off it. Every ticket gets its own branch off current `main`, never off a previous ticket's branch (genuine dependencies are blocked-by relations, so a blocker is already merged before its dependent is eligible).
2. **Claim** — move the ticket to In Progress and **clear the assignee**. This *is* the lock: a ticket In Progress under `agent:F0-RGE` is being worked and is never re-grabbed. No separate lock label.
3. Read the ticket, its embedded acceptance criteria (Pattern A), any PM comment, and — if this is a bounce — Aled's note. **If the ticket delivers files as Linear attachments** (ops-publish tickets do this routinely), fetch them with `mcp__Linear__get_attachment`, not `curl` or `WebFetch` — see *Guardrails*.
4. Run Claude Code on this ticket's branch. Open a PR. Never merge.
5. **Rebase before hand-off** — fetch `origin/main`, rebase the branch onto it, resolve any conflicts, and re-run format and tests to confirm the PR is clean against the current baseline. Force-push the updated branch. If the rebase produces unresolvable conflicts, take the Blocked path.
6. **Tick both sections, then hand off** — walk **both** checkbox sections, `## Acceptance criteria` (per ticket) and `## Definition of done` (per type), and tick every box this change genuinely satisfies (`- [x]`); leave any box you cannot satisfy unticked and call it out, never quietly skip it. **Never author or edit the acceptance criteria** — AC is the author's, ticked here, not written here; a box you cannot meet is a bounce or a Blocked, not a reworded criterion. Never hand off with a tickable box in either section left blank. Then set `agent:PR-1SM` (evicts `agent:F0-RGE`), move to In Review, **leave the ticket unassigned**, and comment the PR link and a short summary. The assignee stays clear through exec and review; Prism assigns Aled once review is done (see qa). This keeps "assigned to Aled" meaning "Aled's decision is needed now", not "in flight". Leave a comment for the PM leg where project management is needed.

Then re-run the eligibility scan and take the next ticket. **End the run** when no eligible ticket remains. If the run is nearing its time budget, stop claiming new tickets — but always finish the in-flight ticket's handoff first, so an ending run leaves at most the one ticket it was actively working part-done (the same exposure as a single-ticket run).

**Blocked (fail-safe).** If you hit a blocker you cannot resolve — push rejected / no write access, a missing dependency or credential, or a requirement too ambiguous to act on safely — do **not** leave the ticket In Progress and do **not** open a half-baked PR. Move it to **Blocked**, set `exec:human` (evicts `agent:F0-RGE`), assign Aled, and comment plainly what blocked you and what is needed to clear it. This empties In Progress so the leg is not jammed and the next ticket can run, and it surfaces the blocker to Aled. Aled clears the blocker and moves the ticket back to Todo to retry. Within a run, a Blocked outcome ends only *that ticket's* cycle — the run continues to the next eligible ticket.

**Bounce:** Aled moves In Review → **Todo** with a note; the next run re-picks it and iterates on the existing PR. (Todo, not In Progress, so In Progress always means "running now.")

## Guardrails

- Never merges. Main branch protected; merge is Aled's.
- On an unresolvable blocker, move the ticket to **Blocked** + `exec:human` + assign Aled. Never leave it In Progress — that jams the leg and it never retries.
- A crashed run can leave a ticket In Progress under `F0-RGE` with no PR — a ghost claim. Detect it at run start (In Progress, no open PR, no comment since claim, older than one poll interval) and reset it to **Todo** with a note, rather than skipping every future run. The skip guard is for active runs only.
- **Linear attachments — fetch with the MCP, never curl or WebFetch.** Linear attachment URLs (`uploads.linear.app`) are blocked by the environment proxy (`curl` returns `CONNECT tunnel failed, 403`). Use `mcp__Linear__get_attachment` with the attachment ID from the issue's `attachments` array; it fetches cleanly first time. This recurs on any exec or ops run where files are delivered as Linear attachments (ops-publish tickets), so reach for the MCP first rather than discovering the proxy block per run.
- **Tick both sections; never author AC.** Tick `## Acceptance criteria` and `## Definition of done` as built, leaving any unmet box unticked and flagged. Forge never writes or edits acceptance criteria — that is the author's at refine; an AC it cannot meet is a changes-needed bounce or a Blocked, not a reworded criterion. Never hand a ticket to review with a tickable box left blank in either section.
- One PR per ticket. Never open a partial or speculative PR to "make progress" — Blocked is the honest outcome.
- A run works the whole eligible queue, but **serially** — one ticket fully handed off before the next is claimed, each on its own branch off latest `main`. Never run two tickets' branches in one session in parallel; they would collide in the repo.
- Fresh cloud session each run, so all state lives in Linear, not the agent.
- Scope GitHub access to delivery repos only. Never touches the Pipeline team's work.

## Setup

- Deployed as a Claude Code cloud scheduled task (cloud, machine off).
- Connectors: Linear + GitHub. The Claude Code GitHub App must have **Contents: read & write** and **Pull requests: read & write** on the delivery repo — read-only causes a 403 on push, which is a Blocked outcome, not a crash.
- One repo to start; widen once the loop is proven. Cadence: hourly or slower.
- The routine prompt: "Run the exec skill."

> Lock note: this skill and `convention-linear` both use **status as the lock** (Todo = ready, In Progress = running). No `state:locked` label exists or is needed.

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.
