name: bonesaw-ship description: Ship accumulated Bonesaw work to the live realm and to players - build, warn, save, replace worldserver, import SQL, publish the client, tag, and post Discord notes. Only ever invoked by the user typing /bonesaw-ship. disable-model-invocation: true
Ship Bonesaw
Stop unless the user asked for this
This is the only skill allowed to touch the live realm.
disable-model-invocation: true in the frontmatter enforces that under Claude
Code, but it is not honoured by every runtime - Pi, for one, reads the same
SKILL.md and ignores that key, which means the model can reach this file on its
own. So the guarantee has to live in the instructions too:
Before Phase 0, confirm the user actually asked to ship in their most recent
message - /bonesaw-ship, "ship it", "deploy", "push it live". If they did
not, say so and stop. Do not ship because the work looks finished, because the
status output says there are unshipped commits, or because a previous message
said to ship something earlier in the session. Authorisation is per-ship and
does not carry forward.
Work through the phases in order and stop at the first failure - do not carry on to a later phase to "get most of it out".
Announce which phase you are entering as you go, so an interrupted ship can be resumed from a known point.
Phase 0 - preflight
python tools/bonesaw_status.py
- Working tree must be clean. If it is not, commit first (do not stash - the stash stack is shared with other worktrees).
- Show the user the unshipped commit list. That list is the patch notes source; do not invent items that are not in it.
- If
UNSHIPPEDis empty and the server is not behind, there is nothing to ship. Say so and stop.
Glance at the disk (not every ship - roughly monthly)
Phase 1 builds an image every single ship, and WSL2 virtual disks only ever
grow. Nothing shrinks docker_data.vhdx on its own, so it creeps until C: is
full and a build fails for reasons that look nothing like disk space.
powershell -NoProfile -Command "'{0:N1} GB free on C:' -f ((Get-PSDrive C).Free/1GB); '{0:N1} GB vhdx' -f ((Get-Item $env:LOCALAPPDATA'\Docker\wsl\disk\docker_data.vhdx').Length/1GB)"
This is a look, not a gate - never block a ship on it, and do not compact
mid-ship. If C: is under ~40 GB free, or the vhdx is far larger than
docker system df says Docker is actually holding, mention it to the user and
move on. It is its own maintenance job, done between ships.
When they want it done, the sequence is a realm restart plus a compaction, so it follows the same warn/save rules as Phase 2:
docker builder prune -f # build cache; check `docker system df` first
tools/save_world.ps1 # or restart_worldserver.ps1 if humans are on
docker compose down # NEVER with -v: that deletes wow-bonesaw_ac-database
wsl --shutdown # quit Docker Desktop first or it restarts WSL
# then, elevated: diskpart /s <script> with
# select vdisk file="...\docker_data.vhdx" / attach vdisk readonly / compact vdisk / detach vdisk
docker compose up -d
On 2026-09-01 that vhdx had reached 171 GB while holding ~35 GB of data;
compaction returned 133 GB. Also worth a glance:
%LOCALAPPDATA%\Temp\wsl-crashes had 43 GB of worldserver dumps, nine of them
in three days - they accumulate on their own after every crash and nothing
cleans them up.
Decide the version: bump the patch number in tools/client-update/Bonesaw.version.
Never skip a number, including for server-only ships - the launcher and the
Discord thread both key off it.
Phase 1 - build before anything live moves
docker compose build ac-worldserver ac-db-import
Both images. This is not optional and it is the bug that keeps recurring.
ac-db-import bakes data/sql/updates/pending_db_* into the image - there is
no bind mount. Rebuilding only ac-worldserver means every new migration is
silently skipped forever: on 2026-08-22 rev_living_gear_account_keys.sql had
been sitting unapplied since 08-21 because of exactly this, so the account key
ring was dead while its code read perfectly.
If the build fails, stop here. Nothing live has been touched yet, which is the entire reason the build comes first. Fix the compile error and restart the ship.
Phase 2 - warn and save
Skip entirely if ac-worldserver is not running.
First, count REAL players online. The countdown exists to stop players taking a rollback; with nobody to roll back, five minutes of announcements to an empty realm is just five minutes. Playerbots do not count - they have no progress a restart can cost them.
docker compose exec -T ac-database mysql -uroot -ppassword -N -e "SELECT COUNT(*) FROM acore_characters.characters c JOIN acore_auth.account a ON a.id = c.account WHERE c.online = 1 AND a.username NOT LIKE 'RNDBOT%';"
Bot accounts are all named RNDBOT* (144 of 147 accounts at the time of
writing), so excluding them is what makes this count mean "humans". Do NOT use
the worldserver console's server info for this - its "Connected players"
includes bots, so it reads non-zero on an empty realm.
If that count is 0: save and restart immediately, no announcements.
powershell -NoProfile -ExecutionPolicy Bypass -File tools/save_world.ps1
On the Linux host, use python3 tools/save_world.py instead. There is no
PowerShell there, and the .ps1's documented fallback tools/worldserver_cli.py
is Windows-only too (it opens the Docker named pipe through ctypes.WinDLL), so
with SOAP disabled the entire warn/save path is unavailable and the ship silently
has no way to save at all. save_world.py attaches to the console on a pty and
leaves with ctrl-p ctrl-q. Do not replace it with a pipe into docker attach:
stdin goes straight to the worldserver console and EOF halts the server.
Note that port 7878 answering a TCP connect does NOT mean SOAP is up -- docker
proxy binds the host side regardless. Check SOAP.Enabled in
env/dist/etc/worldserver.conf.
The save still runs. It costs about three seconds and it is not a warning - it
protects world and bot state, and a stale online flag left by an earlier crash
is the one case where this count could be wrong in the dangerous direction. If
it is wrong the other way (a human logs in during the restart) they get a clean
reconnect, not a rollback.
If that count is greater than 0: the full countdown, no shortcuts.
powershell -NoProfile -ExecutionPolicy Bypass -File tools/restart_worldserver.ps1
There is no Linux port of the countdown. If humans are on and you are on the
Linux host, drive it by hand with the same helper -- python3 tools/save_world.py --command "announce <text>" --expect "" for each step of the 5/2/1/0:30/0:10
ladder, then a final plain python3 tools/save_world.py for the saveall. Do not
skip the ladder because the tool is missing.
Announces in game on a staged countdown (5 minutes, then 2, 1, 30s, 10s), then
saveall. Wait for its final line, OK: players warned ... and saved; restart is allowed. An exit code of 0 is not the same thing: on 2026-08-23 that
script was killed partway, exited 0, and had never reached the saveall - the
missing final line was the only evidence. Never use AzerothCore
server shutdown for a docker replace - it races saveall on its own timer.
Run it in the background rather than in the foreground, or a tool timeout will kill it mid-countdown, which is exactly how that happened.
Phase 3 - replace the server
docker compose up -d ac-db-import
docker compose up -d ac-worldserver
db-import first and let it complete; worldserver depends on it. Then confirm the module actually loaded and no migration errored:
docker logs --tail 200 ac-worldserver | grep -iE "living gear|error|Applying"
Re-run python tools/bonesaw_status.py - pending SQL must now read
all imported. If it does not, the db-import image did not pick the file up;
go back to Phase 1.
Phases 4-6, automated
The bookkeeping after the container replace (client publish, manifest, patch
notes, Discord, release notes, tag, push) is mechanical and was the recurring
source of forgotten steps - 0.1.79/0.1.80 shipped with no Discord post, and the
live manifest went stale twice in the 0.1.105/0.1.106 era. One command runs
the whole tail in the right order, refuses to continue on any failed step, and
tags LAST so git log --first-parent ship/<latest>..HEAD is empty when it exits:
python tools/ship_bookkeeping.py --version X.Y.Z --notes-only # then --post
python tools/ship_bookkeeping.py --version X.Y.Z --no-client --notes-only
python tools/ship_bookkeeping.py --version X.Y.Z --no-client --post
Run stage 1 (--notes-only) after Phase 3's bonesaw_status says
pending SQL: all imported. It bumps the version (exactly one patch),
builds the client if any, writes tools/patch-notes/X.Y.Z.md and stops -
nothing has been posted. The script's bullets are raw commit subjects and
only a human pass makes them read as notes, so the pass is now mandatory and
happens between the stages: edit the notes file (player-facing bullets, retest
asks for THIS ship's own changes first, no code names or file paths), then run
stage 2 (--post), which re-runs the retest check, prints the notes it is
about to publish, and only then posts to Discord, updates the GitHub release,
commits the notes, tags LAST and pushes. One set of notes goes out - the
reviewed one. The phases below document what the script does; use them as the
manual fallback when the script cannot run.
Push only to origin Raajik/wotlk-bonesaw, never to the playerbots
remote. Never force-push main.
Phase 4 - client, if client files changed (manual fallback)
Needed when anything under modules/mod-living-gear/client_addon/,
tools/client-patch/ or tools/launcher/ changed since the last ship:
git diff --stat ship/<previous>..HEAD -- modules/mod-living-gear/client_addon tools/client-patch tools/launcher
If it is empty, skip to Phase 5 - a server-only ship still bumps the version and still gets Discord notes.
Otherwise follow tools/client-update/README.md under "Ship to all players".
That file is the source of truth for these steps; do not reproduce them from
memory. The order that matters: build the launcher, create the GitHub release,
then --verify, then push the manifest. The exe is not byte-reproducible, so
rebuilding between hashing and uploading strands every player.
Push only to origin Raajik/wotlk-bonesaw, never to the playerbots
remote. Never force-push main.
Phase 5 - record the ship (tag LAST)
git commit -am "Ship Bonesaw X.Y.Z: <one line>"
git tag ship/X.Y.Z
The tag is what makes the next /bonesaw-status able to say what players have.
Without it the whole thing goes blind again. Tag after the notes commit, not
before - a tag chased by manifest/notes commits leaves --first-parent ship/<latest>..HEAD
non-empty forever and every later status reads "N commits not shipped"
(0.1.104/0.1.105/0.1.106 all needed a tag re-point for exactly this).
Phase 6 - tell people
This phase used to read "Post to Discord" with nothing to run, so it was done by hand or not at all - 0.1.79 and 0.1.80 both shipped without the notes ever reaching players, and the GitHub releases carried the placeholder "Run Bonesaw.exe." instead of a changelog. Writing notes into the chat reply is not the same as posting them. Do all three steps below.
1. Write the notes to a file
tools/patch-notes/X.Y.Z.md
Built from the Phase 0 commit list, following
.cursor/rules/discord-patch-notes.mdc: player-facing only, ASCII, plain -
bullets, grouped by theme, no file paths or spell IDs, only what actually
shipped. Extra jump is disabled - do not advertise it. Title line is
**Bonesaw X.Y.Z - patch notes**. If the client changed, end with Close Wow
and run Bonesaw.exe to update.
Every notes file ends with a "Please test these things" section, placed
just before the update line. Fixes that shipped but were never confirmed
otherwise silently age out on the tracker, and players are the only test
environment that counts. Source it live at notes-writing time, never from a
stale list. GitHub is the canonical tracker and status:awaiting-retest is
its queue of shipped-but-unconfirmed work (in-game lg_bug_report rows reach
that label via tools/bug-reports/bug_resolve.py after the sweep):
python tools/retest_list.py --raw
- First 1-4 asks about THIS ship's own changes that only a player can verify
("craft something that needs banked materials - does anything land in your
bags?"). Pass them as
-a "..."totools/retest_list.pyfor a paste-ready section, then rephrase anything that still reads like a bug title. - Then the longest-waiting open
status:awaiting-retestissues, so stale items get the spotlight instead of aging out. Drop the[Report #N]prefix - players cannot see the tracker, so issue numbers never appear in notes. - One short ask per item ("Try X, tell us if Y still happens"). Roughly 8 bullets max so the notes stay one Discord message where possible.
- If nothing is genuinely awaiting retest, omit the section. Never re-list an issue that already got a confirmed retest and was closed - the tool only lists open ones, but check before hand-adding anything.
python tools/retest_list.py --check tools/patch-notes/X.Y.Z.mdfails the ship if the section is missing or a[Report #N]leaked through.
The file is the record: it is what gets posted, what the GitHub release shows, and what the next person reads to see what a version contained. Commit it.
2. Post to Discord
python tools/post_patch_notes.py tools/patch-notes/X.Y.Z.md --dry-run
python tools/post_patch_notes.py tools/patch-notes/X.Y.Z.md
Dry-run first - it checks the notes are ASCII and previews the title-line body
plus the attachment before contacting anyone. Posting is attachment-only: the
body is just the title line and the full notes ride as a .txt attachment, so
the 2000-character message limit never splits or truncates anything. The
webhook lives in
tools/client-update/discord.webhook, which is gitignored because it is a
credential; never echo it into a log or a commit.
The script exits non-zero if Discord rejects anything, so a failed post stops the ship rather than being reported as done.
3. Put the same notes on the GitHub release
gh release edit vX.Y.Z --repo Raajik/wotlk-bonesaw --notes-file tools/patch-notes/X.Y.Z.md
Phase 4 creates the release with a placeholder because the notes may not exist yet at that point. This is where it gets the real changelog, so the release list is a readable history rather than a wall of identical one-liners.
4. Record the learnings
Append durable learnings to A:\obsidian\jeremy\wiki\Bonesaw.md - crashes, UI
rules, deploy gotchas, spell IDs, do-not-repeat mistakes. Part of shipping, not
after it.
Same file from the Linux host: /run/media/muckfup/aba/obsidian/jeremy/wiki/Bonesaw.md.
It is CRLF -- write it back as CRLF or the whole page shows as modified.
Finally
Re-run python tools/bonesaw_status.py. It should exit 0 with
"Everything committed, built, imported and published." If it does not, say
exactly what is still outstanding rather than calling the ship done.
Also confirm the notes end with the test section:
python tools/retest_list.py --check tools/patch-notes/X.Y.Z.md
Docker Compose Architect
DevOps
Designs optimized Docker Compose configurations.
Incident Postmortem Writer
DevOps
Writes structured and blameless incident postmortem reports.
Runbook Creator
DevOps
Creates clear operational runbooks for common DevOps procedures.