Files
capabilities/skills/deploy/SKILL.md
T
pastilhasandClaude Opus 5 c24859c925 skill names are identifiers, not labels
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>
2026-08-27 13:11:20 +00:00

111 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.