ruvnet-brain 4.7.18 → 4.7.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/configure.md +3 -3
- package/plugin/scripts/nightly-scheduler.mjs +29 -8
- package/plugin/scripts/session-start-core.mjs +3 -1
- package/plugin/scripts/session-start-issue-alert.mjs +42 -0
- package/scripts/codex-hook-trust-reconcile.mjs +126 -9
- package/scripts/codex-hook-trust.mjs +45 -0
- package/scripts/issue-watch.mjs +50 -9
- package/scripts/job-heartbeat.sh +107 -10
- package/scripts/nightly-gists.sh +4 -1
- package/scripts/ntfy-inbox.mjs +526 -0
- package/scripts/release-qualification-contract.mjs +9 -0
package/scripts/job-heartbeat.sh
CHANGED
|
@@ -81,7 +81,7 @@ PREV_HB=""
|
|
|
81
81
|
|
|
82
82
|
# Start receipt. If the job vanishes without ever writing an end receipt, THIS is the evidence that
|
|
83
83
|
# it started and never finished — a state the watchdog reports as FAILING, not as silence.
|
|
84
|
-
write_receipt "{\"label\":\"$LABEL\",\"started_at\":\"$STARTED\",\"state\":\"running\",\"pid\":$$,\"run_id\":\"$RUN_ID\",\"command\":\"$(
|
|
84
|
+
write_receipt "{\"label\":\"$LABEL\",\"started_at\":\"$STARTED\",\"state\":\"running\",\"pid\":$$,\"run_id\":\"$RUN_ID\",\"command\":\"$(printf '%s %s' "$1" "${2:-}" | tr -d '"\\\n\r' | cut -c1-200)\"}"
|
|
85
85
|
|
|
86
86
|
notify() { # notify <title> <body> <priority>
|
|
87
87
|
# NTFY_TOPIC set-but-EMPTY is an explicit opt-out (2026-07-18): unit tests wrap this script around
|
|
@@ -92,8 +92,10 @@ notify() { # notify <title> <body> <priority>
|
|
|
92
92
|
if [ "${NTFY_TOPIC+set}" = "set" ] && [ -z "$NTFY_TOPIC" ]; then return 0; fi
|
|
93
93
|
topic="${NTFY_TOPIC:-}"
|
|
94
94
|
[ -z "$topic" ] && [ -f "$HOME/.cache/ruvnet-brain/ntfy-topic" ] && topic="$(cat "$HOME/.cache/ruvnet-brain/ntfy-topic")"
|
|
95
|
-
[ -z "$topic" ] && return
|
|
96
|
-
|
|
95
|
+
[ -z "$topic" ] && return 1
|
|
96
|
+
# --fail: an HTTP error is a failed delivery. The caller records whether the page landed, so a page that
|
|
97
|
+
# never arrived is retried on the next run instead of muting the whole failing streak (review 2026-10-25).
|
|
98
|
+
curl -sS --fail -m 10 -H "Title: $1" -H "Priority: $3" -H "Tags: ${4:-rotating_light}" -d "$2" "https://ntfy.sh/$topic" >/dev/null 2>&1
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
# The run_id currently on disk for this label, or empty if the receipt is missing/unreadable/has
|
|
@@ -104,6 +106,17 @@ current_run_id() {
|
|
|
104
106
|
|
|
105
107
|
finish() {
|
|
106
108
|
code=${FORCED_CODE:-$?}
|
|
109
|
+
# Stop the run-time timer FIRST, on every exit path (skip, stale writer, normal), so it can never outlive
|
|
110
|
+
# this wrapper (Opus review 2026-10-25).
|
|
111
|
+
[ -n "${TIMER:-}" ] && kill "$TIMER" 2>/dev/null
|
|
112
|
+
timed_out=0
|
|
113
|
+
if [ -f "$HB.timeout-$RUN_ID" ]; then
|
|
114
|
+
# Anything the overrunning job left behind (started during its TERM handling, say) goes now.
|
|
115
|
+
timed_out=1; stop_job; rm -f "$HB.timeout-$RUN_ID"
|
|
116
|
+
fi
|
|
117
|
+
# An overrun is a failure however the job chose to exit on TERM, including 0 and 75 (the skip code), so this
|
|
118
|
+
# comes before the skip branch (GPT-6.1 review round 2).
|
|
119
|
+
[ "$timed_out" -eq 1 ] && [ "$code" -le 128 ] && code=143
|
|
107
120
|
ended="$(ts)"
|
|
108
121
|
dur=$(( $(date +%s) - START_EPOCH ))
|
|
109
122
|
# Exit 75 = SKIP (lock held by a live run). Restore the pre-fire receipt so the live run's evidence
|
|
@@ -132,15 +145,36 @@ finish() {
|
|
|
132
145
|
elif [ "$code" -gt 128 ]; then
|
|
133
146
|
sig=$((code - 128))
|
|
134
147
|
state="killed"; extra=",\"signal\":$sig"
|
|
148
|
+
[ "$timed_out" -eq 1 ] && extra="$extra,\"timed_out_after_sec\":$MAX_SECONDS"
|
|
135
149
|
else
|
|
136
150
|
state="failed"; extra=""
|
|
137
151
|
fi
|
|
138
|
-
|
|
139
|
-
#
|
|
140
|
-
#
|
|
152
|
+
# PAGE ON THE TRANSITION, NOT ON EVERY RUN (owner, 2026-10-25: 10,747 notifications on one topic). A job that
|
|
153
|
+
# starts failing pages once, urgently, while it is still tonight's problem; while it KEEPS failing it stays
|
|
154
|
+
# quiet (failing_since carries the first failure; the nightly watchdog's digest still lists it); the run
|
|
155
|
+
# that passes again sends one low-priority "recovered" note. The previous run's state comes from PREV_HB.
|
|
156
|
+
prev_state="$(printf '%s' "$PREV_HB" | sed -n 's/.*"state":"\([^"]*\)".*/\1/p' | head -1)"
|
|
157
|
+
prev_since="$(printf '%s' "$PREV_HB" | sed -n 's/.*"failing_since":"\([^"]*\)".*/\1/p' | head -1)"
|
|
158
|
+
was_failing=0; case "$prev_state" in failed|killed) was_failing=1 ;; esac
|
|
159
|
+
# A failing streak counts as already paged only if that page was delivered ("paged":false = retry it).
|
|
160
|
+
# Only a page PROVEN delivered ("paged":true) keeps the streak quiet; a receipt from before this field existed
|
|
161
|
+
# counts as unknown and is paged once more (review round 2).
|
|
162
|
+
prev_unpaged=1; case "$PREV_HB" in *'"paged":true'*) prev_unpaged=0 ;; esac
|
|
163
|
+
since=""
|
|
141
164
|
if [ "$code" -ne 0 ]; then
|
|
142
|
-
|
|
165
|
+
if [ "$was_failing" -eq 1 ] && [ -n "$prev_since" ]; then since="$prev_since"; else since="$STARTED"; fi
|
|
166
|
+
extra="$extra,\"failing_since\":\"$since\""
|
|
143
167
|
fi
|
|
168
|
+
if [ "$code" -ne 0 ] && { [ "$was_failing" -eq 0 ] || [ "$prev_unpaged" -eq 1 ]; }; then
|
|
169
|
+
reason="exit $code after ${dur}s"; [ "$timed_out" -eq 1 ] && reason="stopped after running past its ${MAX_SECONDS}s limit"
|
|
170
|
+
if notify "🔴 SCHEDULED JOB FAILED: $LABEL" "$reason — see the job's log. Receipt: $HB. Further failures stay quiet until it recovers." "urgent"; then
|
|
171
|
+
extra="$extra,\"paged\":true"; else extra="$extra,\"paged\":false"; fi
|
|
172
|
+
elif [ "$code" -ne 0 ]; then
|
|
173
|
+
extra="$extra,\"paged\":true"
|
|
174
|
+
elif [ "$was_failing" -eq 1 ]; then
|
|
175
|
+
notify "✅ Recovered: $LABEL" "passed again after failing since ${prev_since:-an earlier run}" "low" "white_check_mark" || true
|
|
176
|
+
fi
|
|
177
|
+
write_receipt "{\"label\":\"$LABEL\",\"started_at\":\"$STARTED\",\"ended_at\":\"$ended\",\"state\":\"$state\",\"exit_code\":$code,\"duration_sec\":$dur,\"run_id\":\"$RUN_ID\"$extra}"
|
|
144
178
|
exit "$code"
|
|
145
179
|
}
|
|
146
180
|
trap finish EXIT
|
|
@@ -148,8 +182,46 @@ trap finish EXIT
|
|
|
148
182
|
# The exit codes (143 = 128+15 TERM, 130 = 128+2 INT) intentionally reuse the same 128+signal
|
|
149
183
|
# convention `wait` uses for a directly-signalled child, so finish() classifies BOTH as "killed"
|
|
150
184
|
# with the right signal number, whether the signal reached the child directly or via this wrapper.
|
|
151
|
-
|
|
152
|
-
|
|
185
|
+
# The job stays in this wrapper's process group, so launchd's own cleanup of a dead job (it kills the job's whole
|
|
186
|
+
# group) still covers everything, even if this wrapper is SIGKILLed (Opus review round 3: a separate group escaped it).
|
|
187
|
+
# job_pids: every process the job owns. Under launchd this wrapper leads its group, so that is every group member
|
|
188
|
+
# except this wrapper, its timer, and this wrapper's own helper processes (ps/awk/command substitutions) — which
|
|
189
|
+
# catches processes the job started late or that were reparented. Run any other way (a terminal, a test harness),
|
|
190
|
+
# it falls back to the job's current process tree.
|
|
191
|
+
OWN_GROUP=0; [ "$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')" = "$$" ] && OWN_GROUP=1
|
|
192
|
+
job_pids() {
|
|
193
|
+
if [ "$OWN_GROUP" = 1 ]; then
|
|
194
|
+
ps -A -o pid= -o ppid= -o pgid= 2>/dev/null | awk -v me=$$ -v t="${TIMER:-0}" -v c="${CHILD:-0}" '
|
|
195
|
+
{ pid[NR]=$1; pp[NR]=$2; pg[NR]=$3 }
|
|
196
|
+
END { helper[t]=1
|
|
197
|
+
for (i=1;i<=NR;i++) if (pp[i]==me && pid[i]!=c) helper[pid[i]]=1
|
|
198
|
+
for (i=1;i<=NR;i++) if (pg[i]==me && pid[i]!=me && !(pid[i] in helper) && !(pp[i] in helper)) print pid[i] }'
|
|
199
|
+
else
|
|
200
|
+
tree_of "$CHILD"
|
|
201
|
+
fi
|
|
202
|
+
}
|
|
203
|
+
tree_of() { kill -0 "$1" 2>/dev/null && printf '%s ' "$1"
|
|
204
|
+
for k in $(ps -A -o pid= -o ppid= 2>/dev/null | awk -v p="$1" '$2==p {print $1}'); do tree_of "$k"; done; }
|
|
205
|
+
# stop_job: TERM everything the job owns, give it up to 10 s (inside launchd's 20 s exit allowance), then KILL.
|
|
206
|
+
# Every pid seen during the stop is remembered, so a descendant that outlives its parent (and so drops out of the
|
|
207
|
+
# process tree in the fallback mode) still gets the final KILL. The full guarantee, including processes started
|
|
208
|
+
# later, holds when this wrapper leads its group, as under launchd.
|
|
209
|
+
stop_job() {
|
|
210
|
+
seen="$(job_pids)"; [ -n "$seen" ] && kill -TERM $seen 2>/dev/null
|
|
211
|
+
i=0; while [ "$i" -lt 10 ]; do
|
|
212
|
+
now_pids="$(job_pids)"; seen="$seen $now_pids"
|
|
213
|
+
alive=""; for p in $seen; do kill -0 "$p" 2>/dev/null && alive="$alive $p"; done
|
|
214
|
+
[ -z "$alive" ] && return 0
|
|
215
|
+
sleep 1; i=$((i + 1))
|
|
216
|
+
done
|
|
217
|
+
for p in $seen $(job_pids); do kill -KILL "$p" 2>/dev/null; done
|
|
218
|
+
return 0
|
|
219
|
+
}
|
|
220
|
+
trap 'FORCED_CODE=143; [ -n "${TIMER:-}" ] && kill "$TIMER" 2>/dev/null; stop_job; exit 143' TERM
|
|
221
|
+
trap 'FORCED_CODE=130; [ -n "${TIMER:-}" ] && kill "$TIMER" 2>/dev/null; stop_job; exit 130' INT
|
|
222
|
+
# The timer never kills anything itself: it marks the overrun and signals this wrapper, which stops the job.
|
|
223
|
+
OVERRUN=0
|
|
224
|
+
trap 'OVERRUN=1' USR1
|
|
153
225
|
|
|
154
226
|
# Run the job in the BACKGROUND and `wait` for it — do NOT run it in the foreground.
|
|
155
227
|
# Break-test finding (2026-07-13): a POSIX shell blocked on a FOREGROUND child does not run its trap
|
|
@@ -160,4 +232,29 @@ trap 'FORCED_CODE=130; kill -TERM "$CHILD" 2>/dev/null; exit 130' INT
|
|
|
160
232
|
# ("started and never finished"). Trap for catchable deaths, watchdog for uncatchable ones.
|
|
161
233
|
"$@" &
|
|
162
234
|
CHILD=$!
|
|
163
|
-
|
|
235
|
+
# RUN-TIME LIMIT (2026-10-25: five jobs finished their work and then never exited, each holding its own
|
|
236
|
+
# next run off for 7-9 hours). After JOB_MAX_SECONDS (default 2 h; a plist may set less) the job is
|
|
237
|
+
# stopped: TERM, then KILL 10 s later. It is recorded as killed and pages once like any other failure.
|
|
238
|
+
MAX_SECONDS="${JOB_MAX_SECONDS:-7200}"
|
|
239
|
+
WRAPPER_PID=$$
|
|
240
|
+
case "$MAX_SECONDS" in ''|*[!0-9]*) MAX_SECONDS=7200 ;; esac
|
|
241
|
+
[ "$MAX_SECONDS" -lt 2 ] && MAX_SECONDS=2
|
|
242
|
+
(
|
|
243
|
+
# Short sleeps, not one long one: killing this shell then leaves at most a 2 s sleep behind, never a 2 h one
|
|
244
|
+
# holding the job's output open (review round 2).
|
|
245
|
+
# A wrapper that is gone (SIGKILLed: launchd then cleans the group itself) ends the timer at the next tick.
|
|
246
|
+
n=0; while [ "$n" -lt "$MAX_SECONDS" ]; do sleep 2; n=$((n + 2)); kill -0 "$WRAPPER_PID" 2>/dev/null || exit 0; done
|
|
247
|
+
# Only for OUR child: still alive and still parented by this wrapper. If the PID was reused, do nothing.
|
|
248
|
+
[ "$(ps -o ppid= -p "$CHILD" 2>/dev/null | tr -d ' ')" = "$WRAPPER_PID" ] || exit 0
|
|
249
|
+
: > "$HB.timeout-$RUN_ID"
|
|
250
|
+
kill -USR1 "$WRAPPER_PID" 2>/dev/null
|
|
251
|
+
) </dev/null >/dev/null 2>&1 &
|
|
252
|
+
TIMER=$!
|
|
253
|
+
# `wait` returns early when USR1 arrives; stop the job, then wait again for its real exit status.
|
|
254
|
+
while :; do
|
|
255
|
+
if [ "$OVERRUN" = 1 ]; then OVERRUN=0; stop_job; fi
|
|
256
|
+
wait "$CHILD"; RC=$?
|
|
257
|
+
if [ "$OVERRUN" = 1 ]; then OVERRUN=0; stop_job; continue; fi
|
|
258
|
+
break
|
|
259
|
+
done
|
|
260
|
+
exit "$RC"
|
package/scripts/nightly-gists.sh
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
set -eu
|
|
18
18
|
|
|
19
19
|
cd "$(dirname "$0")/.."
|
|
20
|
-
|
|
20
|
+
# The owner's macOS-approved node (~/.local/share/ruvnet-brain/job-bin) and Apple's tools come first: this repo and
|
|
21
|
+
# the model cache live on an external volume, and launchd-started Homebrew node is held at its first read there
|
|
22
|
+
# (2026-10-25: every embedding shard "stalled" for 15 min, then the run failed after 1 h 49 min).
|
|
23
|
+
export PATH="$HOME/.local/share/ruvnet-brain/job-bin:/usr/bin:/bin:/opt/homebrew/bin:/usr/local/bin"
|
|
21
24
|
export KB_MODEL_CACHE="${KB_MODEL_CACHE:-/Users/stuartkerr/Code/PowerPlatePulse/scripts/models-cache}"
|
|
22
25
|
|
|
23
26
|
mkdir -p logs
|