# Auto Prep

> Prepare a team's certified backlog for a fleet of parallel /loop /auto sessions — audit `specified` labels for unattended-shippability (route human-gated issues by shape — `needs decision` for pending decisions, `human` for human-performed work — mark fleet-hostile ones `solo`, flag decision-gated ones, and re-audit `simple` labels in both directions), consolidate same-defect-family point fixes into class-scoped sweep issues, wire `blocks` edges between file-colliding candidates, validate through next-candidates.sh, audit the release scope (every unstarted Planned/Todo issue classified workable/needs-keeper-action/draining, blocker chains walked to their root causes, fan-out first — the keeper's personal unblock list leads the report) plus the second-order certified-Backlog fleet-drain edges, and recommend a parallel-session count. Use when the user says 'auto-prep', 'fleet prep', 'prep the backlog for auto', or before launching multiple /loop /auto sessions.

- Skill: `alienfast/auto-prep` (Agent Skill)
- Install (CLI): `npx skillmds@latest add alienfast/auto-prep`
- Raw SKILL.md: https://api.skillmd.com/api/skills/alienfast/auto-prep/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-prep

---


# Auto-Prep — Certified-Pool Review Before a Parallel /auto Fleet

`/next` observes exactly one dependency signal — Linear `blocks` relations — and `/auto` ships whatever carries the `specified` label. Before running many `/loop /auto` sessions in parallel, both signals must be honest: every certified issue genuinely unattended-shippable, and every same-file pair serialized so two sessions never collide in the same components. This skill audits and repairs both, then sizes the fleet.

Read [skills/linear/SKILL.md](../linear/SKILL.md) first (gotchas: relations direction, label add/remove helpers, state-update verification). Interactive by design — the flags it raises are decisions for the user; never run it unattended.

After the fleet finishes, [`/fleet-retro`](../fleet-retro/SKILL.md) is the bookend — it measures where the run's capacity went and audits what it filed, which is where this skill's next inputs come from.

**Writes it may make** (all reversible, all reported): add/remove issue labels, add `blocks`/`duplicate` relations, post explanatory comments, adjust priority. It issues **no unconditional state write** — the `duplicate` relations it wires (Step 2's duplicate-filing cleanup, Step 3's family consolidation) land the absorbed issue in the team's duplicate-type state on their own, and the only state writes it may make are Step 3's read-back fallback for when that did not happen and Step 4's user-approved Backlog-blocker promotion (stage-inversion check); it never touches states otherwise, and never assignees.

## Step 1: Resolve scope and fetch the pool

Team scope: a `team:KEY` (or bare key) argument, else `$LINEAR_TEAM`, else error — fleet prep is a deliberate per-team act, never workspace-guessed. Multi-team fleets: run once per team.

One paginated GraphQL fetch for everything (descriptions, labels, parents, relations both directions) across the team's unstarted workable states (Backlog/Planned/Todo — match `/next`'s `WORKABLE_STATES`).

**Page it — `first:` is a hard cap and overflow is silent.** A one-shot query returns its cap and stops: no error, exit 0, `hasNextPage` simply unread, so a pool that has outgrown one page is indistinguishable from a complete one. 250 is the API maximum, so raising the number is not the fix. Measured on BF (2026-08-08): 266 workable-state issues, so a one-shot `first: 250` hid 16 — and the cut lands on whichever issues sort last (the query sets no `orderBy`), not on a random sample, so a whole tail of the backlog goes unaudited in Step 2, unwired in Step 3, and uncounted in Step 5's lane math. **Step 4 will not catch it**: `next-candidates.sh` paginates, so the ranking it prints is complete and reads as confirmation of a pool that was not.

```bash
q='query($team:String!,$after:String){issues(filter:{team:{key:{eq:$team}}, state:{name:{in:["Backlog","Planned","Todo"]}}}, first:250, after:$after){nodes{identifier title description priority labels{nodes{name}} state{name} parent{identifier state{name}} relations{nodes{type relatedIssue{identifier state{name}}}} inverseRelations{nodes{type issue{identifier state{name}}}}} pageInfo{hasNextPage endCursor}}}'
pages=tmp/pool.pages; : >| "$pages"; after=''
while :; do
  if [ -z "$after" ]; then page=$(linear-cli api query -q -o json -v team=<KEY> "$q")
  else page=$(linear-cli api query -q -o json -v team=<KEY> -v after="$after" "$q"); fi
  [ -n "$page" ] || { echo "ERROR: issue fetch failed (auth? network?)" >&2; break; }
  [ "$(printf '%s' "$page" | jq 'has("errors")')" = "true" ] && { printf '%s' "$page" | jq -c '.errors' >&2; break; }
  printf '%s\n' "$(printf '%s' "$page" | jq -c '.data.issues.nodes // []')" >> "$pages"
  has=$(printf '%s' "$page" | jq -r '.data.issues.pageInfo.hasNextPage // false')
  after=$(printf '%s' "$page" | jq -r '.data.issues.pageInfo.endCursor // empty')
  { [ "$has" = "true" ] && [ -n "$after" ]; } || break
done
jq -s 'add | {nodes: .}' "$pages" >| tmp/pool.json
jq '.nodes | length' tmp/pool.json    # state the pool size in the report — a short fetch has no other tell
```

Two traps this shape exists to avoid, both of which silently reproduce the bug:

- **Read `hasNextPage` through the `data` envelope** — `.data.issues.pageInfo.hasNextPage` ([linear gotcha #6](../linear/SKILL.md)). The path copied straight out of the query text, `.issues.pageInfo.hasNextPage`, returns `null` at exit 0, and both `// false` and any shell truthiness test read that as "last page": the loop exits after page 1.
- **Accumulate pages on disk, not in a shell variable.** Threading the growing array through `jq -n --argjson` each iteration hits `ARG_MAX` (~1MB on macOS), and a team's descriptions alone run several hundred KB — `next-candidates.sh` carries this same caveat at `fetch_team_issues`.

This is the paginated idiom `next-candidates.sh`, `fleet-blockers.sh`, and `linear-deps-graph.sh` already run against this connection — copy one of them rather than re-deriving it.

Split into the certified set (`specified` label) and the rest. Read every certified issue's description in full — the audit and the collision analysis both depend on body content, not titles.

## Step 2: Certification honesty audit

`specified` means "an unattended agent may pick this up and ship it" ([standards/issue-spec.md](../../standards/issue-spec.md)). Test every certified issue against that sentence and sort failures into four dispositions — then run item 5's tier audit over every certified issue, failures included:

1. **Mark `needs decision`** — the body itself contradicts unattended shipping. Two tests, both mechanical:
   - The text says so: "must not ship from an unattended run", "needs dedicated review", or equivalent present-tense claims about *this* issue's work. (Past-tense "was deferred from BF-X because too big for that unattended run" does NOT count — being its own issue with its own review cycle is exactly the remedy.)
   - It requires capabilities no agent has: contacting a vendor/support rep, credentials or console access, a product/design decision with **no** testable success criteria (an "## Ask" body with no checkboxes).

   Post a comment stating the specific decision or access needed, then apply the label: `~/.claude/scripts/linear-add-label.sh <ID> 'needs decision'`. The issue keeps `specified` — the spec is gated, not wrong (`standards/issue-spec.md`) — and `next-candidates.sh` hides it from every ranking until a human decides and clears the label (directly, or via `/spec <ID>`).

   **When the capabilities test fires because the work is *itself* human-performed** — outreach, vendor contact, credentials-or-console-gated configuration (a web console only a person can drive is a human step, not a pending decision — the BF-858 miss), production data remediation, sign-offs, a roll-up a person owns — apply `human` instead (`~/.claude/scripts/linear-add-label.sh <ID> human`): that gate is permanent and refuses targeted `/auto <ID>` too, while `needs decision` advertises a pending decision that would return the issue to agents (`standards/issue-spec.md`). A body that *bundles* an agent-shippable slice with human acts takes neither label as the fix — flag it for `/spec` to split at the handoff, exactly the BF-856 shape (a fully-specified schema fused to a confirmation pass the plan itself called human-in-the-loop).
2. **Flag: decision-gated** — the first success criterion is a product/design decision but a conservative implementable path exists (e.g. "apply the same filter the sibling uses"). Keep the label, list it in the report: the user either decides now (best) or accepts that an agent will pick the conservative option and record it. **A decision made now counts only once it is in the description** — route it through `/spec <ID>` so the answer lands in the body and the criterion is rewritten to prescribe it; a decision left in a comment does not unblock `/auto` ([standards/issue-spec.md](../../standards/issue-spec.md)).
3. **Mark `solo`** — implementable unattended but fleet-hostile. The constraint is concurrency, not attendance: worktrees isolate the working tree, so the remedy is sequencing, not a human. Apply the label (`~/.claude/scripts/linear-add-label.sh <ID> solo`; exit 2 → `linear-cli labels create "solo" -t issue`, then retry once) and recommend a targeted `/auto <ID>` (or `/full <ID>`) while the fleet is quiet, first or last, never mid-fleet. The label hides the issue from every ranking so no fleet session can pick it, while targeted runs still ship it normally ([standards/issue-spec.md](../../standards/issue-spec.md)). A report-only flag would not survive the session that read it — this is the one disposition where the old advice ("don't hand-pick these") was addressed to a human who wasn't going to be there.

   **This disposition over-triggers, and a false positive is expensive** — it pulls a workable issue out of the fleet pool and parks it on the one serial resource, the user. Qualify on exactly two grounds, and name which one in the comment:

   - **(a) It changes the rules other in-flight sessions are already playing by.** Merge-driver or `.gitattributes` registration, the `pnpm check` / turbo task graph, a package's `test` script going from stub to real, a shared CI gate, a dependency or lockfile change every other worktree's install predates. The test is whether a session that started *before* this merged would behave differently after it — not whether the file sounds important.
   - **(b) It collides so broadly that serialization does not scale.** A sweep across a directory where much of the certified pool lives. Make this quantitative: if wiring the Step 3 `blocks` edges would take more than a handful, one `solo` mark is the same guarantee for a fraction of the wiring — count the colliding issues and put the count in the comment. Below that threshold, wire the edges instead; that is what Step 3 is for.

   **Not solo, however it sounds:**

   - **Touching a generated artifact.** Regenerating codegen output is not a collision when the repo already resolves it — check `.gitattributes` for a `merge=ours` driver on those paths and CI for an unconditional regen before assuming otherwise. Under both, sibling sessions never conflict there and CI rebuilds the truth anyway. Changing the generation *pipeline or its gate* can still qualify under (a); emitting different *content* through an unchanged pipeline does not. A **serial** artifact the repo does not resolve — a migration journal with chained snapshots, a numbered changelog — is the case neither remedy fits: wiring edges puts every issue that touches it on one chain (BFP, 2026-09-04: nine schema-touching issues across three providers in a single line, the forecast chain-bound from hour 8 with two of three lanes idle), and `solo` takes them out of the fleet entirely. Surface the pipeline fix as a FOCUS-ROOT instead — a project rule that regenerates the artifact at merge plus a consistency check and a drift check in the gate (bfp-control-panel `doc/architecture-decisions.md` D14 is the worked case) — and once it lands drop the artifact-only edges, keeping the pairs that also share ordinary source files.
   - **A class-scoped sweep confined to one file or type.** That is Step 3's serialization case by the same "same file → serialize" rule, and often already wired. Scope, not the word "sweep" or "audit," decides.
   - **An ordinary schema or API change** whose only shared-file contact is the regenerated output above.
4. **Pipeline label repair** — `/reflect` filings ("Auto-filed by /reflect…") certify by provenance: ensure `specified` + `reflection`, plus `keeper` when the proposal edits the shared `~/.claude` repo. Duplicate filings (same proposal from different sessions): keep one canonical, `linear-cli relations add <dup> <canonical> -r duplicate`, cross-comment both.

5. **`simple` audit — both directions.** Test every certified issue against the `simple` semantics ([standards/issue-spec.md](../../standards/issue-spec.md) § The `simple` label: risk-low, no open decision; effort irrelevant below the reviewability ceiling). Add it (`~/.claude/scripts/linear-add-label.sh <ID> simple`) where the body qualifies and the label is absent; **remove** it (`~/.claude/scripts/linear-remove-label.sh <ID> simple`) where the body itself no longer qualifies or a `security`, `human`, `needs decision`, or `epic` label has since attached. The remove test is body-first, like every other disposition here; open code only for issues near the top of the ranking the fleet will actually pick this run, and only as one grep of the named file for a policy, lock, or transaction token that would move the remedy across a contract — never a per-issue code read across the pool. This is the backstop for every producer that labels at filing or grooming time, when the reading was fresh: descriptions go stale in both directions, and a stale `simple` costs one escalated review while a missing one costs a full-tier review on a one-line fix. Interactive like every flag here — list the adds and removes with the reason, and apply what the user confirms.

While reading bodies, also catch cheap ranking wins: a flake fix or check-stabilizer that other sessions' quality gates depend on deserves a priority bump (it sorts within-tier by priority); `bug`/`security` labels missing from issues that plainly are one feed the class rank.

## Step 3: Consolidate families, then wire collision edges

**Before wiring any edge, confirm the team has a completed-type state that `/finish` actually lands in.** `next-candidates.sh` clears a blocker only once its state's
*type* is `completed` (or `canceled`), so an edge whose blocker ships into a `started` state never releases — the dependent stays hidden until a human transitions it
by hand, and an unattended fleet idles the lane instead. Read the team's state types with
`linear-cli statuses list -t <KEY> --no-cache -o json | jq -r '.statuses[] | "\(.name) \(.type)"'` and compare against where `/finish` lands
(`mark-ready-for-release.sh` targets a Ready-For-Release-like state). When no completed-type landing state exists, still wire the edges — they are correct about the
collision — but say so in the report and treat every chain as manual: the lane count stands, the release does not. Measured 2026-08-18 on a team whose landing state
was then `In Review` (type `started`): three wired edges, three hand transitions mid-run, and one session drained ~2.3h before the deadline with work available
behind an edge it could not see.

From the descriptions' named files/components, build overlap groups. For each group, consolidation is the first disposition; serialization is the fallback.

**A named file is not necessarily an edited file — classify each mention before it becomes an edge.** Descriptions cite paths for three reasons that are not collisions: as **precedent** ("the sibling half of this flow already locks — `email_verification.rb:11`"), as **explanation** of blast radius ("`useQuery.tsx` rethrows to the error boundary, so one missing row kills the page"), and as a **pattern to follow** ("following `spec/lib/bullet_spec.rb`'s `around`-block pattern"). The discriminator is which *section* the mention sits in, never how precise it is: Success Criteria / Requirements checkboxes and In Scope entries are what the issue will edit (`/start` Step 6 takes the checkboxes as its requirements, and [standards/issue-spec.md](../../standards/issue-spec.md)'s quality bar bans implementation-planning file lists from specs, with one exception — a spec whose *work* is site enumeration, where the census is the requirement and is written as greppable shapes anchored to a commit — so a path that survives certification inside a criterion is there because it names the work target, either as the edit or as the census itself), while prose in Problem, Notes, or a Boundaries rationale is a citation. **Line numbers prove the author read the code, not that they will change it** — `tenant.rb:72-77` quoted in a Problem to explain a symptom is a citation; a criterion naming a bare `debt_series/create.rb` is an edit. Keep a pair only where **both** sides edit; one-sided contact is not a collision, at most a `related` link when the mechanism is genuinely shared. The cost is asymmetric and silent: a false edge hides a workable issue from every ranking until its supposed blocker ships, and unlike the keeper / `needs decision` / `solo` gates there is no trailing note reporting it — a missed edge costs at most one merge conflict. On one BF run 5 of 27 computed edges were citation-only, including a three-issue N+1 cluster whose members all merely cited `config/application.rb` and `bullet_spec.rb` and so had no collision at all.

**Paths are the weak signal; SUBJECTS are the strong one.** The same certification bar that makes a surviving path trustworthy also makes it rare — `issue-spec.md`'s quality bar bans file lists from specs, so a well-groomed pool names its work target as a *symbol* in prose (a class, policy, operation, mutation, or component) far more often than as a path. Measured on BF (2026-08-08): 35 of 62 workable candidates named no repo path in any edit-bearing section at all, and the run's two real collisions — one pair on `ObligationsTab`, one on `SecureClosingFormContent` — shared no literal path string anywhere in either description. So extract the CamelCase and namespaced identifiers from each issue's title and criteria, resolve each to the file(s) on disk (`find`/`grep`), and build the overlap groups on the *resolved* paths, running the path pass second rather than first. Resolution is what lands two issues naming one subject different ways in a single group; a symbol resolving to nothing means renamed or moved, not gone — re-locate it (`git log -S <symbol>`) before dropping it from the analysis.

**A subject takes the same edit-vs-citation test as a path — and the section rule does not apply it for you.** A pattern-to-follow citation sits perfectly happily inside a Success Criteria bullet, where the section discriminator reads it as an edit: on that same BF run, every path collision the section rule surfaced was a false positive of exactly that shape (two of them the `bullet_spec.rb` `around`-block example the paragraph above already names). So read the clause around each mention, symbol or path, before it becomes an edge — the asymmetric cost is unchanged, and a wider net without that test buys recall by manufacturing false edges.

`~/.claude/scripts/auto-prep-pool.sh --team <KEY> --collisions` runs this extraction — edit-section split, path and symbol extraction, on-disk resolution, overlap grouping — and emits candidate pairs plus the skipped-issue list; treat its output as candidates to confirm with the clause-level read above, never edges to wire blind.

**Family consolidation (one decision per group, interactive like every flag here).** When a group's issues are the same defect *class* with the same fix *shape* — the bodies name one root cause across N sites (N header-scoped policy predicates, N unscoped finds, N copies of a missing guard) — N point-fix issues cost N worktrees, roughly 2N reviewer dispatches, and N serialized merges for what one class-scoped sweep fixes in a fraction. Adversarial review generates exactly this shape when it pulls a defect-family thread: a fleet night can file point fixes *and* its own sweep issues (BF-623, BF-642) for the same families in different sessions. Propose the merge: absorb the point issues into the existing sweep/audit issue when one exists, else promote the most complete point issue to canonical and widen its scope to the class. On approval for a group: append each absorbed issue's Problem + Success Criteria as a checklist block on the canonical (comment via `~/.claude/scripts/linear-post.sh`), wire `linear-cli relations add <absorbed> <canonical> -r duplicate`, and remove `specified` from the absorbed issues — but only once the scope test below passes. **Adding the `duplicate` relation moves the first argument — the absorbed issue — into the team's duplicate-type state on its own**, leaving the canonical untouched (verified on BF: `Backlog` → `Duplicate` from the relation alone, with no state write and no `fromState`/`toState` history entry, so issue history will not show it). Read the state back and issue an explicit update only if it did not land there — the target is the team's duplicate-*type* state, whose name is team-configurable (`Duplicate` in BF and PL) and which a team may not have at all. **Before absorbing into a *certified* canonical, test the canonical's stated scope against the absorbed issue's subject.** The selection test is same fix *shape*, deliberately not same *file* — so the canonical's `### In Scope` and success criteria routinely do not reach the file, class, or predicate the absorbed issue names, and there "the absorbed criteria live on in the canonical" does not hold. `/start` Step 6 does read absorbed criteria out of the comment (it names this protocol by name, after BF-627), but the **description** is what the rest of the pipeline consumes: `/quality-review` sources its "Issue requirements" from `linear-cli issues get <ID>`, which returns the description and no standalone comments, and `/finish` checks off *description* checkboxes — so a criterion living only in a comment is shipped at the planner's discretion, verified by no reviewer, and checked off by nobody, while the absorbed issue sits de-certified in the duplicate state tracking nothing. Two dispositions, and the test picks between them: **scope reaches the subject** → absorb as above; **it does not** → either leave the two separate, wired `related` with a cross-comment and `specified` intact on both, or absorb and **flag the canonical for `/spec`** to widen its Boundaries and criteria into the description before the fleet picks it up — the same interview-grade judgment the uncertified-canonical branch below defers, and the reason not to edit a certified description here. **Never strip `specified` from the absorbed issue on the strength of a comment alone** — do it only once the canonical's description names its subject. Worked case: BF-874 (High, `security`+`bug`, subject `apps/api/app/policies/secure_entity_policy.rb`) was absorbed into BF-849, whose In Scope names only `disbursement_of_funds_policy.rb` and `disbursement_of_funds.rb`'s `participant_emails` and whose six criteria are all written against `DisbursementOfFundsPolicy`; `specified` came off BF-874 eleven seconds after the absorption comment, and BF-866's review re-found the same unguarded predicate and could only file the scope gap back as another comment. A canonical that is not itself certified (a Triage-filed sweep, say) takes the absorbed work out of the fleet pool until it is groomed — flag it for `/spec` in the report rather than certifying it here (widened-scope certification is an interview-grade judgment, not label repair). Declined groups fall through to serialization. The test is same *fix shape*, never same *file*: two different defects in one file are a serialization case below, and merging them would build exactly the fleet-hostile blast radius Step 2's `solo` disposition exists to keep out of fleets — when a proposed merge would cross that line, leave the group unconsolidated.

Then wire **minimal chains** with `linear-cli relations add <BLOCKER> <BLOCKED> -r blocks` (blocker ships first):

- **Same file → serialize.** Adjacent pairs only (A→B, B→C — never a redundant A→C; `/next` requires all blockers terminal, so transitivity is free).
- **Order each group against the edges it already carries, then re-check every pair.** The pool arrives pre-wired — `/quality-review` files up to one dedup-adjacent edge per filed item (same-file sibling, both certified) plus intra-batch chains, and a prior prep adds more (one BF run started with 60) — and Step 1's `relations`/`inverseRelations` already carry them, so no extra fetch. Sort each group by existing reachability first, falling back to `standards/issue-spec.md`'s direction rule (semantic/work order where discernible, else the earlier/already-open issue blocks the newer), or a fresh sort buries a chain head at the tail (a priority-first sort did exactly that to an issue already blocking two others). Where neither existing edges nor semantic order decide, put higher priority and wider fan-out first — a serial group's internal order is the run's tail, and a filing-order chain that led with a Low config issue ahead of Medium pulls cost a full wave in the 2026-09-04 BFP forecast. Then check every pair in the group: adjacent-pairs-only wiring plus an existing edge makes a group *look* serialized while a pair sits mutually unreachable — that pair is a live collision, and nothing downstream reports it, since Step 4 confirms the edges you wired and never the ones you missed.
- **Semantic order → direct the edge**: mechanism before consumers, client display fixes before the server withholds the data they render, code before the docs that describe it, small surgical fixes before the sweep that enumerates the area.
- **Disjoint-overlap diamonds stay parallel**: if A and C are disjoint but both overlap B, wire A→B and C→B and leave A ∥ C.
- **Never chain through a Step 2-flagged issue** — a `needs decision` or decision-gated blocker never ships unattended, so anything wired behind it is stranded. Flagged issues sit at chain *tails* only.
- **A `solo` blocker is a chain head or nothing.** Unlike the flags above it does ship — just outside the fleet window — and blocker resolution reads the blocker's *state*, never its labels, so a dependent behind it stays blocked for the whole fleet run. Wire one as a blocker only when you will run it in the pre-fleet solo pass; otherwise leave it at a tail and let its dependents run free.

Fleet safety rails that already exist and need no wiring: Linear is the claim registry (In Progress is invisible to `/next`), worktree creation is repo-locked, `/finish` merges serialize, and `/quality-review` wires same-batch filing collisions itself (BF-581), plus one dedup-adjacent edge per filed item against an open same-file sibling. This step covers what those can't see: the rest of the overlap across *previously filed* siblings.

## Step 4: Validate through the real ranking

```bash
~/.claude/scripts/next-candidates.sh --team <KEY> --label specified --limit 30
```

Confirm: tier-0 reflection filings lead; every Step 3 dependent is absent (blocked); de-labeled issues are gone; every issue marked `solo` is absent and accounted for by the trailing hidden-count note; `epic`-labeled issues absent with their own hidden-count note (never fleet-workable — the BF-504 shape — and, once the pickable Planned set ships, gate holders the keeper must close); and the `PLANNED-HOLD` note's *need the keeper* entries read as the fleet's stopping point. To reconcile that count against what Step 2 marked, re-list with **`--label solo --include-blocked --limit 50`**.

**Pass `--limit` on every listing you reconcile or work from — the script's default is 3.** `next-candidates.sh` defaults `limit=3`, so a bare `--label 'needs decision' --include-blocked` prints three rows whatever the real count. It is not silent about it — a trailing `_N more workable candidate(s) available; pass --limit to see more._` carries the remainder — but that makes the reconciliation `3 + N` against the hidden-count note rather than the row count against it, and counting rows is the natural reading (a `grep -c` over the listing discards the remainder line outright). A label whose real count is at or under 3 matches by luck and never exposes the difference. Measured on BF: the bare needs-decision listing returned 3 rows plus "15 more" against a 19-issue note — i.e. 18 in workable states, the residual 1 being the state-span effect below. Note that "bare" here, in Step 5's launch checklist, and in the Report's `solo` running order all mean *without `--include-blocked`* — never without `--limit`.

Two independent causes make the numbers disagree, neither a bug. The blocker filter is one: a bare `--label solo` ranking still hides an issue behind an open blocker, which `--include-blocked` restores. The larger one is usually **state span** — the fetch excludes only the `completed` and `canceled` state *types*, and the hidden-count notes run over that unfiltered list, so a labeled issue in any other state the fetch admits is counted as hidden even though it was never workable and the `--label solo` listing correctly omits it. That is not just the `started` states (`In Progress`, `In Review`) — `triage` and `duplicate` survive too, and Step 3's own `duplicate` wiring moves absorbed issues into one of them. The `needs decision`, `human`, and keeper notes span the same states. **Reconcile against the listing, not the note** — and read a note that exceeds the listing as parked, in-flight, or blocked siblings before suspecting a wiring fault. If a blocker already in the team's ship state still reads as unresolved, suspect a state-name mismatch against the script's `TERMINAL_STATES` before suspecting the wiring.

**Release-scope audit + fleet-drain blockers — what the keeper must do for the unstarted stage to drain.** Step 3's wiring rules keep *new* chains from running through flagged issues, but the pool arrives pre-wired. Run the tested implementation — do not re-derive the filters inline (a mistranscribed filter prints nothing at exit 0, which reads as "nothing blocks the fleet"); the `FOCUS:` and `FLEET-BLOCKED:` summary lines are the verdicts — branch on them, never on empty output:

```bash
~/.claude/scripts/fleet-blockers.sh --team <KEY>
```

The output is two sections, and they are not equal. **FOCUS is the report's headline** — the user's stated priority is finishing the unstarted stage (Planned/Todo: the committed release scope, which stage-first ranking already drains first), and this section is their personal unblock list for it: every unstarted issue classified fleet-workable / needs-keeper-action / draining, with each blocked issue's chain walked transitively to its root causes and grouped by fan-out (`FOCUS-ROOT: <root> — <remedy> — unblocks <deps>`), widest first, so the top row is the highest-value keeper hour of the day. Present the rows grouped by remedy, each with what it releases and the released issues' priorities as its worth: decisions to make now (`needs decision` — route through `/spec <ID>`; a bare comment doesn't unblock `/auto`), uncertified scope to `/spec` — **excluding claimed issues**: an assignee other than the viewer is a person's claim (standards/linear-workflow.md — the same rule `next-candidates.sh` enforces at pick time, and `fleet-blockers.sh` now carries in its `claimed by <email>` rows), so a claimed issue is its owner's work whatever its certification state — report it in one line, never as a keeper action, and never recommend `/spec` on it (2026-08-15: the highest-value-hour line recommended four spec interviews on teammates' claimed High issues) — human-performed work to do or re-scope, `solo` roots for the pre-fleet quiet window, `stalled` in-flight roots to resume or release, **parked roots holding banked work** — a `needs decision`/`human` root whose preserved worktree carries verified commits gets its drift quantified (`git rev-list --count <branch>..<source>`) and a split-and-merge recommendation leading its row, since the gate usually covers only part of the work and the verified remainder ages while it waits (BF-858: 21 verified files went 4 → 91 commits behind in three days before the split banked them) — Triage roots to groom (grooming is triage acceptance — never promote around it with a bare state write), and **every Backlog member of a Planned blocking chain**, which is release scope by implication whatever its own labels — an issue is scoped by what it gates, not by the column it sits in or the gate parked on it. Promote the whole membership as one batch (`linear-cli issues update <ID> -s Planned`, one user approval for the set, each write verified per linear skill gotcha #8), and take the set from the FOCUS-ROOT rows rather than re-deriving it — the script's `ancestors()` walk already returns the full ancestry, intermediates included. The promotion is column hygiene, not what makes the fleet reach them: `next-candidates.sh` inherits the Planned stage for every Backlog issue that transitively blocks Planned/Todo work (standards/linear-workflow.md § Stage Priorities), so a chain wired between preps — a review filing, a hand promotion of the dependent alone — still ranks its Backlog members ahead of deferred Backlog work; the batch is what puts them in the keeper's Planned view and the `needs decision` × Planned grooming filter. Gate labels are orthogonal to the column and never hold a member back (keeper-settled 2026-08-13, second ruling): `needs decision`/`human` already hide an issue from every fleet ranking in ANY state, so promotion changes nothing about pickability — while the keeper's own grooming filter is that label × the Planned column, so a gated blocker left in Backlog is buried among the dozens of non-release-scoped issues wearing the same label (BF-553, the 2026-08-13 miss, sat among 17 others; its false justification was "promoting while parked would strand dependents" — the dependents are stranded by the unmade decision, not by the column). Promotion does not unpark a gated member: its FOCUS remedy line — decide, `/spec`, do the human step — still leads the report, and an uncertified member still surfaces as FOCUS-ACTION `/spec` work. The only state never promoted around is Triage, per the grooming-is-acceptance rule above. Both halves of the membership rule matter: an intermediate left in Backlog drops out of the keeper's Planned view the moment its own blocker ships (until 2026-08-28 the ranking also parked the Planned dependent behind a Backlog pick at that point — the inherited stage closes that half; the column still needs the batch), and a hand-rolled re-derivation is how that happens — on the 2026-08-13 BF run the rows named 21 members, a 'roots-only' recomputation promoted 12, and the 9 intermediates had to be found and promoted in a second pass after the keeper intervened. A root co-gated with siblings frees nothing alone (BF-838 freed 0 of its 9 dependents until BF-1044 and BF-844 joined it), one more reason the batch, not the row, is the unit of action. After promoting, re-run the Planned-tier collision pass — the batch just enlarged the set the fleet picks from concurrently. This chain-membership case is the **only** promotion this skill ever suggests: the unstarted stage is a curated release-scoping signal, and bulk-promoting Backlog issues that gate only Backlog work destroys it — the script never emits those rows (68 of the old audit's 71 BF rows were exactly that noise). **FLEET-BLOCKED is second-order** — certified Backlog candidates stranded behind blockers the fleet can never pick; it matters only after the FOCUS actions are exhausted, and is presented briefly and last. If the user declines a remedy, report the dependent as stranded for this run; if inspection shows the edge itself is stale, dropping the edge is the fix, not the promotion.

## Step 5: Fleet size and launch checklist

Count **lanes** from the Step 4 ranking, which already excludes both label gates — and, under the Planned gate (keeper ruling 2026-08-28, `standards/linear-workflow.md` § Stage Priorities), is the **Planned stage alone** until that column drains: Backlog candidates are withheld and never count as lanes while anything Planned remains, and a session beyond the Planned lanes idles at zero usage until a chain releases or the keeper acts. Lanes are the immediately-workable candidates minus the decision-gated flags, grouped by cluster (chain heads count once; independent standalones count individually). Recommend `min(lanes, 3)` parallel sessions; below 3 lanes, recommend the lane count and note when chains will release more work (each blocker's ship unblocks its dependent automatically).

### The cap is 3, fixed — capacity is not a question to ask

**Keeper-settled 2026-08-10: concurrency is capped at 3, and this skill asks the user nothing about capacity.** The account meters a 5h burst window and a weekly window; only the first sizes a fleet, and its answer is already measured:

- **The 5h burst window constrains CONCURRENCY.** Burn per window is `n x 5 x rate` — duration does not appear, so no deadline choice relieves it — and the bracket is settled: at opus/xhigh on this workload, **3 sessions run for any duration without reliably exhausting a 5h window, and 4 do not** (2026-08-06: n=4 cut off 4.9h into a 12h deadline; 2026-08-08: n=3 ran ~11.6h; 2026-08-09: n=3 ran a 12h deadline unthrottled). Saturating the window throttles every session at once — same wall-clock, less shipped, worktrees still held open — so the cap is a ceiling to respect, not a target to creep past.
- **The bracket assumes the fleet is the window's ONLY consumer.** An interactive session the operator runs alongside the fleet draws on the same 5h burst budget, and so does an earlier fleet whose window has not yet rolled off — neither appears in `n x 5 x rate`, and both move the cutoff earlier. The failure is quiet and expensive rather than loud: sessions park *preemptively* at `/auto`'s one-issue reserve, so nothing crashes, no harness limit message is emitted, no stall group is detected, and the parked sessions present with the same stale-ledger signature as death. Measured 2026-08-21: three fleet sessions parked for the last ~3 hours of a 12-hour window with account-wide trailing-5h at 1,441,738 — inside the documented ceiling band — while the fleet's own burn was only 845,734 output tokens over 10.3 session-hours; the remainder came from one concurrent interactive session and an earlier fleet the same day, and three separate readings during the run mistook the parked sessions for dead ones. Say in the report which other consumers are expected, so the recommended count is read against the whole account rather than the fleet alone. **Keeper ruling 2026-08-29: a user-driven extra consumer never reduces the recommended count.** The operator's own session and an earlier fleet's window are disclosed, not sized against — parking at `/auto`'s reserve is the designed response to them, and shrinking `n` for a consumer the operator chose to run forfeits throughput for nothing.
- **The weekly window is NOT a sizing input.** The keeper runs multiple accounts, so the weekly remaining on any one of them constrains nothing: do not ask for `/usage` readings, do not scale duration by weekly remaining, and do not read a `weekly` `limit_kind` in a past retro as a reason to shrink `n` — account rotation is the keeper's lever there, not fleet shape.

A materially different workload voids the bracket — a different backlog's burn rate, a lower-tier model mix — and the signal to re-derive it is a `5-hour` `limit_kind` cutoff at n≤3 in a retro's `quota_stall_groups`. Until one appears, recommend 3 without discussion.

**Record `rate` and `peak_5h_observed` in the persisted sizing block** from the latest retro's `windows` output (`~/.claude/scripts/fleet-metrics.py --json | jq .windows`): use `output_tokens_per_session_hour_at_peak`, never `output_tokens_per_session_hour_all_sessions` — the all-sessions mean pools idle and interactive sessions with fleet ones and lands roughly half the truth (measured 55.9k vs 84.9k on the same BF data). These feed the report's margin display and `/fleet-retro`'s projection audit, not a sizing decision.

State the binding term in the report — lanes or the cap. "7 lanes available, capped to 3 by the 5h burst bracket" says the backlog is not the constraint; a bare lane-bound "2 sessions" names the real lever (certify more work, or say wh

…(truncated)
