Agent skill
rotate-agent
Quota-triggered failover between agent harness vendors, either direction.
Filed under Outbound email.
From sno-ai/sno-station-skills · 31 skill entries · 264 · pushed 2026-10-08
What it does when it runs
Quota-triggered failover between agent harness vendors, either direction. When asked to arm it, it starts a watch (optionally time-limited) that reads the working vendor's remaining subscription every few minutes; below a threshold, and only while the sender is still alive, it orders a handoff to a receiver from the other vendor, once. Use when a long run must finish even if one vendor's quota runs out. Not a monitor for anything else.
Automated analysis of the skill and the 6 files bundled beside it. A skill’s own description is written to be selected by an agent, so it describes the job and not the dependencies.
- Keys and connectors you must supply
- None found.
- Hosts it reaches
- No third-party host appears in the skill or its bundled files.
- Tool permissions it declares
- No
allowed-toolsin the frontmatter. It does act, so it runs under whatever permissions your session already grants. - Actions present in the files
- shellwrites files
Install it
View source on GitHub ↗git clone --depth 1 --filter=blob:none --sparse https://github.com/sno-ai/sno-station-skills.git /tmp/sno-station-skills git -C /tmp/sno-station-skills sparse-checkout set "skills/S-communication-and-handoff/rotate-agent/skill" mkdir -p ~/.claude/skills/rotate-agent cp -R "/tmp/sno-station-skills/skills/S-communication-and-handoff/rotate-agent/skill/." ~/.claude/skills/rotate-agent/
Picked up without a restart. A project skill of the same name is shadowed by your personal one. For one repository only, swap ~/.claude/skills for .claude/skills. Claude Code docs ↗
The folder is the same in every client that implements the format — 46 of them — so if yours is not above, only the destination changes.
The skill
Source on GitHub ↗Reproduced in full from sno-ai/sno-station-skills/blob/0451ca25ab9c70aac62162becbec9a4e1e936d56/skills/S-communication-and-handoff/rotate-agent/skill/SKILL.md, which is licensed APACHE-2.0 (repository). 1,711 words, 8 headings.
rotate-agent
Needs: tmux, jq, the sno CLI with its Reach runtime (sno reach), both vendor CLIs
(claude and codex, each installed and logged in, since the quota of both is read), plus
sno heartbeat and sno subscription-quota-check from the same install, and the handoff skill (its sno handoff-checkpoint keeps the progress record). A seat is a registered terminal
address (see join-talk). The owner is the person (or agent) who owns the work; the watchman
usually is that owner's session. The sno heartbeat command runs in the background
and its log is read by a reader (a background-process tool and a log-tail tool; the examples below
are Claude Code syntax, so use your harness's equivalents).
Rotation is who steps in when the one working is throttled. This skill makes that automatic
for one long run: a watchman agent arms a heartbeat whose hook reads the working vendor's
quota, and when the remaining share falls to the threshold the watchman tells the working
agent to hand the task over to a fresh agent from the other vendor. The handoff itself is the
handoff skill, unchanged; rotation only decides when and to whom.
Quota is normally read on demand only: sno subscription-quota-check reads it when the person asks
or after a vendor refuses work, and never on a timer of its own. Rotation is the one deliberate
exception. The person's request to rotate is the moment; arming this skill authorizes the
watchman's heartbeat to read the working vendor's quota every few minutes (the arming example
below has no time limit; set --max-hours <N> to bound it) so the handoff can happen before the refusal. Nothing else should poll quota.
Either direction works. --from codex --to claude and --from claude --to codex are the same
procedure with the seats swapped.
The one rule
Rotate while the sender is still alive. A handoff needs the sender to write the brief,
verify the receiver and release. Once a vendor refuses work, none of that is possible. So the
trigger is a threshold on remaining quota, read a few minutes apart, never the refusal itself.
If a tick prints LATE, the window was missed: the receiver must resume from disk state
instead (see below).
Three seats
| seat | who | does |
|---|---|---|
| working agent (A) | the agent executing the task, vendor --from | registers with join-talk before starting; checkpoints after every task; runs handoff when ordered |
| watchman | any agent with background processes and a reader (usually the session of the task's owner) | arms the heartbeat, reads ticks, sends the order |
| receiver (B) | a fresh agent of vendor --to | spawned by A inside handoff; continues to completion |
Working agent: three lines added to its prompt
Put these after the task instruction:
$join-talk
After every completed task, commit and run `sno handoff-checkpoint <record>` (the progress record, kept outside the checkout), then update its Done and Next lists.
When you receive "ROTATE: hand off to <vendor> now", stop editing and run $handoff to a receiver of that vendor at once.
Per-task checkpoints bound what a receiver has to redo. Registration is what lets the watchman reach it.
Watchman: preflight, then arm, read, order, stop
Before arming, prove the rotation can happen at all. rotate-agent-preflight and
rotate-agent-watch both ship in this skill's scripts/; put them on PATH or give their full
paths (the examples below use bare names). Run the preflight in the working agent's checkout:
rotate-agent-preflight --from codex --to claude --cwd <checkout> --sender <A-seat>
It checks, in order, that the tools are installed, the receiver vendor's command exists,
the checkout is a git work tree, the sender's seat is registered, both quotas can be read
and the receiver has headroom, and finally that a receiver of the --to vendor can be
spawned on a tmux seat in that checkout and answers a call; the trial seat is torn down.
Every line is OK <check> or FAIL <check>: <what> -> <what to do>; the last line of a
clean run is READY. Do not arm on a FAIL: give the owner the -> part verbatim (a
vendor's first run in a directory stops at its trust prompt, and that key is the owner's
to press) and rerun the preflight after they act.
Arming is two calls, as sno heartbeat requires (shown in Claude Code syntax: one call starts the
background process, the other tails its log; use your harness's own tools for both). Create a fresh empty log with
mktemp "${TMPDIR:-/tmp}/heartbeat.XXXXXX" and a state path under
${XDG_STATE_HOME:-$HOME/.local/state}/rotate-agent/<label>.state that does not exist yet.
Bash({
run_in_background: true,
command: "sno heartbeat --interval 5 --max-hours 0 --label rotate-agent-<work> --log \"<LOG>\" \
-- rotate-agent-watch --from codex --to claude --threshold-pct 2 --state \"<STATE>\" --cwd \"<checkout>\""
})
Monitor({
command: "tail --pid=<from the arm output> -F -n +1 \"<LOG>\"",
persistent: true,
description: "rotate-agent-<work> ticks"
})
--max-hours 0 matters: a heartbeat stops itself after 24 hours by default, and the quota
trigger would then never fire on a longer run; give a number of hours instead to bound the watch.
Every tick of rotate-agent-watch prints one line. Say what it says, then act on the first word:
| line | meaning | watchman does |
|---|---|---|
HOLD from=… remaining=N% threshold=T% | above threshold | nothing |
HOLD receiver-<verdict> … | threshold crossed but the other vendor is also blocked | nothing; report it — the run will end at refusal and needs resume from what is on disk |
HOLD unreadable … | quota could not be read on this tick | nothing; three in a row is worth reporting |
ROTATE from=X to=Y remaining=N% … | threshold crossed, receiver has headroom, state file written | send the order (below); keep the heartbeat running |
DONE rotated <time> … | a ROTATE already fired | keep waiting for COMPLETE; after COMPLETE, stop the heartbeat if still running |
COMPLETE from=X to=Y seconds=… commits_before=… commits_after=… | the checkout has a new commit after the order | sno heartbeat --stop rotate-agent-<work> |
LATE blocked … | sender already refused | resume from what is on disk (below); stop the heartbeat |
The hook fires ROTATE once: the state file is the lock. Later ticks print DONE until
the checkout has a new commit, then print COMPLETE once. Stop the heartbeat after
COMPLETE, not after ROTATE. A receiver that never commits leaves the heartbeat running
until its --max-hours limit; choose that limit when arming.
To re-arm for a second rotation (the receiver may run out too), use a new label and a new
state path with the seats swapped.
Only the --from vendor is read on ordinary ticks. The --to vendor is read once, at the
moment of crossing, so the rate-limited side is not polled.
The order
Seat on tmux:
sno reach call <A-seat> "ROTATE: hand off to <to-vendor> now. Remaining quota <N>%. Run the handoff skill; receiver kind <to-vendor>, spawned --window (a tmux seat, never an ACP seat: the non-terminal Agent Client Protocol seat that `sno reach spawn` uses without `--window`; see the reach skill); same checkout; report to <owner session>." --expect HANDOFF_RELEASED --timeout 300
If the seat runs in another terminal host, send the same text there, press Enter, and read its new output (repeating a bounded number of times) for the release line.
HANDOFF_RELEASED in new output means B holds the task. No release inside the deadline
means A did not finish the handoff: inspect A's screen; do not send the order twice — a
second order is a duplicate, not a retry.
A seat marked stale in sno reach seats --json is not proof that its agent is gone: the
mark only says the record was not refreshed for 15 minutes, and an agent that works without
sending mail never refreshes it. Before treating A or B as dead, look at the terminal or
process behind the seat.
When the window was missed: resume from the progress record
If a tick says LATE, or the order gets no release, A is gone. Start B with one command. It points B
at the progress record A kept with sno handoff-checkpoint, so B continues from the record's Next list and
does not redo what Done lists:
sno rotate-agent-resume --to <to-vendor> --cwd <checkout> --checkpoint <record> --work <work> --report-to <owner session>
The command first refuses (FAIL checkout) when the record belongs to a different checkout than --cwd,
naming both. It then compares the record with the checkout (MATCH, or DRIFT lines naming what changed
after the last checkpoint, which it hands to B to inspect), then runs sno reach init and
sno reach spawn --window to put B on a tmux window, and waits for B's fresh RESUME_ACK line, not an
echo of the prompt. Every step prints OK <step> or FAIL <step>: <what> -> <what to do>; the last
line of a clean run is RESUMED seat=<address> verify=MATCH|DRIFT. A <work> label resumes once: a
second call names the existing seat instead of starting another receiver.
B prints DONE_<work> on its own line when every task is done. When a task needs the owner it prints
BLOCKED_<work> <reason> on its own line instead.
It never prints DONE_<work> while any task waits on the owner.
--window puts B on a tmux seat, which keeps working and stays readable after every call to it has
timed out; an ACP seat's work (see the handoff skill, step 2) is lost once its caller's --timeout
expires.
This is the fallback, not the design: it loses whatever A had half-done since its last checkpoint,
which is why the working agent runs sno handoff-checkpoint after every task.
Five things that stay true
- The threshold is code, not judgement.
rotate-agent-watchdecides; the watchman relays. - Never rotate on a blind read.
unknownandneeds_authhold; a percentage that cannot be shown current is not a percentage. - Never hand to a vendor that is also out. The receiver is checked at the crossing.
- Once. The state file makes a second
ROTATEimpossible under the same label. - Vendor names are seats, not sides. Nothing here assumes which vendor is which.
Files bundled with it
These load only when the skill asks for them, so they cost nothing until it runs.
Need help setting it up?
This page tells you what rotate-agent does and what it needs. Cheetah builds the agent setup it runs inside: data, CRM, sequencing and the guardrails.
Book a call →The directory stays free. There is nothing gated behind this.