Bonesaw Ship

VerifiedCaution

Ship accumulated Bonesaw work to live realm and players: preflight, build, save world, replace worldserver, import SQL, publish client, tag, post Discord. Requires explicit user request.

Sby Skills Guide Bot
DevOpsAdvanced
9/7/2026
Claude Code
#deployment#wow-server#docker#release-management#release-notes

Recommended for

Automated summary

Drives a phased production release of a self-hosted AzerothCore (World of Warcraft) server: build images, warn players, save the world, swap worldserver, import SQL, publish the client, tag the version and post Discord notes.

Strengths

  • Enforces per-ship human authorisation and stop-at-first-failure execution, announcing each phase so an interrupted ship can be resumed from a known point.
  • Encodes project-specific traps: both images must be rebuilt because ac-db-import bakes SQL updates into the image, and `docker compose down -v` is forbidden because it deletes the database volume.
  • Derives patch notes strictly from the unshipped commit list rather than inventing items.
  • Includes useful operational hygiene guidance (WSL2 vhdx growth, crash-dump accumulation) as advisory glance-only checks that never block a ship.

Limitations

  • Tightly bound to one repository layout and toolchain (tools/bonesaw_status.py, Windows, WSL2, Docker), so it is not reusable as-is.
  • `disable-model-invocation` is not honoured by every runtime, so the authorisation guarantee rests on the instruction text alone.
  • No rollback procedure is described for a failure after the worldserver has been replaced.
When to use it

Use it when a maintainer of the Bonesaw server explicitly asks to ship accumulated work to the live realm.

When not to use it

Do not use it for routine development, code review, or builds that must not touch the live realm.

Security analysis

Caution
Quality score82/100

There is no exfiltration, obfuscation, or curl|sh payload; commands are transparent and aimed at a legitimate self-hosted server deployment. However the skill drives Docker, MySQL-as-root, WSL shutdown, and elevated diskpart against a live realm, and relies partly on runtime-specific frontmatter for its safety guarantee. That is powerful-tool usage for legitimate purposes, so caution rather than risky — though the inline root password and undefined allowed-tools are worth fixing.

Findings
  • •Runs privileged infrastructure operations: docker compose build/down/up, wsl --shutdown, elevated diskpart vdisk compaction, docker builder prune — all capable of disrupting or destroying the live realm if misapplied.
  • •Uses MySQL as root with a literal password on the command line (`mysql -uroot -ppassword`), which exposes credentials in process listings / shell history; a secrets file or env var would be safer.
  • •No `allowed-tools` declared despite heavy shell/docker/PowerShell usage, so tool restrictions are not enforced by the manifest (the skill itself acknowledges `disable-model-invocation` is ignored by some runtimes).
  • •Documented `docker compose down` step is destructive-adjacent; the text does correctly warn 'NEVER with -v', which mitigates the main data-loss risk.
  • •Touches a production/live server and imports SQL migrations — high blast radius if invoked without authorization.
How this analysis is made

Examples

Explicit ship command
/bonesaw-ship
Ship with notes
Ship the unshipped commits to the live realm, then post the patch notes in the Discord thread.
Deploy after a fix
Deploy the creature-loot fix that's committed but not live yet — warn players first and tag the new version.

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 UNSHIPPED is 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 "..." to tools/retest_list.py for a paste-ready section, then rephrase anything that still reads like a bug title.
  • Then the longest-waiting open status:awaiting-retest issues, 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.md fails 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

From the file to a first result

The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.

View the source and supporting files

This download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.

1. Download the file

Download SKILL.md

From the root of a test project, save the file as .skill-downloads/bonesaw-ship-mtqry32s/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.

Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.

mkdir -p '.skill-downloads/bonesaw-ship-mtqry32s' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/bonesaw-ship-mtqry32s/download' --output '.skill-downloads/bonesaw-ship-mtqry32s/SKILL.md.part' && mv '.skill-downloads/bonesaw-ship-mtqry32s/SKILL.md.part' '.skill-downloads/bonesaw-ship-mtqry32s/SKILL.md'

2. Check prerequisites

Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.

No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.

The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.

3. Load the skill

After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.

Claude Code
test -f '.skill-downloads/bonesaw-ship-mtqry32s/SKILL.md' && test -s '.skill-downloads/bonesaw-ship-mtqry32s/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/bonesaw-ship' && cp -R '.skill-downloads/bonesaw-ship-mtqry32s' '.claude/skills/bonesaw-ship'

OpenSkills

OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.

Official OpenSkills installation
openskills install './.skill-downloads/bonesaw-ship-mtqry32s'

openskills read 'bonesaw-ship-mtqry32s'

Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.

For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.

openskills sync

Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.

4. Try a first task

Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:

Read .skill-downloads/bonesaw-ship-mtqry32s/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.

Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.

Related skills