Read-only fleet-size consumer for the 6 mac + 16 linux ezgha runner invariant. Restart-only remediation is UNAVAILABLE (fail-closed) pending jleechanorg/ez-gh-actions PR 67 (dual-Lima convergence) and PR 70/issue 60 (recovery controller) merging AND being proven live-deployed — a 2026-07-09..11 audit found restarts can kill active jobs, create offline-busy 422 registrations, and cannot repair a dead/wrong-namespace backend. Use when the user says "make sure we have N mac and M linux runners", "ezgha fleet short", "supervisor stuck", or after any /runner-health AMBER/RED verdict — but diagnose with ez-gh-actions' doctor-runner first, never treat a shortfall as expected churn.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Read-only fleet-size consumer for the 6 mac + 16 linux ezgha runner invariant. Restart-only remediation is UNAVAILABLE (fail-closed) pending jleechanorg/ez-gh-actions PR 67 (dual-Lima convergence) and PR 70/issue 60 (recovery controller) merging AND being proven live-deployed — a 2026-07-09..11 audit found restarts can kill active jobs, create offline-busy 422 registrations, and cannot repair a dead/wrong-namespace backend. Use when the user says "make sure we have N mac and M linux runners", "ezgha fleet short", "supervisor stuck", or after any /runner-health AMBER/RED verdict — but diagnose with ez-gh-actions' doctor-runner first, never treat a shortfall as expected churn.
["ezgha serve is a long-running supervisor that respawns runner containers as jobs complete (one job per container, then exit). It replaces churned slots but does NOT aggressively top-up to N when below configured count — if a slot stays deregistered for any reason (rate limit, crash, race), serve won't refill it on its own loop. That part of the diagnosis is still true.","What is NO LONGER true: treating a restart as the normal/expected fix, or treating 5/6 mac or 14/16 linux as unremarkable churn. A 2026-07-09..11 audit (bead rev-ft3i8, GH issue #8329) proved restarts can kill ACTIVE jobs mid-run, can create offline-busy 422 runner registrations that neither `ezgha status` nor the GitHub API auto-cleans, and CANNOT repair a dead or wrong-namespace backend (e.g. a Lima VM pointing at the wrong Docker context) — a restart just respawns containers against the same broken backend, looking 'recovered' while masking the real defect.","Runtime recovery logic (graceful drain, backend health verification, safe respawn ordering) belongs in jleechanorg/ez-gh-actions, not your-project.com. This repo's skills/scripts are READ-ONLY CONSUMERS of that repo's health contract (doctor-runner, ezgha status, GitHub runner API) — do not add backend-recovery mutation logic here.","Expected fleet: 6 mac (`ez-mac-runner-b-1..6` on MacBook via `~/.config/ezgha/config.toml` `count=6`) + 16 linux (`ez-runner-b-1..16` on jeff-ubuntu via SSH `~/.config/ezgha/config.toml` `count=16`) = 22 total.","A shortfall (N/M below configured count) is a DIAGNOSTIC PROMPT, not a threshold to dismiss. Run `doctor-runner` (see below) before deciding anything is 'expected'. Never lower `count` or the alert threshold to make a shortfall look normal — that hides the same defect the audit found."]
/ezgha-watchdog — read-only consumer of the 6 mac + 16 linux runner invariant
STATUS (2026-07-13): restart-only remediation is UNAVAILABLE — diagnose-first, fail-closed
Do not treat launchctl kickstart / systemctl --user restart ezgha as a
routine self-heal. The previous version of this skill said restarting the
supervisor was the normal/expected fix and that 5/6 mac or 14/16 linux was
"expected churn — don't alarm." Both statements are now known to be unsafe
defaults per the 2026-07-09 through 2026-07-11 audit (bead rev-ft3i8, GH
issue #8329):
Restarts can kill active jobs mid-run, not just idle slots.
Restarts can create offline-busy 422 runner registrations that neither
ezgha status nor GitHub's runner API cleans up automatically.
A restart cannot repair a dead or wrong-namespace backend (e.g. a Lima
VM pointing at the wrong Docker context) — it just respawns containers
against the same broken backend, which looks recovered while the real
defect stays live.
Gating condition for restart-only mutation to become available again —
BOTH must be independently confirmed merged in git AND proven live-deployed
on the Mac/Linux hosts (a git merge alone is not activation — see "MANDATORY:
post-merge activation check" below):
jleechanorg/ez-gh-actions PR #67 — dual-Lima convergence to a single
canonical Docker backend (bead ez-gh-actions-apye). Verify current
status with:
gh api repos/jleechanorg/ez-gh-actions/pulls/67 --jq '{state,merged,merged_at}'
Status as of 2026-07-13: {"state":"open","merged":false,"merged_at":null}
— NOT merged.
jleechanorg/ez-gh-actions PR #70 — singleton backend-aware recovery
controller for Mac+Linux (bead ez-gh-actions-ghd2.7, tracking issue
ez-gh-actions#60).
Verify current status with:
gh api repos/jleechanorg/ez-gh-actions/pulls/70 --jq '{state,merged,merged_at}'
Status as of 2026-07-13: {"state":"open","merged":false,"merged_at":null}
— NOT merged.
Until both are merged AND their live-deployment is independently proven (see
"Live-deploy proof required" below), any restart of — via this
skill's script, , or — is a
, never an automated first
response or something to silently normalize.
ezgha serve
launchctl kickstart
systemctl --user restart
last-resort, human-approved action only
Restart gate (fixed 2026-07-16, bead rev-5rqci): scripts/ezgha-fleet-watchdog.sh
(the launchd/systemd-installed watchdog, ticking every 120s) now always
detects and logs a below-target shortfall, but only executes the actual
restart command (launchctl kickstart / systemctl --user restart) when
EZGHA_WATCHDOG_ALLOW_RESTART=1 is explicitly set — unset/0 is fail-closed
(alert-only), matching this doc's stated policy. Do not set that env var in
the launchd/systemd unit until PR #67/#70 (or an equivalent live-deployed
recovery path) is confirmed non-destructive. Regression test:
scripts/test_restart_gate.sh.
Diagnose with the real tool: doctor-runner (ez-gh-actions repo)
doctor.sh in ez-gh-actions is LEGACY/BROKEN on Docker 27+ (bead
ez-gh-actions-91r) — on modern Docker it silently misclassifies every
working runner as IDLE. Use doctor-runner instead — the authoritative
script (shipped 2026-07-08; extended 2026-07-09 with the 4-state per-slot
activity-truth model):
# From an ez-gh-actions checkout (clone a scratch copy if you don't have one):
git clone https://github.com/jleechanorg/ez-gh-actions.git /tmp/ez-gh-actions-check
cd /tmp/ez-gh-actions-check
./doctor-runner # health gate: exit 0 = healthy, real per-slot state
./doctor-runner --prove # + dispatches a live canary job and verifies it lands# on the configured runner prefix with conclusion=success# — the strongest available evidence
If working directly inside an ez-gh-actions checkout, use its own slash
command instead: /doctor-ezactions (runs doctor-runner, then /harness
automatically when unhealthy or the queue tail exceeds threshold).
doctor-runner section 9 classifies every configured slot via
docker top <container> | grep Runner.Worker into exactly one of four
states:
State
Meaning
Defect?
EXECUTING
Runner.Worker process present, job running
No
IDLE-OK
listening; nothing queued, or queued < 5 min
No
IDLE-STARVED
listening; queue non-empty ≥ 5 min
Yes
DOWN
no running container
Yes
The GitHub runner API alone cannot be trusted for fleet state. Under
rate limiting it returns truncated/partial data — the same live fleet has
been reported as 7/11/16/19/22 across calls minutes apart. docker top /
docker ps on the host is the source of truth for per-slot state; never
conclude fleet health from API counts in isolation.
Live-deploy proof required before any "recovered" claim
A code fix merging upstream is not the same as it running (see the
"MANDATORY: post-merge activation check" section below and the user-scope
Runtime Activation Claim rule). Before claiming any fleet is "recovered" or
"healthy" after any remediation — restart, manual docker rm -f, service
restart, or a future recovery-controller action once PR #67/#70 ship — show
real command output for BOTH of the following:
Docker identity proof — confirm which Docker backend/context is
actually live and matches the intended one. This is exactly the class of
bug PR #67 fixes (dual-Lima drift to the wrong backend):
docker context show
docker info --format '{{.Name}} {{.ServerVersion}}'
docker ps --filter label=ezgha=managed --format '{{.Names}} {{.Image}} {{.Status}}'
Functional runner/job proof — a real job actually completing on the
fleet, not just a container existing or a registration showing:
docker top <container-name> | grep Runner.Worker # process-level proof it's executing# stronger:
./doctor-runner --prove # dispatches + verifies a live canary job
managed=N container count alone is insufficient — a container can
exist and even show registered while still being IDLE-STARVED or wedged
against a dead/wrong backend. Anything short of both (1) and (2) above is a
tool-layer claim ("container started", "systemctl restart exit 0"), not an
end-state recovery claim.
your-project.com scope: read-only consumer only
This repo (.claude/skills/ezgha-watchdog/) must only:
Read fleet state (ezgha status, docker ps, the GitHub runner API,
/runner-health output) to report AMBER/RED verdicts.
Surface the diagnose-first doctor-runner command to the operator.
Never implement its own backend recovery/drain/respawn-ordering logic.
That responsibility lives in jleechanorg/ez-gh-actions (PR #67, PR #70).
Any future change to this repo that adds new autonomous mutation logic
beyond a human-approved, last-resort restart is out of scope here and
should be redirected upstream to ez-gh-actions instead.
Hysteresis counters — consecutive below-target samples per arch, persisted across invocations since each 120s tick is a fresh process, not a long-lived loop
Each tick is one sample. The script only restarts the supervisor once the
consecutive below-target count exceeds 2 (i.e. on the 3rd consecutive
sample, ~4-6 minutes of continuous shortfall). The counter resets to 0 the
moment a sample is back at/above configured count. Override the state
directory with EZGHA_WATCHDOG_STATE_DIR (used by the test suite for
isolation).
This hysteresis threshold does not make the resulting restart safe — it
only reduces restart frequency, not the risk that a given restart kills an
active job or masks a wrong-backend defect. Treat every fired restart as a
signal to run doctor-runner afterward and confirm live-deploy proof (above)
before considering the shortfall resolved.
MANDATORY: post-merge activation check
A PR merging the watchdog script/templates is NOT the same as the watchdog
running. This is the exact failure this session hit: PR #8193 (the
watchdog fix) merged and passed all CI/codex//advice gates, but the
launchd job was never actually installed on the Mac host —
launchctl list | grep ezgha-watchdog returned nothing for ~19 hours after
merge, silently leaving the Mac fleet under-provisioned (4/6) the whole
time. The code/test layer was verified; the deployment/installation layer
was not (this is this repo's "Runtime Activation Claim" trap applied to an
operational daemon). The same discipline applies upstream: PR #67 and PR
#70 merging in ez-gh-actions is not proof they are running on the actual
Mac/Linux hosts — verify live deployment there too before relying on them.
Whenever a PR touches this skill's scripts/ or install/ files, the
close-out checklist is incomplete until BOTH of these are run and show real
output, not just "PR merged":
# Mac
launchctl list | grep ezgha-watchdog # must show a PID or "-" (loaded), NOT emptytail -5 /tmp/ezgha-watchdog.log # must show a recent timestamp (<5 min old)# jeff-ubuntu (Linux)
ssh jeff-ubuntu 'systemctl --user list-timers ezgha-watchdog*'# must show a NEXT/LAST fire time
ssh jeff-ubuntu 'tail -5 /tmp/ezgha-watchdog.log'
If either check comes back empty, install using the "Install on a new host"
section below — a merged PR is a necessary but not sufficient condition for
this being active.
How to invoke manually
# Both hosts, fix if needed
bash .claude/skills/ezgha-watchdog/scripts/ezgha-fleet-watchdog.sh
# Only one host
bash .../ezgha-fleet-watchdog.sh --host mac
bash .../ezgha-fleet-watchdog.sh --host linux
# Dry-run (report only, do not restart) — PREFER THIS until PR #67/#70 land
bash .../ezgha-fleet-watchdog.sh --dry-run
# View recent watchdog activitytail -n 20 /tmp/ezgha-watchdog.log
Exit codes
Code
Meaning
0
Both hosts at configured count
1
One or more hosts below count (fixed by restart, or dry-run reported it)
2
Cannot read state (ezgha missing, SSH timeout, config unreadable)
Install on a new host
Templates live in this skill's install/ dir:
org.jleechanorg.ezgha-watchdog.plist.template (macOS launchd) and
ezgha-watchdog.service + ezgha-watchdog.timer (Linux systemd --user).
Both use an @INSTALL_DIR@ placeholder for the absolute repo checkout path
(the macOS plist also has @HOME@ / @LOG_DIR@) — substitute with sed
before installing, same convention as
.claude/skills/runner-health/launchd/*.plist.template.
# 1. MacBook: install launchd plist from the templatecd .claude/skills/ezgha-watchdog/install
sed -e "s|@HOME@|$HOME|g" \
-e "s|@INSTALL_DIR@|$(cd ../../../.. && pwd)|g" \
-e "s|@LOG_DIR@|$HOME/Library/Logs|g" \
org.jleechanorg.ezgha-watchdog.plist.template \
> ~/Library/LaunchAgents/org.jleechanorg.ezgha-watchdog.plist
launchctl load ~/Library/LaunchAgents/org.jleechanorg.ezgha-watchdog.plist
# 2. Linux (jeff-ubuntu): install systemd --user timer from the templatescd .claude/skills/ezgha-watchdog/install
mkdir -p ~/.config/systemd/user
sed "s|@INSTALL_DIR@|$(cd ../../../.. && pwd)|g" ezgha-watchdog.service \
> ~/.config/systemd/user/ezgha-watchdog.service
sed "s|@INSTALL_DIR@|$(cd ../../../.. && pwd)|g" ezgha-watchdog.timer \
> ~/.config/systemd/user/ezgha-watchdog.timer
systemctl --user daemon-reload
systemctl --user enable --now ezgha-watchdog.timer
Troubleshooting
Symptom
Cause
Fix
Watchdog keeps restarting serve every 2 min
Supervisor fundamentally broken, or backend is dead/wrong-namespace (restart cannot fix this — see STATUS above)
Run doctor-runner first; check /tmp/ezgha-launchd-stderr.log (Mac) or journalctl --user -u ezgha (Linux); check Docker identity (docker context show) before assuming another restart will help
managed=0 for >5 min
Docker daemon dead OR container image missing
docker info + docker images | grep ezgha-runner
configured=N missing in log
Config file unreadable
cat ~/.config/ezgha/config.toml — should have count = N under [runner]
Linux check returns "cannot read state"
SSH key / alias broken
ssh -o ConnectTimeout=5 jeff-ubuntu 'echo ok'
Anti-patterns
Do NOT treat a restart as routine self-heal, and do NOT treat N/M below
configured count as "expected churn" without first running doctor-runner
— both were the exact false assumptions the 2026-07-09..11 audit disproved.
Do NOT claim a fleet is "recovered" without both Docker-identity proof and
functional runner/job proof (see "Live-deploy proof required" above).
Do NOT run ezgha start --count 1 to fill a single missing slot — the
supervisor's own slot state will conflict. Prefer diagnosis over any
restart while PR #67/#70 remain unmerged.
Do NOT lower count in config to "match reality" — the count is the
target, not the actual. Lowering it removes the alert.
Do NOT add a separate alerting Slack channel — /runner-health AMBER/RED
already names the failing arch with the diagnostic command inline.
Do NOT add backend recovery/drain/respawn logic to this repo — that
belongs in jleechanorg/ez-gh-actions (PR #67, PR #70). your-project.com
stays a read-only consumer of its health contract.