Skip to content
Back to skills

Coordinator Deploying A Node

ASecurity

> This runbook covers EVERY deploy to a single node, migrating or not. Most > sections apply to every deploy; only migration-weight and store-corruption > specifics are bump-only.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
databasespythonrustgosqlnodegit

Works with

  • terminal
  • cli

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add thrum-team/thrum-pro --skill coordinator-deploying-a-node --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Coordinator Deploying A Node?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Coordinator Deploying A Node
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/thrum-team-coordinator-deploying-a-node-thrum-pro/badge)](https://www.skillsdirectory.com/skills/thrum-team-coordinator-deploying-a-node-thrum-pro)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: coordinator-deploying-a-node
description:
  "Use when deploying a build to a single node, migrating or not — deploying a
  build, schema-migrating deploy, pro-bundle install plus service restart on a
  node, roll out a build to a node, ship to a node, single-node canary. Loads
  the deploy runbook — verifying the artifact actually CONTAINS the fix,
  install-before-restart ordering, verify-by-effect, and preventing store
  corruption on migration."
# source: claude-plugin/skills/coordinator-deploying-a-node/SKILL.md
---

## Coordinator: Deploying a Build to a Single Node — migrating or not

> This runbook covers EVERY deploy to a single node, migrating or not. Most
> sections apply to every deploy; only migration-weight and store-corruption
> specifics are bump-only.

### 0. Pull-current first (ff-only)

The absolute first action before anything else: fetch the control-plane
repository and fast-forward to the orchestrator's pin.

- Read the project's deployment adapter inside `.thrum/config.json`
  (`fleet.deployment`; reference at the deployment-adapter reference) to obtain
  `preflight.pull_command` and `preflight.pin_command`.
- Run `preflight.pull_command` (must be ff-only; if not fast-forwardable, STOP
  and surface — never blind-merge in the shared tree).
- Verify the delivered artifact actually CONTAINS the fix at the pinned SHA
  (adapter `artifact.commit_file` sidecar).

This ensures the node's coordinator reasons from a checkout that is actually
current before evaluating backup, install, or migration.

> Example — Thrum project: pull via
> `git fetch origin && git merge --ff-only origin/<branch>` before any backup or
> install.

### 1. Who executes — resolve the node's local coordinator (via adapter identity)

The node's own coordinator drives the deploy locally on its own host. Never
drive a node's build/install/restart from another host.

- Resolve WHO via `topology.identity_resolution` (adapter
  `fleet.deployment.thrum.topology.identity_resolution` or
  `fleet.deployment.products.<id>.topology`) — a `daemon_id` join plus hostname
  and absence of `.thrum/redirect`, never by agent name or worktree name.
- When connected-repo daemons share a host, respect the adapter-defined restart
  order within that host.

If the target node has no working coordinator, state why explicitly before using
any fallback.

### 2. Back up before touching anything (via adapter backup command + sizing formula)

- Run the project's backup command (`fleet.deployment.thrum.backup.command` or
  `fleet.deployment.products.<id>.backup.command`) and verify per
  `backup.verify`.
- Size BEFORE you run it on a bloated node: apply the adapter's
  `backup.sizing_formula`
  (`free_at_completion = free_now - zip(stale current/) + size(stale current/) - size(live lane)`)
  — the stale `current/` is removed before the fresh export, so peak is not
  double-count. Retention runs last and does not bound the peak.
- On failure, roll back via `backup.restore_command`; never delete the store.

> Example — Thrum project: backup via `thrum backup`, sized per the formula
> above, verified with `sqlite3 ?mode=ro&immutable=1` read.

### 3. Storage access — through the engine or control-plane CLI, never file-bypass

Access the state store only through the engine or the control-plane CLI in
read-only mode. Never bypass the storage engine's file ownership.

- Via: `storage_access.via` (e.g., CLI or `?mode=ro`).
- Forbidden: `storage_access.forbidden` (e.g., hand-copy of live `-wal`/`-shm`,
  concurrent `.backup` on live store).
- A bare read-write open can fire a checkpoint that truncates the write log out
  from under live readers → sustained corruption. Always use `?mode=ro` where
  applicable.

If the project has no WAL/SHM, this section is N/A — skip and note.

### 4. Never wrap service start/restart in a blind timeout

🔴 **On macOS, use `service.mac_restart_command`, not
`service.restart_command`.** (Example — Thrum adapter:
`service.mac_restart_command` preserves Local Network (TCC) permission.) A
daemon restarted from a terminal or agent pane gets its Local Network (TCC)
permission attributed to that terminal and is silently denied LAN peer dials
(`no route to host`): peers go dark while the node itself looks healthy, and
`PPID 1` does NOT prove it was re-parented correctly. Check the node's OS before
choosing the command. After a mac restart, verify the peers on the LAN
reconnect, not just the version.

Run the project's service restart command
(`fleet.deployment.thrum.service.restart_command`, or
`service.mac_restart_command` on macOS, or product equivalent) without a blind
timeout wrapper. A timeout that kills a migrating restart mid-migration
manufactures a half-migrated store.

- Adapter flag `service.never_timeout` is true.
- Signal-window rules are adapter data: `migration.signal_rules` (`SIGQUIT`
  fatal-always, `SIGUSR1` fatal-before-socket). Respect them — do not send fatal
  signals in the forbidden window.

> Example — Thrum adapter: use the configured `service.mac_restart_command` on
> macOS; use `service.restart_command` otherwise. Never wrap either in
> `timeout`.

### 4a. EULA gate — before the binary swap

EULA gate (0.11+): before swapping the binary, record acceptance for the box
user by running the new build once with `THRUM_ACCEPT_EULA=<bundled version>`.
Confirm that `~/.thrum/license/eula-acceptance-<version>-*.json` exists and that
a plain `thrum inbox` works. Skipping this breaks every agent's CLI on the box.

### 5. Install from the delivered artifact (never rebuild on the node)

Install from the identical bundle the orchestrator built, delivered to
`artifact.distribution.drop_path`.

- Verify BEFORE unpack that the delivered bundle's version/checksum/commit
  sidecar equals the orchestrator's pin (adapter `artifact.checksum`,
  `artifact.version_file`, `artifact.commit_file`).
- Run `install.command` (from adapter). The node does not compile, sign, or
  rebuild — it installs.
- Collect BEFORE readings (current version, state counters, checkpoint counts,
  health probes) before the install.

> Example — Thrum project: install from the adapter's
> `artifact.distribution.drop_path` via the adapter's `install.command`.

### 6. Measure from the running process, never the checkout

Read version/state from the running service, not from the filesystem checkout.

- Verify via `install.verify_by_effect` probes: version equals pin, NEW PID,
  service status reports pin, sidecar equals pin.
- Do not trust installer exit 0 alone.

### 7. Migrate via service restart — verify by effect

Migration is performed by restarting the service after install; the service's
own boot migration does the schema/data move.

- Calibrate weight via `migration.weight_calibration` (heavy bulk-export vs
  light add-column) — do not assume a fixed duration.
- Known signatures that require a cold-restart recovery:
  `migration.sigbus_signatures` (e.g., WAL-index recover signatures) and
  `migration.on_sigbus`.
- Observe that the restarted service reports the new version/state and that
  health probes have converged (adapter `health.probes`,
  `health.convergence_wait`).

### 8. Verify post-migration (liveness, health probes, checkpoint counts, agent liveness)

Run adapter `health.probes` and compare BEFORE/AFTER:

- Service alive and reporting pin.
- State counters converged per `health.convergence_wait` (mechanism-derived,
  e.g., `3m`).
- Crash log empty, expected agents live.

For tandem daemons that share a host or depend on each other, verify the
coordinated set per adapter.

### 8a. Re-render configuration/templates if the build changes them

If the build carries template or role changes, re-render between install and
restart:

- Commands: `templates.refresh_command` and `templates.deploy_command`.
- Order: `templates.order` (`refresh→deploy` between `install.sh` and daemon
  restart).
- Verify by effect (render output, then service restart).

> Example — Thrum project: `thrum roles refresh` then `thrum roles deploy`
> between install and restart.

### 8b. Plugin/skill refresh if the deploy carries skill changes

If the bundle carries skill/plugin changes, refresh the plugin cache:

- Refresh driver: `plugin.refresh` (version bump required — cache keyed by
  version).
- Manifests: `plugin.manifests` (all skill-carrying manifests bumped in the same
  commit as the new skills).

Do not hand-edit generated marketplace manifests.

### 8c. Reconcile registry/phase after restart

Re-verify the daemon registry and agent phases after restart (adapter
`health.probes` includes phase reconciliation). Confirm the pinned version is
durable across a clean cold-boot probe where applicable.

### 8d. Verify the GUI shows every agent (by effect)

A build can decode `agent.list` fine on the daemon and still hide agents in the
GUI (a strict UI decode once hid 485 of 538 on a canary). Compare what the
daemon lists with what the GUI renders, on the node:

1. Expected numbers, run ON the node (python3 only, read-only, calls the
   daemon's own `agent.list` over its socket):
   `python3 <skill>/resources/agent-count-expected.py` prints `rail_expected`,
   `fleet_expected` and `records_lacking_proof`. Do not use `thrum team --all`
   as the baseline: it is `team.list`, a different set (stale rows included).
2. Rendered numbers. Needs Playwright plus a browser. The script tries
   Playwright's bundled Chromium, then the newest installed `chromium-<rev>`
   under the Playwright browsers dir (`PLAYWRIGHT_BROWSERS_PATH`, else the
   per-user `ms-playwright` cache; a Playwright upgrade can leave only other
   revisions), then an installed Google Chrome. It prints the route used
   (`browser=...`) on stderr and never installs a browser, so a node near its
   disk guard is not touched.
   `node <skill>/resources/agent-count-gui.mjs <ui-url>` (the `ui` line of
   `thrum daemon status`; the script header says where Playwright must resolve)
   prints `rail` and `fleet`; it exits 3 with a message when no browser is
   usable. Without a browser, open `<ui-url>` at 1280x720 and read by hand:
   - rail: the heading `Agents (N)` above the left-rail list (`h3` in the agent
     list);
   - Fleet: add the five badges `category-count-Working`, `-Standby`,
     `-EmptySession`, `-RuntimeExited`, `-Worktree` (the Stale and Archived
     badges come from a separate query and are not part of the sum);
   - notice: the box `incomplete-agents-notice`, if present.
3. PASS = `rail == rail_expected` AND `fleet == fleet_expected`. The GUI must
   render ALL records, complete or not: a record lacking its daemon or repo id
   is shown, not dropped. If the notice is present it is information only.
   Report `records_lacking_proof` as an info line ("N records lack repo/daemon
   id"); it is not a failure and it will stay above zero until every home daemon
   is upgraded.
4. FAIL = either number is lower than expected (the hidden-fleet class), or the
   GUI shows 0 where the daemon lists agents. Stop the roll and report both sets
   of numbers.

Rules mirrored from `AgentList.tsx` and `FleetGrid.tsx` (rail: local-origin
rows, no users, no retired rows, deduplicated by bare name; Fleet: every
non-stale, non-archived record). If those components change the filter, update
`agent-count-expected.py` and its test in the same change.

### Red flags — STOP

- Pull not ff-only.
- Backup skipped or undersized.
- Storage accessed via forbidden path (`storage_access.forbidden`).
- Restart wrapped in a blind timeout or killed with `SIGQUIT`/`SIGUSR1` in a
  fatal window (`migration.signal_rules`).
- Install rebuilt locally instead of from the delivered artifact.
- Post-migration health not verified by observed effect.
- Template re-render skipped when the build changed templates (`templates.order`
  violated).
- GUI agent counts not compared with `agent.list` after the roll, or a lower
  rail or Fleet count accepted (section 8d).

Files in this skill

  • SKILL.md11.7 KB
  • agents/openai.yaml903 B
  • resources/agent-count-expected.py4.7 KB
  • resources/agent-count-gui.mjs7.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.