Skip to content
Back to skills

Coordinator Deploying A Node

ASecurity

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.

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

Works with

  • terminal
  • cli

Security analysis

A100/100

Pro scans all 3 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/badge)](https://www.skillsdirectory.com/skills/thrum-team-coordinator-deploying-a-node)

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."
---

# 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.6 KB
  • 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.