Emit-on-event health watcher for a long paired/ab benchmark run, meant to be fed to the Monitor tool (each stdout line becomes one notification) so an unattended 75-min run cannot fail silently. Coverage-first by design — it speaks up on liveness (ALIVE), on new error signatures in the proxy/tee logs (ERROR), on a stall or crash (STALL/DEAD — silence is not success), and on completion (COMPLETE) — not just the happy path. Stall is tracked on the SHADOW (A0) turn-log, which is passthrough with...
Installs into .claude/skills of the current project.
Are you the author of Watch Run?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/zapgun-ai-watch-run)
---
name: watch-run
description: Emit-on-event health watcher for a long paired/ab benchmark run, meant to be fed to the Monitor tool (each stdout line becomes one notification) so an unattended 75-min run cannot fail silently. Coverage-first by design — it speaks up on liveness (ALIVE), on new error signatures in the proxy/tee logs (ERROR), on a stall or crash (STALL/DEAD — silence is not success), and on completion (COMPLETE) — not just the happy path. Stall is tracked on the SHADOW (A0) turn-log, which is passthrough with keep-alive OFF so every line is a real turn; the PRIMARY (A5) log also grows from keep-alive pings during idle gaps and would mask a stalled conversation, so it must not drive stall detection. Use it to babysit `.skills/paired` or `.skills/ab` runs whose wall clock exceeds the Monitor's 60-min non-persistent cap (run it persistent).
---
# clawback run health watcher
`.skills/watch-run/scripts/watch_run.sh <out-dir> [listen-port primary-port shadow-port]`
Defaults: ports `8788` (tee/listen) `8790` (primary) `8791` (shadow) — the
`.skills/paired` defaults. Pass the run's `--out` dir as the first arg.
## How to use (with Monitor)
Launch the run in the background first (e.g. `.skills/scripts/run_paired.sh … --out
runs/paired-haiku-L1`), then attach this under the **Monitor** tool with
`persistent: true` (a 75-min run exceeds Monitor's 60-min non-persistent
timeout). The watcher exits on its own at COMPLETE/DEAD/WATCH-END, which ends
the Monitor cleanly; TaskStop it early if you abort the run.
```
Monitor(persistent=true,
command=".skills/watch-run/scripts/watch_run.sh runs/paired-haiku-L1 8788 8790 8791")
```
## Events it emits (one line = one notification)
- `ALIVE a0=.. a5=..` — first turns observed; the run came up.
- `ERROR(+n) … :: <last matching line>` — n new error-signature lines in
`proxy.primary.log` / `proxy.shadow.log` / `tee.log` (overload, 429,
EADDRINUSE/ECONN*, `→ 4xx/5xx`, Traceback, force-exit, `[error]`, …).
- `STALL …` — no new SHADOW turn for 12 min but a proxy is still listening
(warned once; the conversation is wedged, processes alive).
- `DEAD …` — no new SHADOW turn for 12 min AND no proxy listening → the run
crashed; exits 1.
- `COMPLETE a0=.. a5=.. (+Nm)` — `report.md` present (analyzer finished);
exits 0.
- `WATCH-END` — ~95-min wall-clock backstop tripped; exits 2.
## Why shadow-tracked stall (the subtle bit)
On a paired run the PRIMARY arm (A5 stack) fires keep-alive **pings** during
idle gaps, each appended to `instance.A5.ndjson`. So A5's line count rises even
when no real turn happens — tracking it for staleness would hide a stalled
conversation behind ping traffic. The SHADOW (A0) is `--passthrough` with
keep-alive off, so its line count == real-turn count: the honest liveness
signal. `ALIVE` still fires on either arm (proxies-up signal); only stall/crash
detection keys on A0.
## Tuning
Both are **env vars** (soft default + knob), so tune them per profile without
editing the script — pass them on the Monitor command line:
```
STALL_SEC=2400 .skills/watch-run/scripts/watch_run.sh runs/paired-haiku-L2 8788 8790 8791
```
- `STALL_SEC` (env, default 720s ≈ 12 min) — raise for L2+ where long gaps are
normal: `STALL_SEC=2400` for L2 (up-to-30-min gaps), `~6000` for L3 (90-min).
Set it above the profile's max gap + response slack. Too-low only emits a
STALL warning; it never causes a false DEAD (that requires no proxy
listening), so erring high is safe.
- `HARD_MAX_SEC` (env, default 5700s ≈ 95 min) — raise for runs capped longer
than 75 min.
## Self-check (error detection is the fragile part)
The error-signature pipeline misfired twice in production — a startup crash
(`grep -c`'s print-0-and-exit-1 double-counted into `0\n0: syntax error`) and a
false ERROR on a healthy `.429Z` keep-alive ping (a bare `429` matched the
timestamp's milliseconds). Both are fixed and the detection now lives in one
tested function reachable via a hidden entry point:
.skills/watch-run/scripts/watch_run.sh --errcount <logfile> # prints the error count for one file
`test/watch_run.test.js` pins the must-NOT-cry-wolf cases (the `.429Z` ping, the
`→ 404 [no-route]` probe, a 4-digit `→ 4096` count) and the must-NOT-go-blind
cases (`→ 529`, `→ 5xx`, `EADDRINUSE`, `Traceback`, `[error]`). After touching
`ERRRE`/`IGNORE`, run `npm test -- watch_run`.