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>
111 lines
6.0 KiB
Markdown
111 lines
6.0 KiB
Markdown
---
|
||
name: deploy
|
||
title: Deploying Officer
|
||
description: >-
|
||
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.
|
||
version: 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
|
||
|
||
```bash
|
||
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.** `origin` on `officer-dev` points 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 as `Connection 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 `officer` process's `pm_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
|
||
as `Permission denied (publickey)`, which reads as a broken key.
|
||
- **`bun run plugins:update` restarts every plugin's sidecar itself.** A report listing only `officer` under
|
||
"restarted" understates what moved by an order of magnitude.
|
||
- **Skip the host you are running on, entirely.** Restarting `officer` there 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 --porcelain` counts 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:
|
||
|
||
1. Install the plugins whose tables are involved, **before** pushing.
|
||
2. 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.
|
||
3. Expect ONE failure on a `role_permissions` foreign key, then `seed-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.
|