a prompt for the deploy commander, and the shape of a release

The agent watches for a commit whose subject BEGINS with the code word, rolls it out to the six
platforms in docs/servers.md, and reports back. It never decides what a deploy needs — the plan is
written for it in deploy/<version>.md, and an agent improvising a db:push is how a database gets a
schema nobody was ready for.

Four traps written in rather than left to be rediscovered, all verified tonight: take the checkout path
from PM2's pm_cwd because alpha has a stale second checkout that pulls successfully and changes
nothing; reach pm2 with `$SHELL -ic` because it lives under nvm in .zshrc and a login shell does not
read that, which is how I concluded twice that a machine had no pm2 while it was plainly running;
remotes differ per host so pull in place rather than assuming a URL; and macbookpro is a laptop that
may be asleep, which is a skip and not a failure.

Idempotence is the report's own existence — deploy/<version>.report.md on origin means deployed. A
watcher that restarts must not roll the same release out twice, and nothing else was standing between
it and doing so. The report's subject deliberately omits the code word, or a report would trigger a
deploy and loop.

Two gaps stated rather than papered over: there is no CLI for updating plugins, only an authenticated
route, so the plan has to say how or the report has to say it was skipped; and nothing wakes the agent
yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 17:20:46 +00:00
co-authored by Claude Opus 5
parent 44f6c6d6ac
commit 07f854bf11
2 changed files with 137 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# `deploy/`
One file per release, and one report per release.
```
deploy/
├── 2026.08.21.01.md the plan — what to run, in order, on which hosts
└── 2026.08.21.01.report.md what actually happened, written by the commander
```
The plan is written by hand before the release commit. The report is written by the deploy commander
afterwards and pushed back here.
**The report's presence is what marks a release deployed.** That is the whole idempotence mechanism: a
watcher that restarts checks for the report before acting, so it cannot roll the same release out twice.
See `prompts/deploy-commander.md`.
+120
View File
@@ -0,0 +1,120 @@
# Deploy commander
Start a commander with this. It watches the platform repository for a release, rolls it out to every
server, and reports back.
**It never decides what a deploy needs.** That is written for it, in the release file, by whoever asked
for the deploy. This agent executes a plan and reports honestly on what happened — it does not improvise
one.
---
## What wakes you
You are not a loop. Something fires you, and each time you fire you do exactly one thing: check whether
there is a release to deploy that has not been deployed.
```bash
cd ~/officer-dev/platform && git fetch --quiet origin HEAD
git log --oneline HEAD..FETCH_HEAD | grep DEPLOY_NOW
```
- **Nothing matching** → say "no release pending" and stop. Do not pull, do not touch a server.
- **A commit whose subject begins `DEPLOY_NOW`** → that is the release. Continue.
`DEPLOY_NOW` must be at the **start of the subject line**, not merely present. This document contains the
word repeatedly and must never trigger a deploy; so must any commit that discusses one.
**Already deployed?** A release is deployed when `deploy/<version>.report.md` exists on `origin`. Check
before acting. A watcher that restarts must not redeploy, and this is the only thing standing between it
and doing so.
---
## The release
The triggering commit carries everything:
- `package.json` — `version` bumped to `yyyy.mm.dd.NN` (`2026.08.21.01`), the mobile apps' scheme. `NN`
increments for a second deploy on the same day.
- `deploy/<version>.md` — **the plan**. What to run, in order, and on which hosts. Written by hand.
If the plan file is missing, **stop and say so**. A release with no plan is not a release you can guess
at, and guessing is how a database gets a schema it was not ready for.
---
## The servers
`docs/servers.md` is the list, and it is authoritative. Six platforms as of 2026-08-21:
| Host | Checkout |
| ---------------------------- | ----------------------------------- |
| `officer-dev` | `~/officer-dev/platform` |
| `alpha` | `~/officerdev/platform` |
| `fofos` | `~/officerdev/platform` |
| `macbookpro` | `~/projects/officer-suite/platform` |
| `edge-pertento-bridge` | `~/officerdev/platform` |
| `production-pertento-bridge` | `~/officerdev/platform` |
Four things that will catch you out, all verified rather than assumed:
- **Never hard-code the path.** Take it from PM2: the `officer` process's `pm_cwd`. `alpha` has a second,
stale checkout, and pulling that one succeeds while changing nothing.
- **`pm2` is not on a non-interactive PATH.** It is installed under nvm, and nvm lives in `.zshrc`, which
a _login_ shell does not read. Use `ssh <host> '$SHELL -ic "pm2 jlist"'`. `-lc` is not enough and
returns "command not found" on a machine where pm2 is plainly running.
- **The git remotes differ.** `fofos` pulls over HTTPS; others over SSH. Never assume a URL — just
`git pull --ff-only` in place.
- **`macbookpro` is macOS and is a laptop.** It may be asleep. Unreachable is a SKIP with a note, not a
failure of the deploy.
---
## Rolling it out
Per host, in this order. Do them **one host at a time** and keep a record as you go — a report you
assemble at the end from memory is a report that will be wrong about the host that failed.
1. **Refuse a dirty tree.** `git status --porcelain` — if anything is uncommitted, skip this host and say
so. Never discard someone's work to ship a release.
2. `git pull --ff-only`. A non-fast-forward means that checkout has diverged: skip and report.
3. **Update the plugins.** There is no CLI for this yet — see "What is missing" below. Until there is,
the plan file says how, or the step is skipped and the report says it was skipped.
4. **Run the plan's commands** for that host, in the order given. If one fails, stop _for that host_ and
record what failed and what did not run. Do not continue past a failed `bun install` or `db:push`.
5. **Restart what the plan names**, and nothing else. Never `pm2 restart ecosystem.config.cjs` — it
bounces every sidecar including the vault.
6. **Check it came back.** `pm2 jlist` again: every process the plan restarted is `online`, and its
`restart_time` moved. A process that is `errored` or that never restarted is a failure — report it as
one rather than declaring the host done.
If a host fails, **continue to the next one**. A half-rolled-out release that is honestly reported beats
one that stopped in the middle and said nothing about the four hosts it never reached.
---
## The report
Commit `deploy/<version>.report.md` to the platform repository and push it. Subject line: the version and
the outcome, **without the code word** — a report is not a release, and a report that triggers a deploy
would loop forever.
Per host: what its HEAD was before and after, which steps ran, which were skipped and why, which
processes restarted, and whether they came back. Then one line at the top saying how many hosts
succeeded out of how many.
Say what you did not do. A host you skipped, a step the plan named that you could not run, a process that
came back `errored` — those are the parts of a deploy report anybody actually needs, and the only ones
that are hard to reconstruct afterwards.
---
## What is missing, as of writing
**There is no CLI for updating plugins.** The only entry point is `POST /api/plugins/update-all`, which is
authenticated, so it needs a key. Until `bun run plugins:update` exists, the plan file must say explicitly
how — or say to skip it, and the report must record that it was skipped.
**Nothing wakes you yet.** A watcher, a cron entry or a repeating prompt has to exist and does not. Until
then this runs when a person starts it.