surreptakos

ticket-fleet

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.

surreptakos 0 Updated 2w ago

Resources

7
GitHub

Install

npx skillscat add surreptakos/claude-dotfiles/ticket-fleet

Install via the SkillsCat registry.

SKILL.md

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 the
    session-env-carries-zoho-and-gas-tokens memory 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

  1. Pick the scriptPath the current working directory can reach:

    • In a claude-dotfiles checkout: 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 (or name: '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.js

      Each wave refreshes that copy itself (INTERNALS.md, "A copied script
      refreshes itself"). The tree guard runs without the copy too: Setup probes the repo's
      tools/, .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 with Cannot create agent worktree: not in a git repository and a cd does not help; env-probe stops
    the run before the scout with that cause. Start a new session on the repo (issues 892, 1093).

    Then run git fetch origin in the launching checkout. Implementer worktrees start from its
    local origin/<defaultBranch>, which a merge done through gh never 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; a git rev-parse after any cd, or the "Primary working directory"
    line, follows the shell instead and passed on a /home/user root that env-probe then aborted
    (osh-rfp, 2026-10-02).

  2. Mint the ids. runId names the run and stays the same across a resume
    (printf %x $(date +%s)); invocationId is fresh on every launch, resume included, and must
    differ from runId (printf %x%x $(date +%s) $$). The runtime forbids Date.now(), so the
    caller mints both. Done when both are hex tokens and they differ.

    Then start the shell runner, fleet-shell.js beside 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: --detach starts 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 same treeGuard, treeGuardScript or treeGuardStateDir you pass the wave as
    --tree-guard, --tree-guard-script or --tree-guard-state-dir. On a resume, start it again
    with the same runId: 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.

  3. Set the args (full list below). Always contractVersion: 2, runId, invocationId. Add:

    • instrument: 'mcp' from a cloud session. The gh instrument is desktop-only; a run that
      cannot measure its environment stops rather than guess (issue 322).

    • deliver: false on a repo's first wave, to read the scout, lane and verifier output before
      the fleet pushes anything.

    • testCommand when the repo's documented gate cannot run where the wave runs.

    • In claude-dotfiles, the stamp tooling on every launch. regenCheckCommands defaults to
      [] (issue 814), so without it a Deliver stage that regenerates a skill's stamp pushes it
      unchecked. regenCommands defaults to null, and then the implementer and verifier pick a
      --home themselves (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 commits
      marketplace/, 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 newest
agent/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.

  1. 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 run node tools/fleet-run-record.js --latest in
    the served repo from this session, not a subagent: the journal dies with the container. Where
    the repo gitignores state/, post the record's digest on the repo's tracking issue. Done when
    state/fleet-runs/<runId>.json exists, plus the posted digest where state/ is ignored.
    The record's stages and 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 no tools/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 writes state/fleet-runs/<runId>.json under 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.

  2. Account for every ticket. The run log has one line per ticket, MERGED <sha> or
    open, not merged: <prState>, and the result sorts every candidate into one row below:

    Result key Meaning Next
    delivered PR or comment posted; a mergeNote means the PR still owes a merge of the default branch merge the default branch into it, then the PR
    failed no verified pass, or delivery blocked; conflictPaths names a real merge conflict read failures, re-run the ticket
    inconsistent verified and pushed, but the deliverer could not find the branch, or no deliverer started (recovery names the step); or an orchestrator-head / tree-guard entry for an isolation breach the run restored, or a move or write a halted run could NOT restore deliver it by hand or via finishRunId; an entry saying NOT restored: restore the checkout by hand (RECOVERY.md)
    skippedBlocked an open blocker outside the wave waits for the blocker
    skippedChained chained behind an in-wave blocker that did not merge next wave
    notAttempted never started: halt names 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 and resetsAt: null (the run log's [label] failed: line names the limit) relaunch after the reset; bullets are in discoveryList
    skippedOpenPR / skippedParked / skippedPrd / skippedAwaitingOwner already has a PR / in Maybe Someday / a prd container / waiting on the owner after a handoff nothing

    Done when every ticket the scout listed sits in one row above.

  3. Land the session. When the loop launches no further wave (the ready-for-agent and
    ready-for-local-agent queues are empty, or only tickets held by a ruling, a date, another
    machine or an open blocker remain), run /aac-skills:session-end without 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 carrying
    label (default ready-for-agent).
  • deliver (default true): false stops 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 as no-result, the ticket moves on (a hung implementer is a failed
    attempt) and the wave does not wait; the hung agent is not stopped. 0 or false turns it off
    (issue 1486).
  • instrument (auto | gh | mcp) and remote (boolean): the tracker route. auto is
    measured by the first agent; remote is 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}) and difficulty (default true) pick the
    implementer model from a TypeSafe Jev difficulty Score per ticket; retries take the design pin.
    A score under 0.8 confidence is ignored and the ticket runs on implModel (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 pins fleet-verifier only 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 as fleet-oneshot (Bash only) on a desktop that has the file,
    unpinned elsewhere (issue 1196). '' forces unpinned.
  • followupsFile (default FOLLOW-UPS.md): where discoveries are appended.
  • generatedPaths, regenCommands, regenCheckCommands: what the pre-push merge may resolve by
    regeneration, and how. Defaults and claude-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, false turns 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
    closed clears 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, from origin/<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's sharesWorkWith says 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 labelled prd (a decomposed container:
    its children carry the work, and it closes when they do; issue 1362). A prd issue named in
    tickets: [...] still runs.
  • A ticket whose latest comment is an unanswered fleet handoff is parked.
  • A wave serves the default branch only. The scout reads defaultBranch from origin/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 of tickets and 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: merge origin/<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 to ready-for-local-agent, or ready-for-human for 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.