# Auto

> Autonomous Linear-backlog iteration — ships exactly ONE issue per invocation (preflight → /next specified → /full auto wt → record outcome), with a skip-and-circuit-breaker failure policy. Accepts an optional issue ID (`/auto BF-123`) for targeted mode — skip the pick and ship that specific certified issue unattended. Run continuously via `/loop /auto`; the loop ends itself on NO-CANDIDATES or AUTO-HALTED. Invoking /auto IS the run-scoped commit/push grant (standards/git.md). Use when the user says 'auto', 'work autonomously', 'work through the backlog', 'ship the next issue unattended', 'ship BF-123 unattended', or invokes /auto (typically as /loop /auto).

- Skill: `alienfast/auto` (Agent Skill)
- Install (CLI): `npx skillmds@latest add alienfast/auto`
- Raw SKILL.md: https://api.skillmd.com/api/skills/alienfast/auto/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: alienfast (https://skillmd.com/u/alienfast)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/alienfast/auto

---


# Auto (Autonomous Backlog Iteration)

Ships **exactly one Linear issue per invocation**, end-to-end and unattended: finish any in-flight work, pick the best next issue via `/next specified` (certified issues only) — or, in **targeted mode** (`/auto BF-123`), take the issue named in the invocation — ship it via `/full auto wt`, record the outcome, emit a tagged final line. Continuous operation is `/loop /auto` — each loop iteration is one issue, and the loop ends itself when the backlog drains (`NO-CANDIDATES`) or the circuit breaker trips (`AUTO-HALTED`).

**Why one-issue-per-invocation instead of an internal "keep going" loop:** in-prose anti-stop scaffolding is the documented failure mode of autonomous macros (`/full` records it failing three times before its Stop hook existed). `/loop`'s wakeup machinery is the reliable recurrence mechanism; the `full-continue.sh` Stop hook guards the intra-issue start→finish handoff, and the `auto-heartbeat.sh` Stop hook guards the loop heartbeat the same way — added after prose failed a third time (BF-701, BF-665, BF-695). This skill keeps its reliability in those hooks and in `/loop`'s machinery, not in prose scaffolding — it composes the enforced pieces.

**Why worktree mode is not optional:** in-place mode would leave the checkout on issue N's branch, and `/start`'s "already on a non-`main` branch → stay on it" rule would stack issue N+1 onto it — every subsequent issue cascading onto the first issue's branch. `wt` mode forks each issue from the source branch and `/finish merge` folds it back, so successive issues chain correctly (each fork sees the prior merges) with no stacking. `/auto` therefore ALWAYS dispatches `wt`.

## Authorization — read this first

**Invoking `/auto` (directly or via `/loop /auto`) is the explicit, run-scoped standing grant for commits and pushes.** `standards/git.md` forbids treating "commit" or "push" as a session-wide grant; `/auto` is the single named exception, because unattended issue-shipping is its entire documented contract. The grant covers exactly: the `/finish auto` commit+push of each issue this run ships. It does not cover force-pushes, history rewrites, or committing work that cannot be attributed to a Linear issue (see Preflight). Every shipped change is audited via the plan comment (`/start auto` Step 7) and completion comment (`/finish` Step 4) on its Linear issue.

## Unattended-run prerequisites

This skill cannot change permission modes or pick its own model. For a genuinely walk-away run:

- **Launch with `--model opus[1m] --effort xhigh --autocompact 500000`.** Do NOT use `opusplan`: it means "Opus in plan mode, Sonnet otherwise", and Step 3's `/full auto` dispatches `/start auto`, which skips `EnterPlanMode` entirely (`/start` Step 6). The Opus branch never fires, so an `opusplan` run executes end-to-end on Sonnet. `[1m]` gives one oversized issue headroom, but **unbounded** cross-iteration accumulation is the dominant fleet cost: on `opus[1m]` the default compaction threshold sits near the 1M window, so a looping session never compacts, and the 2026-08-13/14 fleets processed 91% of their billable volume at >200k context (cache reads scale linearly with context — this is what let 3 sessions exhaust a 5h window that 9 leaner sessions survived). `--autocompact 500000` caps the working set; cross-iteration state lives in `tmp/auto-state-*.json` and Linear, so compaction between issues loses nothing. Not lower: compaction triggers at ~90% of the window, and the trigger must clear a deep-issue session's post-compact floor (~152–177k) plus the >=110k live working set a review/fix loop re-reads after every compact plus one ~130k worst-case ingestion — 150k thrash-aborted the 2026-08-14 fleet at launch, and 300k fell into a mid-review compaction orbit (9 compacts in 36 min) the same night (doc/compacting-investigation.md, verdict log). Never resume a deep fleet session; recover in-flight issues via fresh targeted `/auto BF-XXX` sessions. Fable 5 (`--model fable`) is the alternative for unusually large issues — better long-horizon retention — but it runs safety classifiers that can return `stop_reason: "refusal"`, which reaches Step 4 as an unrecognized-tag failure and counts against the circuit breaker.
- Run the session in a permissive mode; consider `/fewer-permission-prompts` first to seed a project allowlist. **Interactive** sessions (a watched terminal/VSCode): auto-accept edits or `claude --permission-mode acceptEdits` suffice — someone can answer the occasional Bash prompt. **Background** sessions (`claude --bg`, `/fleet-launch`): use `--permission-mode auto` — acceptEdits only auto-accepts file edits, so the first gated Bash command (observed: `/start wt`'s worktree validation) stalls the session at a prompt nobody can see, before its first pick.
- A permission prompt mid-run does not break anything — the run pauses until answered and `/loop` resumes normally. Expect occasional prompts on milestones that touch unusual commands (e.g., native-bridge/hardware work).
- Context growth across iterations is handled by the harness's automatic summarization. `/compact` and `/clear` are user commands — never attempt them; `tmp/auto-state-<runKey>.json` (Step 4) is the cross-iteration memory that survives summarization. You have ample context; never stop, summarize, or propose a new session on account of context limits.

## Operating unattended

Nobody is watching in real time, so a question ends the run instead of resolving it. For any reversible action that follows from the invocation, proceed — every prompt site in the skills this dispatches already documents an `auto` default, and taking it IS the answer. The exceptions are the ones the workflow names: an unattributable dirty tree, a live sibling's worktree, an uncertified target. Those halt with a tag, which is not the same as asking.

Ground progress claims in tool results, not recollection. Before recording an outcome in Step 4, each claim traces to something this session actually observed — a tag from the dispatched skill, a `git status`, a script's exit code. Report faithfully: if a check failed, say so with its output; if a step was skipped, say that.

Before ending a turn, read your last paragraph. If it is a plan, a question, or a promise about work not yet done ("I'll ship it next", "waiting on the review"), do that work now with tool calls instead. Every turn ends on either a lifecycle tag or a genuine block.

## Fleet-view visibility — issue-keyed narration

The `claude agents` view titles a session from its FIRST prompt (Haiku-generated, fixed once set; `claude -n` and `/rename` are user-side levers this skill cannot pull), so a loop-picked run's title crystallizes from Step 0/1 preflight mechanics and never contains the issue key — only targeted mode's key reaches the title, via the invocation text. The right-hand status line is different: it is re-summarized from recent session activity (~15s cadence plus turn end), so what this session *says* is what the fleet operator *sees*. Therefore: **from the moment the issue ID is fixed (Step 2's pick, or the targeted argument) until the iteration tag, begin every user-visible chat status line with `<ISSUE-ID>: `** — narration between tool calls, checkpoint notes, and progress remarks alike. Keep the lines short; the summarizer compresses them. This composes down through the skills this session dispatches (`/full`, `/start`, `/quality-review` run in-session and inherit the rule). Lifecycle-tag lines are exempt — their `<TAG>: <ISSUE-ID> — ...` contract already carries the ID and must stay TAG-first.

## Self-paced loop pacing

Under self-paced `/loop` (invoked without an interval), every iteration must end with a `ScheduleWakeup` or the loop dies — and the delay is this skill's call, not the tool's default. On `AUTO-CONTINUE`, schedule the **minimum** `delaySeconds` (60s): the backlog *is* the work queue, Step 2 already established it is non-empty, and there is no external state whose change is being waited on. `ScheduleWakeup`'s 1200–1800s idle-tick guidance is written for polling loops (CI, deploys, remote queues) and does not apply here — inheriting it inserts 20–30 idle minutes between every shipped issue, which is how one observed run spent ~3.5 hours doing nothing across 9 ships. The only delays this skill prescribes are the ~900s API-error retry cadence (next section), the ~1800s fallback heartbeat (next paragraph), the 1800s headroom park (Step 2), the `PLANNED-HOLD` wait (Step 2: 600s while a held Planned issue will release on its own, 1800s when only the keeper can release one), and the `BLOCKED-HOLD` wait (Step 2: 600s — a sibling's ship is the wake signal). On `NO-CANDIDATES` or `AUTO-HALTED`, end the loop explicitly with `ScheduleWakeup(stop: true)` — it also cancels a pending heartbeat, which plain not-scheduling would leave to fire one more no-op turn against the sticky terminal state.

**The fallback heartbeat is armed at turn end, never mid-turn.** `ScheduleWakeup` is turn-ending by contract: `/loop`'s dynamic-mode step 3 says "the turn ends as soon as that tool returns", step 4 says to call it "as the last action of this turn", and the tool's own result reads "Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives." It therefore cannot be armed *before* an iteration's work: doing so ends the turn before Step 0 runs and idles the session for the full delay. Arm it as the **last action of any turn that ends mid-iteration** (~1800s), and supersede it with the 60s tick when the iteration completes — only one dynamic-loop wakeup is ever pending, since each `ScheduleWakeup` cancels the previous (the runtime telemeters this as `superseded_count`). The safety net against an *accidental* turn-end is the Stop hook, not pre-arming: `hooks/auto-heartbeat.sh` blocks any turn ending inside a self-paced `/loop /auto` iteration with no `ScheduleWakeup` since the anchor and re-drives the arming (1800s mid-iteration, 60s post-`AUTO-CONTINUE`), giving up after 3 attempts. Both observed stall shapes — BF-701 (ended on `SHIPPED-MERGE` without running Step 4) and BF-665 (announced a re-review dispatch it never made) — are exactly what that hook now catches. Extra firings remain benign: one landing right after an `AUTO-CONTINUE` tick or against a terminal state reads as a status check and is absorbed by the re-entry rules and Step 0's sticky states. (Fixed-interval `/loop` and one-shot/targeted runs get no heartbeat: the interval timer already re-fires the former, and a wakeup would wrongly re-fire the latter.)

## Transient API failures are never failures

A Claude API error — overload (529), rate limit (429), or transient 5xx — from any tool call or delegated agent is **infrastructure, not an issue failure**. Never count it toward `consecutiveFailures`, never mark the issue failed, never emit `AUTO-HALTED` for it — except the beyond-ceiling quota reset carved out below. Instead: retry with a short delay (respect a `Retry-After` header or stated reset time when one is given). If retries keep failing, fall back to a **15-minute wake-and-retry cadence** — under self-paced `/loop`, end the turn with a `ScheduleWakeup`-style ~900s delay and resume the same iteration on wake; under fixed-interval `/loop`, simply end the turn and let the next interval retry. A retry turn still ends with a tagged final line: `AUTO-CONTINUE: <ISSUE-ID> paused mid-issue (Claude API <error>); retrying in ~15m.` — or, when the failure hit before an issue was picked (Step 0/1/2, including `/next` itself), `AUTO-CONTINUE: paused pre-pick (Claude API <error>); retrying in ~15m.` The next iteration resumes where it left off — Step 1's preflight checks route back into the same issue (`/start`/`/full` are idempotent on it).

**A reset beyond the wakeup ceiling is an environment halt, not a slower poll.** `ScheduleWakeup` clamps `delaySeconds` to [60, 3600], so an error stating a reset hours or days out — a weekly or multi-day quota rather than a per-minute rate limit — cannot be waited on: the ~900s cadence above would fire hundreds of futile wakeups against the same limit, and a 3-day block sits inside the dynamic loop's own 7-day age-out, so nothing ends it. This is the one carve-out from "never halt for it" in Failure policy: when the stated reset is materially beyond the 3600s ceiling, treat it as an **environment halt** — the same category as Step 1's unattributable dirty tree and Error Handling's broken `/next`. Set `status: "halted"` with a `reason`, and emit `AUTO-HALTED: Claude API quota exhausted (resets <stated time>) — wait for the reset, then delete tmp/auto-state-<runKey>.json and re-invoke /auto.` Under self-paced `/loop`, pair it with `ScheduleWakeup(stop: true)` as any `AUTO-HALTED` does.

**Re-test before halting on a quota observation you did not just make.** The halt above is the only irreversible response in the failure policy — it is sticky across re-invocations (Step 0) and ends the loop — so it is owed a *current* reading, and a quota error already in your context is not one. The observation goes stale in exactly the case that produces it: an exhausted account stalls the session, and the session resumes only because something changed underneath it — a reset, an account switch, a plan change — none of which announce themselves in the transcript. The stale error is then the most recent thing you saw, which reads as present state. Observed on the 2026-08-06 BF fleet: `d7d04bd6` resumed after a 2.2-hour stall and declared the run halted on the error it had hit before it, and one cheap dispatch afterward proved quota was fine; a human had to push back to recover the session. Before writing `status: "halted"`, spend one trivial call (a `Bash` `echo`, a one-line `Agent`) and halt only if *that* call fails on quota. Treat any gap between the error and the halt decision as making it stale — a turn boundary, a wake, a resumption — and treat a resumption after a long silence as positive evidence the condition may have lifted, never as continuity. The check costs a few tokens against a halt that costs the rest of the run.

It is still **not an issue failure**: record no outcome and leave `consecutiveFailures` and `reviewBlocks` untouched, exactly as the other environment halts do. Leave the in-flight issue In Progress with its worktree and branch intact, and follow the failure rows' outward-markers-first ordering minus the label — post a Linear comment naming the block and the reset time, then **release the worktree's ownership stamp** (`~/.claude/scripts/wt-disown.sh <main-checkout>/.claude/worktrees/<issue-id-lowercased>`, best-effort), then write the state file. The release is not optional: without it the stamp reads `alive` for this harness process's whole lifetime, so Step 1 skips the worktree, Step 2 drops the candidate, and `start-wt-create.sh` refuses — the BF-525 trap, where the resume path the comment advertises is one the stamp itself blocks. Do NOT apply `stalled`; that marker is for genuine pipeline failures, and this is infrastructure. After the reset a human deletes the state file and re-invokes: `/start` and `/full` are idempotent on the issue, so a fresh `/loop /auto` — or a targeted `/auto <ISSUE-ID>` — resumes it.

**Non-Claude API failures of the same shape count too.** A failure from another service's API that is plainly transient infrastructure — `linear-cli` returning `HTTP 5xx` / `Service Unavailable`, or a timeout — is handled identically to the Claude errors above: probe once (`linear-cli issues get <ISSUE-ID>`), retry, and record no issue outcome for it. This includes one that reaches this skill as a *terminal tag* from a dispatched skill: a `/start` auto-mode `BLOCKED-ON-REVIEW: <ISSUE-ID> — claim update failed (linear-cli error)` whose script stderr shows a 5xx is a service blip, not a review block — re-dispatch the same `/full auto wt <ISSUE-ID>` (idempotent on the issue and its worktree) instead of applying Step 4's failure row, which would post a failure comment and `stalled` label on a healthy issue and burn one of the 4 `reviewBlocks` slots. Authentication, permission, and unknown-state-name failures are NOT transient and keep their documented handling.

**Heartbeat re-entry mid-iteration.** Under self-paced `/loop`, a fallback wakeup may re-deliver this skill while the current iteration's delegated work is still in flight in this same session. That re-entry is a status check, not a new iteration: check on the outstanding work (ping stalled agents, verify state), re-arm the fallback at 1200–1800s (`ScheduleWakeup`'s own "long fallback heartbeat" guidance — the delegated `Agent` call's task notification is the real wake signal here, not the timer; this is NOT the 60s `AUTO-CONTINUE` figure above, which is scoped to the no-external-wait backlog tick), then end the turn on that call and wait. **Do not run Step 0's re-anchor / `ExitWorktree`** — the in-flight iteration is still registered on its worktree, and releasing that registration would guard-block its outstanding `developer` writes (Step 0's `ExitWorktree` is only safe at a true iteration boundary, when no delegated work is live). Do not re-run preflight or dispatch /next, and do not treat the in-progress worktree as orphaned — Step 1's resumption path is for a new invocation after a died iteration, not for one still running here.

**Stall recovery on re-entry.** When re-entry finds **no** delegated work in flight and no iteration tag yet, the previous turn stalled — it ended without a tool call (an announced-but-unmade dispatch, or an inner skill's closing message obeyed as a turn end). Recover by resuming the iteration exactly where the transcript left off: make the missing dispatch, or — if `/full`'s terminal tag was already emitted — go straight to Step 4. This recovery, not the status check, is the heartbeat's primary purpose.

**A human prompt that recovers a stalled iteration resumes the ITERATION, not just the issue.** When an operator attaches to a stalled run and prompts it forward ("limit lifted, complete this issue"), completing and verifying the issue is not the end of the work: run Step 4 — record the outcome in the state file and end on a terminal status or wakeup — before reporting back. Nothing mechanical re-drives this: `hooks/auto-heartbeat.sh` deliberately stands down after a human message (`human-override` — it must not fight an operator mid-decision), so the bookkeeping either happens in this turn or not at all. Measured 2026-08-16: a weekly-limit cutoff killed a session in the seconds between `/full` returning and Step 4; the operator's recovery prompt got BF-844 verified shipped, the session reported and stopped, and the ledger read `active`, `shipped: []` for a merged issue until the fleet retro corrected it by hand.

## User stop semantics

A user instruction to stop the loop ("do not run another loop", "stop after this one", "no more iterations") ends **recurrence, not the in-flight iteration**: schedule no further wakeup, and complete the current issue normally — every inner loop keeps its documented budget. A red re-review during wind-down enters `/quality-review`'s fix loop exactly as it would have before the instruction; "not shippable yet" is the fix loop's *input*, never a reason to park (the observed failure mode: a cycle-2 red re-review parked as `terminated-with-open-items`, a verdict whose only documented producers are the 5-cycle ceiling, the regression cap, and contamination — there is no user-stop producer). Parking the in-flight issue requires explicit words — "park it", "stop now", "abandon it" — and is never inferred from a stop instruction, a failing verdict, or their combination. Two further rules: once you have announced a wind-down default ("I'll finish this issue unless you say otherwise"), new information reopens that decision only **through the user** — never by silently switching to the branch they didn't choose; and any surprise decision point during wind-down is ASKED, not improvised — the user just spoke, so they are present. The conservative-abort posture of Failure policy is for unattended operation; the moment the user is interactive, "ask" dominates "abort".

## Parallel sessions

Multiple `/loop /auto` sessions on the same repo are supported. They coordinate through shared session-agnostic state, never through each other:

- **Linear is the claim registry.** `/next` offers only unstarted states (Backlog/Planned/Todo), so a claimed (In Progress) issue is invisible to every other session's pick. The residual pick race — two sessions taking the same top candidate before either claims — is closed at the git layer: `start-wt-create.sh` runs under the repo lock and refuses to reuse a worktree owned by a live other session, so exactly one session wins and the loser exits `SKIPPED-BLOCKED` (a skip, not a failure).
- **Owner liveness distinguishes died from live.** Every `/start wt` stamps the owning harness PID + start time into the worktree identity; Step 1 resumes only a worktree whose owner is provably dead. Alive → a sibling's active work; unknown → parked for a human. Never resume on "In Progress + mine" alone. **Known limitation: the PID tier is harness-process-granular, and under a fleet it fails in BOTH directions (BF-1103).** A session that dies inside a still-live harness (one closed window among several, a long-done fleet session whose shared root lives on) reads `alive` until that process exits — its worktree unresumable and its issue unflagged for that window — while a LIVE fleet session can read `unknown` or `dead`: the pid walk resolves nothing when the pool chain is reparented to pid 1 (stamps carry a blank pid), and the shared fleet root's exit reads every still-live sibling dead at once. `wt-identity.sh` therefore probes the owner's SESSION first — the daemon's per-session job dir (`state.json` lifecycle state, activity mtimes) answers at session granularity for background sessions, in both directions — and `start-wt-create.sh` fails closed on a foreign claim whose liveness stays `unknown`; the pid tier remains the fallback for owners with no job dir (interactive sessions). Residual stalls surface once the harness restarts or the session state goes terminal, via Step 1's dead-owner sweep and its `stalled` labeling. The `released` state closes the tail of this window for stalls this skill itself declares: Step 4's failure rows and Step 1's flagging release the stamp (`wt-disown.sh`) when they label, so a `stalled` issue is immediately resumable by any session even while the shared harness process lives on.
- **Run state is per-session** (`tmp/auto-state-<runKey>.json`): shipped/canceled/skipped/failed lists and the circuit breaker are scoped to one session's run. Parallel runs have independent breakers — systemic breakage can cost up to 2× the failed attempts before both trip, which is accepted.
- **The merge queue is cross-session.** A `DEFERRED-MERGE` marker under `.claude/merge-queue/` gates resumption regardless of which session shipped the issue.

One caveat: **do not run an in-place (non-`wt`) `/start` on the same repo while `/auto` runs.** In-place work dirties the main checkout with no ownership stamp, so Step 1's dirty-tree preflight cannot distinguish it from a died run's leftovers and would try to `/finish` it mid-edit. Interactive work alongside `/auto` belongs in a worktree — `/start interactive <ISSUE-ID>` (worktree-implied) exists for exactly this.

## Arguments

```text
/auto [pr] [TEAM[,TEAM...] | ISSUE-ID]
```

Tokens are case-insensitive and order-insensitive.

- `pr` opens a PR per issue instead of merging (pass-through to `/full`). **Caveat:** in `pr` mode the source branch does not advance until PRs merge, so a dependent issue forks without its predecessor's code — use `pr` only when the queued issues are independent or a human is merging promptly. **`pr` is passed ONLY when this literal token appears in the invocation — never inferred.** Not from how recent issues shipped, not from a project memory or `CLAUDE.md`, not from a sibling session's choice, not from a repo whose `main` is all PR merges. An unattended run's default is merge, and "every recent issue shipped via PR" is a *product* of the interactive convention rather than evidence about this run — treating it as evidence is circular, and it drifted three consecutive issues that way over one 2026-08-20 run before anyone noticed.
- A team scope (`BF`, or a comma list `PL,BF`): restricts the whole run to those teams' certified backlogs — forwarded to Step 2's pick as `team:<KEYS>`. Without it, scope follows `/next`'s resolution (`$LINEAR_TEAM`, else every team in the workspace). **At most one** team token is accepted, and it MUST validate before it is trusted: uppercase it, then check every comma-part against the workspace's real team keys (`linear-cli teams list -o json`, case-insensitive). Any part that is not a real team key — `wt`, `auto`, `team`, a typo — is an unrecognized argument: emit the error below and STOP. An unattended run must never launch scoped to a nonexistent team (it would silently mark itself `drained` against an empty backlog). (`pr` is reserved for the PR flag, so `pr,BF` fails validation by design; a workspace whose team is literally keyed `PR` scopes via `$LINEAR_TEAM` instead.)
- **An issue ID selects targeted mode** (`/auto BF-123`, `/auto pr BF-123`) — ship exactly that issue, skipping Step 2's pick. Parse order: strip `pr` first; a token matching `^[A-Za-z]+-[0-9]+$` is the issue ID (normalize via `~/.claude/scripts/detect-issue-id.sh --validate-only --input <token>`); only then is a remaining bare token tried as a team scope. At most one issue ID, and an issue ID together with a team scope is an error (the target already names its team): `Targeted mode takes no team scope — /auto <ISSUE-ID> ships exactly that issue.`

**Targeted mode requires the `specified` label** — the invariant stays one sentence: `/auto` ships only certified specs, picked or targeted (`standards/issue-spec.md`). Probe before dispatching anything:

```bash
linear-cli issues get <ISSUE-ID> -o json | jq -r '.labels.nodes[].name' | grep -qix specified
```

Label absent → refuse and stop with a plain error — no lifecycle tag, no state change; this is invocation-time argument validation and the user is present, having just typed the ID: `<ISSUE-ID> is not certified (no specified label) — run /spec <ISSUE-ID> to certify it, or /full wt <ISSUE-ID> for an interactive run.` (A probe failure from auth/network is Error Handling's `linear-cli` territory, never a silent pass.)

The probe checks `specified` and one exclusion — `human`. A `human`-labeled target is refused the same way (probe with `grep -qix human` on the same command): that label marks work a person performs, so no mode of this skill can ship it (`standards/issue-spec.md`) — `<ISSUE-ID> is human-owned work (human label) — assign a person, or /spec <ISSUE-ID> to split out an agent-shippable slice.` A `solo` target is expressly legitimate: that label hides an issue from `/next`'s ranking precisely so it can only arrive here, by a human typing the ID once no fleet is running (`standards/issue-spec.md`). Never extend the probe beyond these two — refusing `solo` would strand work that is fully automatable and leave no path to ship it.

Targeted mode narrows the workflow in exactly four places, each marked at its step: Step 0's sticky terminal-state gate is bypassed, Step 1's preflight is scoped to the target, Step 2 is skipped, and Step 4 never transitions `status` or schedules a wakeup. Everything else — always-`wt`, the run-scoped commit/push grant, auto defaults, outcome recording, the failure Linear comment and `stalled` label — is identical. It is one-shot by nature: run it directly, not under `/loop` (a re-fired loop would just skip-block against the now-terminal issue).

Error on anything else — a second team-shaped token, a second issue ID, or a token failing team validation: `Unrecognized argument 'X'. /auto accepts optional 'pr' plus either one team scope matching a real team key (e.g. BF or PL,BF) or one issue ID for a targeted run; worktree mode is always on.`

## Workflow (one iteration)

### Step 0: Entry gate — re-anchor and read the run state

**Re-anchor cwd to the main checkout first.** A prior iteration may have ended with cwd inside `.claude/worktrees/<id>` and the session still registered on that worktree (`/start wt` Step 0 enters via `EnterWorktree`, which switches cwd *and* registers it as the session's isolation root; only the merge path cd's back, and a shell `cd` never releases the registration). **Release it first:** call `ExitWorktree(action: "keep")` — a no-op when no registration is active, and it never removes a `path`-entered worktree — clearing a registration that may point at a worktree the prior iteration's `/finish merge` (or the reaper daemon) has since deleted, so the next `/start wt` `EnterWorktree` is a clean first entry rather than a switch away from a dangling root. Then resolve the main checkout (the first `worktree <path>` line of `git worktree list --porcelain`, unambiguous even with spaces in the path) and `cd` there before anything else. This skill's run-state file (`tmp/auto-state-<runKey>.json` — defined next) lives in the main checkout; write it via the shell (see the bgIsolation note below) — the shell bypasses the Write-tool guard, so it is safe whether or not a worktree is registered. (Reads are unaffected.) **Every read or write of the run-state file — Step 0, Steps 1–2's terminal transitions, Step 4, and all Error Handling transitions — uses `<main-checkout>/tmp/auto-state-<runKey>.json`** (carry the resolved absolute path through the iteration); a state file touched inside a worktree would fragment the run state (resetting the breaker and skip lists, making halts non-sticky) and be deleted with the worktree.

**Session identity → per-session state path.** Resolve this session's identity alongside the re-anchor: run `~/.claude/scripts/session-identity.sh` and read `RUN_KEY` (the per-session id when the harness provides one, else the stable harness PID) plus `HARNESS_PID`/`HARNESS_PID_START` (the liveness anchor embedded in the state file). Never key on raw `$PPID`: in a `claude agents` fleet the tool shell's parent is a transient `bg-spare` pool process whose pid changes between commands, and even solo, a harness restart (extension update) changes pids mid-session — the session id survives both. The run state lives at `<main-checkout>/tmp/auto-state-<runKey>.json` — **per-session by construction, so parallel `/loop /auto` sessions never adopt or clobber each other's run state** (a shared path is exactly how a second session once "resumed" the first's run — see Parallel sessions). Every iteration re-derives the same `RUN_KEY`; wherever this skill (including emitted tag texts) says `tmp/auto-state-<runKey>.json`, substitute the actual value. Two hygiene rules on entry: (1) if the file exists and `RUN_KEY` is a session id, refresh its embedded `pid`/`pidStart` to the current values when they differ (a harness restart changed the pid, not the run); if `RUN_KEY` is the pid fallback, a `pid`/`pidStart` mismatch means PID recycling — recreate it empty rather than inherit foreign state. (2) Best-effort GC, and it has exactly one safe target: a sibling `tmp/auto-state-*.json` whose `status` is `"active"`, whose embedded `pid` is no longer a live process, **and whose `shipped`/`canceled`/`skipped`/`failed` lists are all empty** — a run that died without recording an outcome. **A file holding any recorded outcome is a ledger and is never deleted, whatever its `status`.** **Never delete a file whose `status` is `drained` or `halted`.** That file is not a leftover, it is the run's ledger: its shipped/canceled/skipped/failed lists are the only record of what a finished session did, and `/fleet-retro` discovers sessions from exactly these files. Deleting one erases a completed run from history — including, when the run died at preflight, the evidence that it died. (Ledger *expiry* is `/fleet-launch`'s job, not this GC's: the next launch clears dead prior-run ledgers after the between-runs window in which `/fleet-retro` reads them — this skill's GC still never touches them.) **And never trust `pid` alone to tell siblings apart in a fleet:** `session-identity.sh` resolves `HARNESS_PID` to *the fleet root* when running under `claude agents`, so every session in a fleet embeds the SAME pid, and `kill -0` answers identically for all of them — alive while the root lives, dead for all of them at once when it exits. (It also goes empty mid-run, which is how a session ends up embedding some other pid entirely.) **And `status` alone is not sufficient: never delete an `active` file whose outcome lists are non-empty.** The status check protects a run that *reached* a terminal state; it does not protect one that shipped work and was then killed — in a fleet the common shape rather than the edge case, since the shared root pid above reads dead for every session at once. Such a file is a ledger too: `/fleet-retro` still recovers the *session* from its transcripts (`fleet-metrics.py`'s second discovery pass), but the recorded `shipped`/`canceled`/`skipped`/`failed` counts are gone and that row falls back to transcript tags, which undercount a compacted session. And because a dead shared pid is not evidence *this* sibling died, the deletion can instead reset a live run's breaker and skip lists mid-run. The only safe target is an `active` file whose `shipped`, `canceled`, `skipped`, and `failed` are ALL empty — a run killed before it recorded anything. Content, not liveness, is what protects a ledger; the pid check only bounds which empty `active` files are worth clearing. Observed 2026-08-05: eleven state files, seven of them `active` on one dead fleet-root pid and each recording a shipped issue — every one a literal match for the safe target as written. Observed 2026-08-04: a surviving session ran this GC at 01:53 UTC, found the shared root pid dead, and deleted two `drained` ledgers holding 7 of the fleet's 12 ships — the retro then measured half the run. A legacy un-suffixed `tmp/auto-state.json` predates per-session state and belongs to no current run: ignore it (mention once that it can be deleted).

**Background-job note (bgIsolation).** In a background session the harness's worktree-isolation guard refuses Write-tool edits outside the registered worktree — and `/start wt` Step 0 registers its worktree via `EnterWorktree`, so `<main-checkout>/tmp/auto-state-<runKey>.json` (which this skill REQUIRES to live in the main checkout) is outside it. That file is gitignored, run-scoped bookkeeping owned by this skill, not project code: write it via the shell — always, as Step 0's re-anchor prescribes (e.g. `printf '%s\n' '<json>' > <main-checkout>/tmp/auto-state-<runKey>.json`); the shell `printf` bypasses the Write-tool guard entirely, and unlike a Write-tool edit it never trips the read-before-Write requirement instead of relocating it into a worktree (fragments the run state) or setting `worktree.bgIsolation: none` (removes the guard for all code edits, not just this file — and `/start` Step 8 forbids it). Between issues, Step 0's re-anchor calls `ExitWorktree(action: "keep")` to release the prior iteration's registration before the next `/start wt` re-registers — so the next `EnterWorktree` is a clean first entry, never a switch away from an already-deleted worktree.

Read `<main-checkout>/tmp/auto-state-<runKey>.json` (see Step 4 for the shape). **One file = one run — and one session = one run:** a new session's PID names a fresh file, so it starts a fresh run automatically; within a session, the file is what makes terminal states sticky across `/loop` re-invocations. If it exists and `status` is `halted` or `drained`, do not proceed — re-emit the stored terminal condition using the `reason` field (every transition that sets a terminal `status` MUST also set `reason` to the one-line summary it emitted):

- `halted` → `AUTO-HALTED: run previously halted — <reason from state file>`
- `drained` → `NO-CANDIDATES: <reason from state file>`

The stored `reason` already ends with its next action (usually "delete `tmp/auto-state-<runKey>.json`…"), so append nothing. If a terminal state file predates the `reason` field (or it's empty), emit the tag with `reason unavailable — see the run's Linear comments and the shipped/canceled/skipped/failed lists in tmp/auto-state-<runKey>.json; delete it to start a fresh run.` — do not improvise a cause.

**Targeted mode bypasses this terminal-state re-emission** — an explicitly-typed issue ID is the human intervention the halt asked for, so proceed with the run state as-is rather than parroting the stored condition back at the user. Leave `status` untouched (the stored halt/drain still governs the next bare invocation); the targeted outcome still records into the lists per Step 4.

This makes the circuit breaker hold even under fixed-interval `/loop` (e.g., `/loop 15m /auto`), which re-invokes regardless of the previous iteration's tag. A human starts a new run by deleting the state file (its skip/fail exclusions are run-scoped and would otherwise wrongly suppress issues whose blockers have since been resolved) — or simply by starting a new session, whose fresh PID names a fresh file (Step 0's GC eventually clears the old one). If the file is missing or unreadable/corrupt, recreate it empty (`status: "active"`, plus this session's `pid`/`pidStart`) and continue — worst case the breaker takes one extra failure to trip.

### Step 1: Preflight — finish in-flight work

**Targeted mode scopes this step to the target.** The checks below protect the run, not the backlog, so they narrow: for check 1, dirt attributable to the **target itself** is this run's resumption — dispatch the same `Skill(skill: "finish", args: "auto <ISSUE-ID>")` as the attributable row; dirt attributable to a **different** issue, or unattributable, refuses with a plain error and no state change (the user asked for the target, not a surprise ship of something else, and the loop-mode `halted` transition exists to stop recurrence a one-shot doesn't have): `Main checkout is dirty on <branch> — if the work belongs to another in-progress issue of mine, run bare /auto to preflight-finish it; otherwise resolve it manually (bare /auto would only halt on it). Then re-run /auto <ISSUE-ID>.` Check 2 reduces to the target's own merge-queue guard: if `<main-checkout>/.claude/merge-queue/<target-id-lowercased>.json` exists, refuse — the issue already shipped and the drainer owns it (`/merge-queue` to inspect). Skip the general orphan sweep (foreign worktrees are not this invocation's business — note them in the summary sentence at most); a leftover worktree for the target itself needs no special handling here, because Step 3's `/full` dispatch already resumes a dead owner's worktree idempotently and refuses a live one (`SKIPPED-BLOCKED`).

Two checks, in order:

1. **Dirty main checkout.** Run `git status --short` and `git branch --show-current`. If dirty, first rule out a transient sibling operation: a parallel session's `/finish merge` (or locked worktree create) briefly mutates the main checkout under the repo lock, and a status read that races it looks like orphaned WIP. Re-run the status **under that same lock** — `~/.claude/scripts/with-repo-lock.py "$(git rev-parse --path-format=absolute --git-common-dir)" git status --porcelain` — which simply waits out any in-flight critical section; if the locked re-check comes back clean, there is no dirt (proceed to check 2). Still dirty → attribute the work by **branch name only** — the branch must itself contain an issue ID (`[a-z]+-[0-9]+` segment). Do NOT use `detect-

…(truncated)
