Skip to content
Automation
Skill

/batch

Dispatch a night's batch of Todo tickets — one worktree and one pull request each — and leave a morning summary. Run explicitly; never inferred.

From plugin
csw
35 skills
Install
$ npx -y skills add mikestankavich/claude-ship-workflow --skill batch --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/batch

Context preview

The summary Claude sees to decide when to auto-load this skill.

Dispatch a night's batch of Todo tickets — one worktree and one pull request each — and leave a morning summary. Run explicitly; never inferred.

SKILL.md

batch.SKILL.md
name: batch
description: Dispatch a night's batch of Todo tickets — one worktree and one pull request each — and leave a morning summary. Run explicitly; never inferred.
argument-hint: "[max-tickets] [--dry-run]"
disable-model-invocation: true

Dispatch a batch of tickets

**Announce at start:** "Using csw:batch to dispatch tonight's tickets."

Tonight's invocation carried: $ARGUMENTS

Two modifiers, and they compose:

  • **A bare integer** lowers tonight's cap. Never raises it — see Step 2.
  • **`--dry-run`**, also written `dry-run` or `dry run`, runs selection and stops — see

Step 3.

So `/csw:batch 2 --dry-run` shows tonight's plan cut at 2 without dispatching anything. Anything else in $ARGUMENTS is not a modifier: say what you ignored and why, rather than guessing at what it meant.

Before the first run: the prerequisite

This loop's first filter is "drop blocked tickets," and it only works if the tracker knows what blocks what. If `blockedBy` is empty on most tickets while `relatedTo` carries three or four links each, the dependency information exists but is in the wrong field and in prose.

**Backfill blocking relations on the Todo column before relying on this.** Check a sample first, and if `blockedBy` is empty across the board, say so and stop rather than dispatching a batch whose ordering constraints are invisible.

**Prepped tickets dispatch better.** `/csw:prep <ticket>` leaves a spec and its open questions on the ticket, and `csw:work` Step 2 reads them, so a question that would have cost a whole dispatch to discover is already answered when the loop reaches it. It is not required, and this loop **does not skip unprepped** tickets — an optional command that silently became a gate would be a new manual step in front of every ticket in the column.

Step 1: Pull the candidates

Either way, this step ends with `candidates_json` holding a JSON array in the shape Step 2 pipes into `csw-batch-filter`:

[
  {
    "id": "ENG-1075",
    "state": "Todo",
    "priority": 2,
    "labels": ["migration"],
    "blockedBy": [],
    "relatedTo": ["ENG-1080", "ENG-1081"]
  }
]

Which way depends on one key:

csw-config get trackerCommand

**Empty — read the tracker.** Read every Todo ticket from the tracker named by `csw-config get tracker` and shape it into the array above yourself. This is the default, and it is what `tracker: linear` does through MCP.

**Non-empty — run it, and its stdout _is the filter's input_.** Do not reshape it, do not re-read the tracker, do not merge anything into it. Skipping the in-context reshaping across a whole column of tickets is the entire reason the key exists — reshaping the output anyway keeps the failure mode it was added to remove:

candidates_json=$(bash -c "$(csw-config get trackerCommand)")
  • **`bash -c "<string>"` from the repo root, no arguments and no injected environment** — the

same way `validate` and `gates[].run` are already run.

  • **Capture stdout and check the exit status before piping.** `bash -c "$cmd" |

csw-batch-filter` would report the *filter's* status, not the command's, so a failing command that printed a partial array reads as a selection rather than a failure. A non-zero exit is a **failed selection**: quote its stderr verbatim, say plainly that selection failed and no tickets were dispatched, and stop — exactly as Step 2 does for the filter itself. Do not continue to Step 3.

  • **Step 1 does not pre-check the shape.** `csw-batch-filter` already validates it and exits 2

naming the offending field, and Step 2's existing arm turns that into a failed selection. A second shape check here only drifts from the one that matters. Pass the stdout through unread and let the filter speak.

  • **A command that prints nothing has failed, not found nothing.** Empty stdout hits the

filter's `no input on stdin` and exits 2, which is a failed selection. A command with nothing to report must print `[]`.

  • **`tracker` still has to be right.** `trackerCommand` replaces only the fetch;

`csw-batch-filter` keeps reading `tracker` to rank by priority, so someone shelling out to a Linear GraphQL query still sets `tracker: linear` or gets the wrong order silently.

  • **It must be read-only.** A dry run reaches Step 1 before it reaches Step 3, so it runs this

command too — skipping it would leave a dry run with no plan to report. Step 3's "no side effects" promise covers CSW's own actions, not what an arbitrary configured command does.

Step 2: Select

printf '%s' "$candidates_json" | csw-batch-filter

Three filters, none of which is guesswork:

1. **Blocked and not-Todo** tickets are dropped. 2. **Same-surface clusters** keep their highest-priority member. Two tickets on one nav surface do not conflict textually, but a same-surface audit rule means the agent doing one properly lands in the other's copy. 3. **Single-writer labels** admit one ticket per batch. Two migration-adding tickets each write the next number in a global sequence and both regenerate the checksums file; neither PR is wrong, both validated against the base, and one still has to be redone rather than rebased.

A fourth exclusion, read off the ticket rather than computed by the filter:

4. **A decide-shaped ticket with no answer on it is not dispatched.** Where the description asks for a decision — "decide whether", "choose between", "work out what X is for" — and the ticket carries no `**CSW prep**` comment, or carries one whose `## Open questions` is not `_None._`, move it to `skipped` with that as its reason.

It is a candidate for `/csw:prep` or `/csw:work interactive`, not for a dispatch at 3am. Prep already ends by declaring a ticket dispatchable or not; this is the step that reads the verdict, and without it the verdict is computed and thrown away. An unattended dispatch handed a decision to make will make one, and nobody agreed to it — the expensive versi

Read more
Ships withcsw

CSW takes a ticket from your tracker to a merged pull request, then cleans up after itself. Superpowers owns how the work gets done. CSW owns *how it gets shipped and closed out*. This is one person's idiosyncratic workflow, and it is probably not yours.

Get the whole plugin
Stats
3
Stars
0
Forks
Active
Maintenance
Shell
Language
MIT
License
6d ago
Last commit
11mo ago
Created

Repo: mikestankavich/claude-ship-workflow

Other skills on csw.

merge
Skill

merge

Merge the reviewed pull request for the current branch, then hand straight off to cleanup. A sentence of editorial direction alongside the green light is read…

prep
Skill

prep

Spec a ticket before it is dispatched. Brainstorms it, decides everything it can recommend an answer for, asks a present human about the little that survives,…

work
Skill

work

Dispatch a tracker ticket into an isolated worktree and drive it autonomously to an open pull request, then stop for review. Pass `interactive` to brainstorm…