Skip to main content

autoship

Orchestrate a bounded round of automated issue processing: reclaim orphaned in-progress issues, discover eligible `autoship:ready` issues, and invoke `/ship` sequentially for each — stopping at cost or count caps and surfacing blocked items without halting the round. Requires `--max-issues` and `--max-cost-usd`. Use when you want a self-contained automated delivery round driven from the issue tracker.

Source facts

Repository
bdfinst/agentic-dev-team
Last source activity
August 27, 2026 at 16:48
Detected SKILL.md language
English
Stars
299
Forks
36

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
autoship
description
Orchestrate a bounded round of automated issue processing: reclaim orphaned in-progress issues, discover eligible `autoship:ready` issues, and invoke `/ship` sequentially for each — stopping at cost or count caps and surfacing blocked items without halting the round. Requires `--max-issues` and `--max-cost-usd`. Use when you want a self-contained automated delivery round driven from the issue tracker.
argument-hint
--max-issues N --max-cost-usd N [--dry-run] [--label LABEL] [--max-batch-size N]
user-invocable
true
effort
medium
allowed-tools
Read, Write, Glob, Grep, Task, Bash(python3 *), Bash(gh *), Bash(command -v gh), Skill(ship *), Skill(cost-report *), mcp__github__search_issues, mcp__github__issue_read, mcp__github__search_pull_requests, mcp__github__issue_write, mcp__github__add_issue_comment
# Autoship Role: orchestrator. This skill runs one bounded round of automated issue dispatch. It does not implement code, review, or merge — it sequences the existing `/ship` pipeline per dispatch unit and logs each outcome. You have been invoked with the `/autoship` command. ## Orchestrator constraints 1. **Never start without both caps.** Refuse immediately if `--max-issues` or `--max-cost-usd` is missing from `$ARGUMENTS`. 2. **Sequential only.** Process one issue at a time. Do not launch concurrent `/ship` invocations. 3. **Delegate every phase.** Call the owning scripts and skills; do not re-implement discovery, reclaim, shipping, or cost reading here. 4. **No scheduling logic.** This skill runs once per invocation. Timer or recurring execution is the caller's responsibility. 5. **Dry-run is preview only.** When `--dry-run` is given, run reclaim and discovery in preview mode; never label, comment, invoke `/ship`, or write to the round log. ## Parse Arguments Arguments: $ARGUMENTS Required: - `--max-issues N` — maximum number of issues to process this round (positive integer). - `--max-cost-usd N` — budget ceiling in USD for the entire round (positive number). Optional: - `--dry-run` — preview mode: report what would run without side effects. - `--label LABEL` — override the eligibility label (default: `autoship:ready`). - `--max-batch-size N` — override `autoship_group.py`'s per-batch member cap (default: 5, matching the script's own default). If either required argument is absent, print this message and stop: ``` autoship: --max-issues and --max-cost-usd are both required. Usage: /autoship --max-issues N --max-cost-usd N [--dry-run] [--label LABEL] [--max-batch-size N] ``` **Cross-validate `--max-batch-size` against `--max-issues` (#2073).** A batch larger than `--max-issues` can never be admitted into the queue — `Step 2`'s `autoship_queue.py --max-issues <N>` defers a whole unit rather than splitting it, so a batch sized above the round's own cap is permanently undispatchable and every round that produces one repeats the same `no_unit_fits_cap` early exit until an operator notices and reruns with compatible caps. If `--max-batch-size` is given and its value is greater than `--max-issues`, print this message and stop, without proceeding to Step 1: ``` autoship: --max-batch-size <B> cannot exceed --max-issues <N>. Usage: /autoship --max-issues N --max-cost-usd N [--dry-run] [--label LABEL] [--max-batch-size N] ``` ## gh CLI availability (#1700) Check once, before Step 1: `command -v gh`. - **gh present** (the normal case — a local/CI session with the CLI installed and authenticated): every step below runs exactly as written, invoking `gh` directly (via `autoship_reclaim.py`/`autoship_group.py`'s live fetch, and the raw `gh issue edit`/`gh issue comment` calls in Steps 3b/3d). - **gh absent** (a Claude Code web/cloud session — GitHub access there is provided only through the `mcp__github__*` tools, never a `gh` binary): every step below that would otherwise shell out to `gh` instead uses the MCP-tool path called out in that step. The scripts themselves never gain a network code path of their own — they stay pure decision logic over `--input-file` JSON (already true for `autoship_discover.py`'s read side and, since #1700, `autoship_reclaim.py`'s write side too via `--emit-actions-only`); only the data gathering and the actual GitHub mutation move up to this skill, because MCP tools are only callable from the agent context, never from inside a Python subprocess. - **Known gap, gh-absent discovery only**: `autoship_group.py` (via the shared `autoship_state.fetch_eligible_issues` eligibility filter it calls into) needs two GraphQL-shaped fields (`subIssuesSummary`, `closedByPullRequestsReferences`) that `gh issue list --json` computes for free but that the REST-backed MCP tools don't return in one call. The MCP-path instructions in Step 2 approximate them — `mcp__github__issue_read` (method `get`) per candidate for the epic check, and a `mcp__github__search_pull_requests` query for the open-linked-PR check — and are deliberately conservative (treat an ambiguous match as "has an open PR", i.e. skip it) since a false include is worse than a false exclude for an autoship gate. This is real but bounded: it only touches the small number of issues that already carry the ready label, not the whole repo. Step 2 also documents a second, narrower gh-absent gap specific to grouping itself — the `blockedBy`/`blocking`/`parent` fields `autoship_group.py`'s dependency and shared-parent signals need, which this skill's MCP toolset has no call for at all. - `/ship` (Step 3c) has its own separate `gh` dependency (`Bash(gh pr *)`, `Bash(gh issue *)` in its own `allowed-tools`) that this fix does not touch — a gh-absent round can reclaim/discover/label via MCP, but `/ship` itself still needs `gh` to open the PR. Out of scope for #1700; file a follow-up if full gh-less autoship end-to-end is wanted. ## Step 1 — Reclaim orphaned issues Run the reclaim script to relabel any stale `autoship:in-progress` issues back to `autoship:blocked` before discovery. This does not change `--max-issues` accounting — `autoship_state.is_eligible` already excludes any issue carrying `autoship:in-progress` or `autoship:blocked` regardless of whether reclaim has run, so a stale in-progress issue is excluded from the eligible pool either way. Reclaim's real purpose is unsticking issues orphaned by a crashed round and routing them to human triage before they sit invisibly forever. **gh present:** ```bash python3 "${CLAUDE_PLUGIN_ROOT}/scripts/autoship_reclaim.py" \ [--dry-run] # pass when --dry-run was given ``` **gh absent:** 1. Fetch open issues labeled `autoship:in-progress` via `mcp__github__search_issues` (`query: "is:issue is:open label:autoship:in-progress"`, `fields: ["number", "title", "labels", "updated_at"]`). `mcp__github__search_issues` is a paginated search tool with its own default page cap — page through every result page until exhausted, up to 500 issues, matching the gh-present path's `--limit 500` above: without this, a repo with more than one page of stale in-progress issues would silently have this step see only the first page, reclaiming only part of the full eligible pool. 2. Build a JSON array matching `autoship_reclaim.py`'s `--input-file` schema — one object per issue with `number`, `title`, `state: "OPEN"`, `labels` (as `[{"name": "..."}, ...]`), and `labeled_at` (use `updated_at` from the search result — the script's own live-fetch path falls back to `updatedAt` the same way when it can't resolve the real timeline event, so this is not a regression). Write it to a scratch file. 3. Run the script against that file, with `--emit-actions-only` (never `--dry-run` and `--emit-actions-only` together unless `--dry-run` was itself given — dry-run alone already previews correctly with no gh calls): ```bash python3 "${CLAUDE_PLUGIN_ROOT}/scripts/autoship_reclaim.py" \ --input-file <scratch-file> --emit-actions-only \ [--dry-run] # pass when --dry-run was given ``` 4. In `--dry-run` mode, the script's `would-reclaim` preview lines are the report — stop here, nothing to execute. Otherwise, the script prints one JSON action per line (`{"number", "comment", "relabel_remove": ["autoship:in-progress", "autoship:batch-confirmed"], "relabel_add": ["autoship:blocked"]}`, exit 0) or, on a failure it detects itself (e.g. a malformed input file), a `autoship_reclaim: ...` error on stderr with a non-zero exit — treat that the same as today's "reclaim failure is non-fatal" handling below. For each successfully-emitted action, execute it directly: `mcp__github__add_issue_comment` with the action's `comment`, then `mcp__github__issue_write` (method `update`, removing every label in `relabel_remove` and adding every label in `relabel_add` via the `labels` field — read the issue's current labels first, since `issue_write`'s `labels` replaces the full set rather than diffing it). Report how many issues were reclaimed (or would be reclaimed in dry-run). A reclaim failure is non-fatal — log the error and continue to discovery. ## Step 2 — Discover eligible issues Run the grouping/queueing pipeline to select and order the issues this round will process. This is now **two separate commands with a scratch file in between, not a single shell pipe** — Step 2b (agent-proposed grouping) and Step 2c (block-and-comment) run between them, against that scratch file's `ungrouped` array, before it ever reaches the second command. **gh present:** ```bash python3 "${CLAUDE_PLUGIN_ROOT}/scripts/autoship_group.py" \ [--label "<label>"] [--max-batch-size "<max_batch_size>"] \ > <scratch-grouping.json> ``` **Resolving `confirmed_batch_members` (Step 4.3).** `has_batch_confirmed_override` (`autoship_group.py`) needs, on every eligible candidate carrying `autoship:batch-confirmed`, a `confirmed_batch_members` field — the most recent `<!-- autoship-batch-members: ... -->` marker from that issue's own comments, parsed into an int list — added to its JSON before grouping runs. Resolve it now: run `gh issue view <n> --json comments` per candidate carrying `autoship:batch-confirmed`, and extract the marker from the returned comment bodies (most recent match wins). **Author and value validation (security).** Issue comments are attacker-influenceable on a public repo, so the marker must not be trusted from just any commenter. Resolve the invoking identity concretely: run `gh api user --jq .login` once per round to get the currently-authenticated login. Then run the deterministic transform (#2072 — extracted so a future round can't silently drift from the documented rule) per candidate carrying `autoship:batch-confirmed`: ```bash python3 "${CLAUDE_PLUGIN_ROOT}/scripts/autoship_proposals.py" parse-marker \ --comments-file <scratch-comments.json> \ --invoking-login "<login>" ``` It implements: only extract it from a comment posted by this skill's own actor — e.g. filter `comments[].author.login` against the invoking bot/user identity — before treating it as authoritative (if the `gh api user` call above failed or returned nothing, the identity cannot be resolved — fail closed: pass an empty/unmatchable `--invoking-login` so the marker reads as absent, i.e. no `confirmed_batch_members` for that candidate, same as the "value fails" outcome below). Then, once extracted, validate every parsed value matches `^[0-9]+$` before merging it into `confirmed_batch_members` — if any value fails, drop the whole marker (treat it as absent, i.e. no `confirmed_batch_members` for that candidate) rather than merging a partially-valid list. Its stdout is `{"confirmed_batch_members": [...] | null}` — `null` is the "absent" outcome above. The plain self-fetch invocation above has no seam to receive this enrichment, so whenever at least one eligible candidate carries `autoship:batch-confirmed` this round, replace it with an explicit `--input-file` built from `gh issue list --state open --label "<label>" --limit 500 --json number,title,state,createdAt,labels,closedByPullRequestsReferences,subIssuesSummary,blockedBy,blocking,parent` (the same fields the self-fetch would request, plus `--limit 500` — `gh issue list` applies a default result cap, and without an explicit override this command silently fails to fetch the full eligible pool it claims to) with `confirmed_batch_members` merged onto the enriched subset: ```bash python3 "${CLAUDE_PLUGIN_ROOT}/scripts/autoship_group.py" \ --input-file <enriched-scratch-file> \ [--label "<label>"] [--max-batch-size "<max_batch_size>"] \ > <scratch-grouping.json> ``` When no eligible candidate carries `autoship:batch-confirmed` this round, the plain self-fetch invocation above is used unchanged. Unlike Step 1's reclaim, a discovery failure is fatal for the round — abort before running the second command below, and do not run it against a missing or stale `<scratch-grouping.json>`. The actionable error is the `autoship_group:`-prefixed line on stderr from this FIRST command; if `autoship_queue.py` is run anyway despite that failure, it will report its own unrelated "grouping output is not valid JSON" message, not the real cause. `autoship_group.py` self-fetches the **full** eligible pool — it takes no `--max-issues` truncation at that layer, because grouping needs full visibility across every eligible issue to find dependency, shared-parent, and shared-label signals before anything is capped. It groups that pool into batches and ungrouped singles via those deterministic signals. **gh absent** — `autoship_group.py` already supports `--input-file` to bypass `gh` entirely on the read side (no script change needed); this skill supplies that file via MCP tools instead, the same way this step's `gh absent` path worked before this pipeline replaced `autoship_discover.py`: 1. Fetch open issues labeled `autoship:ready` (or `--label`) via `mcp__github__search_issues` (`query: "is:issue is:open label:<label>"`, `fields: ["number", "title", "labels", "created_at"]`). `mcp__github__search_issues` is a paginated search tool with its own default page cap — page through every result page until exhausted, up to 500 issues, matching the gh-present path's `--limit 500` above: without this, a repo with more than one page of eligible issues would silently have this step see only the first page, undermining `autoship_group.py`'s requirement to see the full eligible pool before grouping. 2. For each candidate, resolve the two fields `gh issue list --json` computes for free but the search result doesn't carry (see the "Known gap" note above): - **Epic check**: `mcp__github__issue_read` (method `get`, that issue number) — use its `sub_issues_summary.total` (or `has_children`) as `subIssuesSummary.total`. - **Open-linked-PR check**: `mcp__github__search_pull_requests` (`query: "is:pr is:open <number> in:body repo:<owner>/<repo>"`). Any result found → treat as an open linked PR (conservative: this is an approximation of GitHub's own closing-keyword graph, not an exact match — a false "has an open PR" only costs deferring the issue to next round, which is safe; a false negative would let a genuinely PR-in-flight issue double-dispatch, which is not). 3. **Known gap, gh-absent grouping only**: `autoship_group.py`'s native-dependency and shared-parent signals need `blockedBy`, `blocking`, and `parent` — fields this skill's REST-backed MCP toolset (the `mcp__github__*` tools listed above) has no call for. A gh-absent round cannot resolve them, so leave all three out of the scratch file entirely rather than guessing — `autoship_group.py`'s signal functions already treat a missing field as "no signal", never an error (they read it via `.get(...)`, same as the epic/PR-check gap above). Only the shared-label signal (which needs just the `labels` field already fetched in step 1) still groups issues in this mode; the round still ships every eligible issue, just solo instead of batched wherever a dependency/parent signal would otherwise have fired. 4. **Known gap, gh-absent `confirmed_batch_members` only**: this skill's MCP toolset (`mcp__github__search_issues`, `mcp__github__issue_read`, `mcp__github__search_pull_requests`, `mcp__github__issue_write`, `mcp__github__add_issue_comment`) has no call that returns an issue's comment bodies, so a gh-absent round cannot extract `confirmed_batch_members` from the `<!-- autoship-batch-members: ... -->` marker. Leave the field out entirely — optional, same `.get(...)` convention as the gap above — so `has_batch_confirmed_override` simply never fires in a gh-absent round; a previously-confirmed batch still groups via any shared non-autoship label it happens to carry, or ships solo, until a gh-present round processes it.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub