lowercase-hyphenated and equal to the directory, per both harnesses' rules. the human name moves to `title`. a skill named "Deploying Officer" was listed to an agent and could not be loaded by it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.0 KiB
name, title, description, version
| name | title | description | version |
|---|---|---|---|
| deploy | Deploying Officer | Use when asked to deploy, release or roll out Officer, to watch for a release and deploy it, or when a deploy has gone wrong — a host behind, a tag not noticed, a rollout that stopped halfway. Points at the procedure, and carries the things about THIS fleet that are not in it. | 1 |
The procedure is a file, and it is not here
Read it:
~/officer-dev/platform/prompts/deploy-commander.md
Not copied into this skill, deliberately. It names scripts/deploy-claim.sh, docs/servers.md and the
plan format, all of which live in that repository and change with the code. A second copy here would be
right on the day it was written and wrong on the day somebody edits the first one — and the copy an agent
happens to read would be the stale one. One hop costs a Read; a wrong procedure costs a fleet.
That file carries the whole rollout: what wakes you, what a release is, the per-host order, what to refuse, and why each rule exists. Everything below is what is true about this fleet and would not belong in it.
What a release is here
A tag, deploy/<version>, version yyyy.mm.dd.NN. Nothing else triggers a deploy — not a commit
message, not a branch, not a push. A tag cannot be typed into prose by accident, which a code word can, and
git ls-remote --tags answers "is there one" in a single call without fetching.
package.json must agree with the tag, and deploy/<version>.md — the plan — must exist. If either is
untrue, stop and say so. A release with no plan is not one you can guess at.
One attempt per release, and it is not yours to enforce
scripts/deploy-claim.sh claim <version> # exit 0 = yours, exit 1 = do not proceed
scripts/deploy-claim.sh done <version> # after it succeeded
Exit 1 means somebody already attempted this release, and that answer never changes back. A failed release is not retried, it is looked at. Clearing a claim is a human act.
This is a script rather than a rule you follow because the watcher loop is rewritten from scratch every time
one arms, and the wrong implementation is the one that looks right: test the .done file, which exists only
on SUCCESS. Write that and a failing release spawns a fresh agent every poll, all night, each one costing
money. That is not hypothetical.
Fleet facts that cost time to learn
- Poll a LOCAL remote.
originonofficer-devpoints at the local gitea. The public hostname resolves through a reverse proxy where every connection shares one source IP, and rate limiting there refuses SSH for long stretches — presenting asConnection closed by <ip>and "make sure you have the correct access rights", which is not an auth problem. Cost: a tag pushed at 21:47 was noticed at 23:07. With the local remote, twenty seconds. - Never hard-code a checkout path. Take it from PM2 — the
officerprocess'spm_cwd. One host carries a second, stale checkout, and pulling that one succeeds while changing nothing. - The login user is not always the same. It is per host, in
docs/servers.md. A wrong username presents asPermission denied (publickey), which reads as a broken key. bun run plugins:updaterestarts every plugin's sidecar itself. A report listing onlyofficerunder "restarted" understates what moved by an order of magnitude.- Skip the host you are running on, entirely. Restarting
officerthere ends the rollout mid-report. Name it as skipped, and say whether it is behind or already current — those are different, and only one of them asks the owner for anything. - A dirty tree means TRACKED changes.
git status --porcelaincounts untracked files; a stray script a fast-forward could never touch is not a reason to skip a host. Use--untracked-files=no.
db:push can offer data loss at a prompt, and it waits for an answer
A schema push on a host that is many commits behind can stop and ask whether a table was RENAMED. Both answers destroy something, so neither is safe to guess and neither is safe to automate.
Seen 2026-08-27 catching edge-pertento-bridge up by 215 commits:
Is command_hq_spawned table created or renamed from another table?
❯ + command_hq_spawned create table
~ dav_app_passwords › command_hq_spawned rename table
drizzle-kit saw one table leaving the platform schema and another arriving, and could not tell a rename
from a coincidence. rename would have turned the DAV app passwords into the spawned-commander table;
create would have dropped them.
The table had not gone anywhere — it had moved to a PLUGIN. Caldav was extracted that day, so
dav_app_passwords belongs to plugins/caldav/db/schema.ts and reaches the barrel through
gen-plugin-schemas. Installing the plugin FIRST declares it, and the question disappears.
So, on a host far enough behind that a plugin extraction is in the gap:
- Install the plugins whose tables are involved, before pushing.
- Then
bun db:push < /dev/null— stdin closed, so it can never hang waiting on a prompt in an unattended run. A push that needs an answer fails instead, which is the outcome you want. - Expect ONE failure on a
role_permissionsforeign key, thenseed-roles.ts, then push again. That sequence is normal on a host predating the roles table.
Back up the database first. pg_dump against the URL in .env — and read the variable name rather
than assuming: several hosts use OFFICERDEV_POSTGRES_URL, not POSTGRES_URL.
The report is an artefact, the lock is the record
Write the local record FIRST, then commit and push the report. If the push fails, say so and stop: the deploy succeeded and its paperwork is pending. Retrying a push into a loop, or treating a failed push as a failed deploy, are both worse than a missing document.
Say what you did NOT do. A host skipped, a step you could not run, a verification you had to perform differently — those are the parts of a report anybody actually needs, and the only ones that are hard to reconstruct afterwards.