diff --git a/tools/INDEX.md b/tools/INDEX.md index 5a5caf1..616bd28 100644 --- a/tools/INDEX.md +++ b/tools/INDEX.md @@ -9,6 +9,7 @@ gets a Soul pass before merge. | script | purpose | consumer | named by | |---|---|---|---| | `tools/stopgap/ygg-verify.sh` | Pushes an exact revision to a mirror on Yggdrasil and runs one command in a capped, niced container (`RUN_ON_HOST=1`: run a host-side command in the same slot, clone and watchdog instead; the watchdog kills every container carrying the job label). `SUMMARY=` keeps the full log and prints a bounded summary (verdict, counts, ZERO TESTS RAN, failing tests); `wait ` blocks at most 270 s and returns the summary once; call it with a Bash tool `timeout` of at least 280000 ms. Dies with the stopgap when Idunn's verify transaction lands. | Hands, Soul, Imagination probes | `references/briefs.md` (Hands long jobs and verification, Soul long jobs), `SKILL.md` (Yggdrasil) | +| `tools/wait-on.sh` | One general waiter for anything that is not a Yggdrasil job: `pid []`, `log ` (with `WAIT_PID=` a dead process with no match is exit 98), `file `. Same contract as `ygg-verify.sh wait`: blocks at most 270 s, exit 0 done with a tail of at most 60 log lines, `RUNNING s` exit 99, exit 98 gone with no completion evidence; call it with a Bash tool `timeout` of at least 280000 ms. Replaces hand-rolled poll loops and idle `true`/`echo`/`sleep` calls. | Hands, Soul, Imagination probes | `references/briefs.md` (Hands long jobs, Soul long jobs, detached scripts) | | `tools/mutation-table.sh` | One verify job that runs a list of hand mutants (file, before, after, test) against a revision and returns a kill table: CAUGHT with the killing test, SURVIVED, UNVIABLE, TIMEOUT, NOAPPLY, ERROR. Refuses a red or zero-test baseline and a red BUILD; rows print live. Isolation is Docker's: the baselines run once in a container that holds the revision, that container is committed to a throwaway image, and every mutant runs in a fresh `--rm` container from it, so nothing a run writes (tracked, untracked or ignored, such as `target/`) reaches the next mutant; the image is removed when the job ends, killed or not. The shared registry and sccache volumes and the network are not isolated. A file-level load failure (vitest `FAIL [ ]`, jest `FAIL ` plus `Test suite failed to run`) reads UNVIABLE. Replaces per-agent runmut/mutrun/strykrun scripts. | Soul, Hands (scoped run) | `references/briefs.md` (Hands verification, Soul reruns) | | `tools/testparse.sh` | Sourced library: failing test names and pass/fail/skip counts from cargo, dotnet, vitest, jest and pytest logs, and test files that failed to load (`LOADFAIL`). Shared by the summary and the kill table. | the two scripts above | not named by a brief; it lives and dies with them | | `tools/test-tools.sh` | Local fixture check of the parsers and the summary over `tools/fixtures/logs` (real cargo and mutation logs, plus hand-written dotnet, vitest, jest, pytest, exit-0-but-failed and no-verdict logs), and of the mutation runner's host driver and container half under a mock docker. Run it before merging a change to the scripts above. | whoever changes those scripts | `SKILL.md` (Shared tools) | diff --git a/tools/test-tools.sh b/tools/test-tools.sh index 401e0a8..12d0056 100644 --- a/tools/test-tools.sh +++ b/tools/test-tools.sh @@ -191,4 +191,31 @@ check "$(seg jest-real.log load)" 1 "FRAMEWORK jest passed=0 failed=0 skipped=0" check "$(seg jest-real.log exit)" 1 "exit non-zero with no failing test parsed" "FAILED process.exit" # F4: the "nothing recognised" line names every framework the parser reads check "$(seg jest-real.log exit)" 1 "no test counts recognised in the log (cargo, dotnet, vitest, jest, pytest)" + +# wait-on.sh: pid, log and file forms, and the vanished-process exit +W=$here/wait-on.sh +wo() { local rc; WAIT_MAX=${WM:-4} bash "$W" "$@" >"$T/wo.out" 2>&1; rc=$?; echo "$rc"; } +wchk() { if [ "$3" = "$2" ]; then echo "ok wait-on $1: exit $3"; else echo "FAIL wait-on $1: exit $3, want $2"; fail=1; fi; } +sleep 3 & sp=$!; printf 'one\ntwo\n' >"$T/w.log" +wchk "pid finishes" 0 "$(WM=10 wo pid $sp "$T/w.log")" +sleep 20 & sp=$! +r=$(WM=2 wo pid $sp); wchk "pid still running" 99 "$r" +grep -q '^RUNNING ' "$T/wo.out" || { echo "FAIL wait-on: no RUNNING line"; fail=1; } +kill $sp 2>/dev/null; wait $sp 2>/dev/null +r=$(WM=10 wo pid $sp "$T/w.log"); wchk "killed pid is done" 0 "$r" +grep -q two "$T/wo.out" || { echo "FAIL wait-on pid: no log tail"; fail=1; } +: >"$T/l.log"; ( sleep 2; echo "step 1" >>"$T/l.log"; echo "all done ok" >>"$T/l.log" ) & +r=$(WM=10 wo log "$T/l.log" 'done ok'); wchk "log gains its done line" 0 "$r" +grep -q 'all done ok' "$T/wo.out" || { echo "FAIL wait-on log: no tail"; fail=1; } +r=$(WM=2 wo log "$T/l.log" 'never-appears'); wchk "log never matches" 99 "$r" +printf 'partial\n' >"$T/v.log"; sleep 1 & vp=$!; wait $vp +r=$(WAIT_PID=$vp WM=10 wo log "$T/v.log" 'FINISHED'); wchk "vanished pid, no completion" 98 "$r" +printf 'FINISHED\n' >>"$T/v.log" +r=$(WAIT_PID=$vp WM=10 wo log "$T/v.log" 'FINISHED'); wchk "vanished pid, completion logged" 0 "$r" +r=$(WM=2 wo log "$T/none.log" 'x'); wchk "log missing, no pid, short cap" 99 "$r" +( sleep 2; echo ready >"$T/f.txt" ) & +r=$(WM=10 wo file "$T/f.txt"); wchk "file appears" 0 "$r" +: >"$T/e.txt"; r=$(WM=2 wo file "$T/e.txt"); wchk "file empty is not done" 99 "$r" +r=$(wo bogus); wchk "bad usage" 2 "$r" +wait exit $fail diff --git a/tools/wait-on.sh b/tools/wait-on.sh new file mode 100644 index 0000000..907abd8 --- /dev/null +++ b/tools/wait-on.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# One general waiter, so no agent hand-rolls a poll loop. Same contract as +# `ygg-verify.sh wait`: one call blocks at most 270 s (WAIT_MAX can only lower +# that), checks locally every 2 s (no model turn), and exits: +# 0 done; prints a bounded tail (at most 60 lines) of the log when one is given +# 99 still going; prints `RUNNING s (...)`: call it again +# 98 the thing waited on is gone with no completion evidence +# Call it with a Bash tool `timeout` of at least 280000 ms. +# +# Forms: +# wait-on.sh pid [] done when the process is gone (exit 0) +# wait-on.sh log done when a log line matches the ERE +# wait-on.sh file done when the file exists and is non-empty +# `log` accepts WAIT_PID=: if that process dies and the regex never matched +# (checked once more after the death), exit 98. Without WAIT_PID a log that does +# not exist after 30 s is exit 98 (never started). `pid` cannot tell a finished +# process from one that never existed: both are exit 0, so pass a log and read it. +# +# PIDs: on Git Bash a Windows PID (PowerShell Start-Process .Id, a task manager +# number) is seen by `tasklist`, but `kill -0` does not know it; Git Bash's own +# `$!` is known to `kill -0` and also to `tasklist`'s WINPID only by luck. Alive +# means either check says so. Linux uses `kill -0`. +set -uo pipefail +mode=${1:-}; cap=${WAIT_MAX:-270}; [ "$cap" -le 270 ] || cap=270 +t0=$(date +%s) +usage() { echo "usage: wait-on.sh pid [] | log | file " >&2; exit 2; } + +alive() { + local p=$1 + case "$p" in ''|*[!0-9]*) return 1 ;; esac + kill -0 "$p" 2>/dev/null && return 0 + case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) + tasklist //FI "PID eq $p" //NH 2>/dev/null | tr -d '\r' | grep -qE "[[:space:]]$p[[:space:]]" && return 0 ;; + esac + return 1 +} +tail60() { [ -n "${1:-}" ] && [ -f "$1" ] && tail -n 60 "$1" | cut -c1-400; return 0; } +running() { echo "RUNNING $(( $(date +%s) - t0 ))s ($1)"; exit 99; } +elapsed() { echo $(( $(date +%s) - t0 )); } + +case "$mode" in + pid) + pid=${2:-}; log=${3:-}; [ -n "$pid" ] || usage + while :; do + if ! alive "$pid"; then tail60 "$log"; exit 0; fi + [ "$(elapsed)" -lt "$cap" ] || running "pid $pid alive; last log line: $(tail -n 1 "$log" 2>/dev/null | cut -c1-160)" + sleep 2 + done ;; + log) + log=${2:-}; re=${3:-}; [ -n "$log" ] && [ -n "$re" ] || usage + wp=${WAIT_PID:-} + while :; do + if [ -f "$log" ] && grep -Eq -- "$re" "$log" 2>/dev/null; then tail60 "$log"; exit 0; fi + if [ -n "$wp" ] && ! alive "$wp"; then + sleep 1 + if [ -f "$log" ] && grep -Eq -- "$re" "$log" 2>/dev/null; then tail60 "$log"; exit 0; fi + echo "wait-on: DEAD - pid $wp is gone and /$re/ never matched in $log"; tail60 "$log"; exit 98 + fi + if [ -z "$wp" ] && [ ! -f "$log" ] && [ "$(elapsed)" -ge 30 ]; then + echo "wait-on: NOT STARTED - no $log after 30 s"; exit 98 + fi + [ "$(elapsed)" -lt "$cap" ] || running "no /$re/ yet; last line: $(tail -n 1 "$log" 2>/dev/null | cut -c1-160)" + sleep 2 + done ;; + file) + path=${2:-}; [ -n "$path" ] || usage + while :; do + [ -s "$path" ] && exit 0 + [ "$(elapsed)" -lt "$cap" ] || running "$path not there yet" + sleep 2 + done ;; + *) usage ;; +esac