Run a ticket-fleet wave over the ready-for-agent queue. Use when asked to run the fleet or clear the queue, or when an orchestrator worker cycle launches it.
Resources
7Install
npx skillscat add surreptakos/claude-dotfiles/ticket-fleet Install via the SkillsCat registry.
ticket-fleet
One Workflow script, ticket-fleet.js beside this file, runs a wave: a scout picks the
tickets, each ticket gets an implementer in an isolated worktree, a blind verifier that tries to
refute it, and a deliverer that opens the PR and merges it once CI is green. One report writer
files the wave's discoveries. The orchestrating session only launches the wave and reads its
result; every ticket is worked by subagents.
One fleet at a time (Dan, 2026-09-26). Wait for a running wave's result before launching
another in the same repo.
Hold a command typed mid-wave until the wave returns (issue 1360). The harness relays this
session's latest chat message to every agent of a running wave as the only user voice. Every fleet
prompt tells its agent that a slash command or loop instruction in it is this session's, not its
own, but that rail is a backstop. When Dan types a command such as /aac-skills:session-end while
a wave runs, tell him it is held and run it once the Workflow result is back. In osh-rfp run
6ac097ff a one-command agent ran session-end off the relay, deleted the orchestrator branch and
closed six issues.
A desktop session runs the ready-for-local-agent tickets itself (Dan, 2026-09-30). The wave
serves one repo and skips a ticket labelled for a local agent, or one that changes another repo.
- When this session is on the desktop that ticket names, it is that local agent: start each such
ticket as a background agent in the right clone, in parallel with the wave. A ticket the fleet
skips is still this session's work: run 6abd47d1 dropped #1069 while its dependant #1072 waited
on it. - A local agent inherits this session's environment, API keys and tokens included: read
env | grep -i -E 'key|token|secret'before writing its brief, and state credentials in it only
as that check found them (Dan, 2026-09-30; the names are in thesession-env-carries-zoho-and-gas-tokensmemory note). - What no variable supplies is a claude.ai web sign-in: a page that runs
window.claude.*calls
needs a browser signed in to the owning account.
Launch a wave
Pick the
scriptPaththe current working directory can reach:In a
claude-dotfilescheckout:aac-skills/ticket-fleet/ticket-fleet.js, launched in
place (a test forbids a.claude/workflows/copy here, issue 299).On the desktop, from any cwd: the plugin cache copy,
~/.claude/plugins/cache/claude-dotfiles/aac-skills/<version>/skills/ticket-fleet/ticket-fleet.js
(${CLAUDE_PLUGIN_ROOT}is that<version>directory); the marketplace auto-updates at
session start, so the newest<version>is the one to launch.In any other repo: copy the script and its guard into the cwd once, then launch
.claude/workflows/ticket-fleet.js(orname: 'ticket-fleet'):mkdir -p .claude/workflows cp "${CLAUDE_PLUGIN_ROOT}/skills/ticket-fleet/ticket-fleet.js" .claude/workflows/ticket-fleet.js cp "${CLAUDE_PLUGIN_ROOT}/skills/ticket-fleet/editable-install-guard.js" .claude/workflows/editable-install-guard.js cp "${CLAUDE_PLUGIN_ROOT}/skills/ticket-fleet/fleet-shell.js" .claude/workflows/fleet-shell.js cp "${CLAUDE_PLUGIN_ROOT}/skills/ticket-fleet/orchestrator-tree-guard.js" .claude/workflows/orchestrator-tree-guard.jsEach wave refreshes that copy itself (INTERNALS.md, "A copied script
refreshes itself"). The tree guard runs without the copy too: Setup probes the repo'stools/,.claude/workflows/, then the plugin cache, and logs the copy it picked
(issue 1284).
In a cloud session the session root must be the repo checkout. A resumed session can come
back rooted at/home/user, beside the clones, where every worktree agent fails withCannot create agent worktree: not in a git repositoryand acddoes not help;env-probestops
the run before the scout with that cause. Start a new session on the repo (issues 892, 1093).Then run
git fetch originin the launching checkout. Implementer worktrees start from its
localorigin/<defaultBranch>, which a merge done throughghnever moves: run 6abd47d1's
#1068 branched from master as of 11:59 on 2026-09-30, missed a 12:33 merge, and its delivery
stopped on a conflict that the fetch would have avoided.Done when the path resolves to a file whose bytes are LF only - the Workflow tool refuses a
script holding a CR (issue 233) - the fetch has run, and, in a cloud session,basename "$(dirname "$(ls -t ~/.claude/projects/*/*.jsonl | head -1)")"prints the repo path
with/as-(-home-user-<repo>, not-home-user). The transcript folder names the root
worktrees are cut from; agit rev-parseafter anycd, or the "Primary working directory"
line, follows the shell instead and passed on a/home/userroot that env-probe then aborted
(osh-rfp, 2026-10-02).Mint the ids.
runIdnames the run and stays the same across a resume
(printf %x $(date +%s));invocationIdis fresh on every launch, resume included, and must
differ fromrunId(printf %x%x $(date +%s) $$). The runtime forbidsDate.now(), so the
caller mints both. Done when both are hex tokens and they differ.Then start the shell runner,
fleet-shell.jsbeside the script you picked, from the same
directory you launch the wave from, detached (issues 1190, 1478):node <script dir>/fleet-shell.js serve --detach --run-id <runId>Run it as a plain foreground command, not a background task:
--detachstarts the runner as a
process of its own and returns once its first pass is written. A background task is stopped at
the harness's 2-hour limit, and a wave can run longer (run 6ac343a2 ran 3 h 20 min and lost its
runner at the 2-hour mark, so every later read fell back to an agent). The runner's output is in.claude/fleet-shell/<runId>.js.log.A workflow script has no shell, so without it Setup, every
tip:read and every isolation
checkpoint start a one-command agent each (17 of run 6abddb76's 43 agents). The runner runs
those commands itself and writes.claude/fleet-shell/<runId>.js, which the wave reads with no
agent. Pass the sametreeGuard,treeGuardScriptortreeGuardStateDiryou pass the wave as--tree-guard,--tree-guard-scriptor--tree-guard-state-dir. On a resume, start it again
with the samerunId: it keeps the baseline it took. Done when it has printed{"ok":true,...,"detached":true}; where it cannot start, launch anyway and the wave falls back
to the agents.Set the args (full list below). Always
contractVersion: 2,runId,invocationId. Add:instrument: 'mcp'from a cloud session. Theghinstrument is desktop-only; a run that
cannot measure its environment stops rather than guess (issue 322).deliver: falseon a repo's first wave, to read the scout, lane and verifier output before
the fleet pushes anything.testCommandwhen the repo's documented gate cannot run where the wave runs.In
claude-dotfiles, the stamp tooling on every launch.regenCheckCommandsdefaults to[](issue 814), so without it a Deliver stage that regenerates a skill's stamp pushes it
unchecked.regenCommandsdefaults tonull, and then the implementer and verifier pick a--homethemselves (PR 1169 went red that way, issue 1195); set, the implementer, verifier
and Deliver prompts carry it verbatim. It names no payload build: a branch never commitsmarketplace/, and master builds it after the merge (issue 1308):regenCheckCommands: ["python3 tools/skill-stamps.py check aac-skills --home 'C:\\Users\\Dan'"]regenCommands: ["python3 tools/skill-stamps.py stamp aac-skills --home 'C:\\Users\\Dan'"]From a Linux container add this
testCommand(the PowerShell restore test has no shell there):testCommand: "node --test tools/*.test.js tests/*.test.js && python3 tools/skill-stamps.test.py && python3 tests/build-cloud-plugin.test.py && python3 tools/skill-stamps.py check aac-skills --home 'C:\\Users\\Dan'"
Workflow({ scriptPath: '<from step 1>', args: { contractVersion: 2, runId: '<hex>', invocationId: '<fresh hex>', tickets: [], deliver: true } })Done when the args carry the three required keys and every bullet above that fits this
session.
While it runs, report only what a live check shows. A launch is not evidence the wave is alive.
Before saying a wave is running, check its process (or Workflow task) is alive and name the newestagent/issue-*-wf_<runId>-* branch push time (git for-each-ref --sort=-committerdate). On
2026-10-02 a session told Dan a headless wave was "still running" three times on the strength of
its launch; Dan asked whether that was true before anyone had looked.
Record the run. When the workflow returns, stop the runner (
node <script dir>/fleet-shell.js stop --run-id <runId>; detached, it outlives this session until stopped or 12 hours), then runnode tools/fleet-run-record.js --latestin
the served repo from this session, not a subagent: the journal dies with the container. Where
the repo gitignoresstate/, post the record's digest on the repo's tracking issue. Done whenstate/fleet-runs/<runId>.jsonexists, plus the posted digest wherestate/is ignored.
The record'sstagesand the digest's "Agents per stage" table give the agent count and boot
tokens per stage, so quote them when you report the wave's cost (issue 1196).
Where the served repo has notools/fleet-run-record.js(it lacks the fleet's helper scripts,
as aac-legal and aac-sales-commissions did in runs 6ac03f2a and 6ac03f6d), run a claude-dotfiles
checkout's copy from the served repo's root:node <claude-dotfiles>/tools/fleet-run-record.js <journal path> --run-id <runId>. It writesstate/fleet-runs/<runId>.jsonunder the directory
you run it from, so the step's Done line still applies. Where no claude-dotfiles checkout is on
the machine, write no record by hand: post the workflow's returned result (each step 5 key with
its tickets,discoveryList,followupsError) and the journal path as a comment on the repo's
tracking issue, or in your reply where it has none, and say the record tool is absent. Done
when that comment or reply exists.Account for every ticket. The run log has one line per ticket,
MERGED <sha>oropen, not merged: <prState>, and the result sorts every candidate into one row below:Result key Meaning Next deliveredPR or comment posted; a mergeNotemeans the PR still owes a merge of the default branchmerge the default branch into it, then the PR failedno verified pass, or delivery blocked; conflictPathsnames a real merge conflictread failures, re-run the ticketinconsistentverified and pushed, but the deliverer could not find the branch, or no deliverer started ( recoverynames the step); or anorchestrator-head/tree-guardentry for an isolation breach the run restored, or a move or write a halted run could NOT restoredeliver it by hand or via finishRunId; an entry saying NOT restored: restore the checkout by hand (RECOVERY.md)skippedBlockedan open blocker outside the wave waits for the blocker skippedChainedchained behind an in-wave blocker that did not merge next wave notAttemptednever started: haltnames the quota or rate limit that ended the run and its reset time; a halt on an agent that resolved null carries a prose reason andresetsAt: null(the run log's[label] failed:line names the limit)relaunch after the reset; bullets are in discoveryListskippedOpenPR/skippedParked/skippedPrd/skippedAwaitingOwneralready has a PR / in Maybe Someday / a prdcontainer / waiting on the owner after a handoffnothing Done when every ticket the scout listed sits in one row above.
Land the session. When the loop launches no further wave (the
ready-for-agentandready-for-local-agentqueues are empty, or only tickets held by a ruling, a date, another
machine or an open blocker remain), run/aac-skills:session-endwithout being asked (Dan,
2026-10-05). It is part of every orchestration run, never a line Dan has to put in the prompt:
commit and push the run record, file what the run surfaced through/to-tickets(no approval
round, see that skill's step 4), clear the end check, and end on the archive line.
Args
Required: contractVersion (integer, must equal the script's, 2 today), runId, invocationId.
tickets(issue numbers): exactly those, any label or state. Empty: open tickets carryinglabel(defaultready-for-agent).deliver(default true):falsestops after verify - no push, no PR, no comment.maxAttempts(default 3): implement-verify rounds per ticket, fresh context each.stageCeilingMinutes(default 120): the wall-clock minutes any one agent stage may run. Past
it the run records that label asno-result, the ticket moves on (a hung implementer is a failed
attempt) and the wave does not wait; the hung agent is not stopped.0orfalseturns it off
(issue 1486).instrument(auto|gh|mcp) andremote(boolean): the tracker route.autois
measured by the first agent;remoteis read only when that measurement returns nothing.testCommand: replaces the gate the scout read, for implementer and verifier alike.scoutModel/implModel/verifyModel/deliverModel/reportModel: per-stage pins.implPins({mechanical, multi-file, design}) anddifficulty(default true) pick the
implementer model from a TypeSafe Jev difficulty Score per ticket; retries take thedesignpin.
A score under 0.8 confidence is ignored and the ticket runs onimplModel(Opus 5.5); the two
lighter pins default to Sonnet 5.5. Haiku never implements: any pin or arg naming it becomes
Opus 5.5 (Dan, 2026-09-28).effort(default'high'): the effort every agent() call runs at, all stages alike (Dan,
2026-09-28).verifierAgent: leave unset. The run pinsfleet-verifieronly on a desktop that has the file;
a cloud session cannot load custom agent types (issue 339).''forces unpinned.oneShotAgent: leave unset. The one-command stages after the env probe (tip reads, checkpoints,
restores, the editable guard) run asfleet-oneshot(Bash only) on a desktop that has the file,
unpinned elsewhere (issue 1196).''forces unpinned.followupsFile(defaultFOLLOW-UPS.md): where discoveries are appended.generatedPaths,regenCommands,regenCheckCommands: what the pre-push merge may resolve by
regeneration, and how. Defaults andclaude-dotfiles' values: step 3.priorImpl/priorProbe,finishRunId: recovery - see RECOVERY.md.treeGuard,treeGuardScript,treeGuardStateDir,orchestratorCwd,editableGuard,editableGuardScript: isolation guards, on by default. The tree guard runs the repo's copy or
the plugin's, and from a linked worktree it also names any write into the main checkout
(never restored; issue 1284).shellSnapshot: the shell runner's snapshot file (launch step 2); default.claude/fleet-shell/<runId>.js,falseturns the runner off.
What a wave does
Selection. No cap: every runnable candidate enters the wave. A maxTickets arg is logged
and ignored; tickets is the only way to narrow a wave.
- Blocker state is read from the tracker, not believed from the ticket body: only a plain
closedclears an edge. - A ticket whose every open blocker is a code ticket in the same wave joins it, chained on its
blocker's lane: it starts after the blocker's PR merges, fromorigin/<defaultBranch>
(issue 854). - Two code tickets that share new work run in one lane the same way: when one body names the
other by number, or the scout'ssharesWorkWithsays one names the other's deliverable or both
would create the same new artifact. The second starts from the first's merged code instead of
writing its own copy (issue 1413). - Dropped before selection: tickets with an open
agent/issue-<N>-PR, and, in a label-driven
wave, tickets in the Maybe Someday milestone and issues labelledprd(a decomposed container:
its children carry the work, and it closes when they do; issue 1362). Aprdissue named intickets: [...]still runs. - A ticket whose latest comment is an unanswered fleet handoff is parked.
- A wave serves the default branch only. The scout reads
defaultBranchfromorigin/HEAD
and no arg overrides it: implementers branch from it and deliverers merge into it. A ticket whose
code exists only on an unmerged PR's branch (a Design sync awaiting its logic keys) cannot run
in a wave. Keep it out ofticketsand work it on that branch outside the wave. A label-driven
wave still picks it up, so say so in the launch reply (aac-sales-cockpit PR 882, 2026-10-03).
Lanes. The scout gives each ticket a kind:
- code - implementer (pushes its branch on commit), blind refuting verifier per attempt, then
deliver: mergeorigin/<defaultBranch>into the branch, push, open the PR, wait up to 20
minutes for CI, squash-merge on a green head with no changes-requested review. - probe - evidence in a comment, no repository change. Prober gathers, verifier re-runs the
commands, deliverer posts one resolution comment. - human - the container verifies what it can and hands the rest back under a Remaining for
a local session heading, relabelling toready-for-local-agent, orready-for-humanfor a
person's judgment, credential or sign-off. It never claims an owner step was done.
Discovery-triage chores share one lane so two cannot file the same finding.
Delivery stops on a merge conflict outside the four resolvable classes (generated files,SKILL.md stamp blocks, harness upgrade rows, append-append hunks), a failing gate after the
merge, red CI, or a changes-requested review. A git merge the classifier refuses twice still delivers, with the
refusal noted on the PR.
Discoveries land on their own branch and PR, agent/fleet-discoveries-wf_<runId>, never on
the session's branch; the result's discoveryReport names branch, sha and PR.
Go deeper
- A wave died before delivering, or its verifiers died: RECOVERY.md.
- A worker, or this session, hit a refused command: REFUSED-SHAPES.md -
worktree guard, auto-mode classifier, GitHub proxy. - Changing the script, its args or its schema; or why a rule above exists:
INTERNALS.md. A contract change moves every copy in its ripple table in one
commit.