Tool-call relief: wait-on.sh, python, brief lessons, Life reads the mind #7

Merged
eureka merged 9 commits from hands/toolcall-relief into main 2026-10-08 13:25:51 +00:00
7 changed files with 196 additions and 19 deletions

View file

@ -636,6 +636,9 @@ Scripts that know a project's layout stay in that project.
- **Before a merge, verify every test project that consumes the changed package, not only the package's
own.** CultMath Phacelle merged on CultMath's green suite (2026-09-30); Caching checks every public
CultMath value type for a converter, formatter and dictionary key, and main went red.
- **A merge of a long-lived branch can break semantically with no textual
conflict** (the LookDirection to Aim rename, 2026-10-07). Dispatch a merge Hands
that verifies and fixes mechanical adaptations only.
- **Pin sibling checkouts with a guard that checks revision and cleanliness.**
Scope the guard to projects that actually reference the sibling.

View file

@ -1,7 +1,7 @@
---
name: life
description: Maintains the agent's Mind — memory files, memory indexes, doctrine, Persona state, project maps and checkpoints. Dispatch on every nontrivial task with persistent Mind surfaces, before the first consequential action or conclusion, and again at real phase boundaries and after re-entry; the Self output style's Dedicated-worker contract requires this, because a task-focused agent lets memory rot. Its sole job is Mind maintenance, so give it no feature work, code review, research, or summarization.
tools: [Read, Write, Edit, Grep, Glob, Bash]
tools: [Read, Write, Edit, Grep, Glob, Bash, mcp__eureka-state__whoami, mcp__eureka-state__view, mcp__eureka-state__query]
model: sonnet
color: purple
---
@ -24,6 +24,10 @@ Life is continuity: keeping the Mind honest so the thread survives compaction, r
Self-improvement law (Epiphany `epiphany_doctrine.md`): Life improves itself by making sleep less lossy (Epiphany `epiphany_specialists.toml` at 71a5d6fb^, `[reorientation] resume`).
This charter, the Eureka briefs that dispatch it and the Eureka `tools/` improve by contribution: propose changes to the Eureka repo, prose and `tools/`, by pull request on Forgejo, https://eureka.gamecult.org/forge/GameCult/Eureka.
The typed mind (`eureka-state`) is yours to read, not to write: `whoami`, `view`
and `query` let you rehydrate before judging memory fitness. You have no `admit`;
Self and the faculties that own a record admit it.
## Refusals
Refuse and return immediately if asked to:

View file

@ -77,7 +77,9 @@ which workarounds are in force.
Rule: for each operator answer, a ruling (answers, choice, authority, and
operator_quote when the words matter). A direction that answers no question is
a ruling with no answers, authority Operator, and the words verbatim in
operator_quote. A changed ruling carries a resolution superseding the old one,
operator_quote. A ruling's operator_quote and text carry only what the operator
said; Self's reading goes in a question, or is clearly marked as Self's (three
supersedes on 2026-10-07 came from text she never said). A changed ruling carries a resolution superseding the old one,
in the same batch.
Triage each open finding: a Hands brief, then a Fixed resolution admitted by
@ -107,6 +109,8 @@ commits.
Rehydrate (recipes: target in force, rulings in force, open questions and
follow-ups). Read enough of the Body to enumerate the persistent kinds. Orient
and probe through scripts, one call per question, output at most 150 lines.
Wait only through `tools/wait-on.sh` (never `true`, `echo` or `sleep`), write only
under `<scratchpad>/<agent>/`, and use `py -3` for Python.
Give one table, a row per persistent kind:
- Identity: what names it. Is the namespace stated? Is the name injective? Can
@ -136,9 +140,11 @@ Admit only cut_specs, their superseding resolutions, and questions. In the
prose map at <path>, edit body facts and rationale. Also edit the model page
whenever a probe changes a row's identity, lifecycle or authority. Imagination
wrote that page in 0b, and a model a cut contradicts is stale steering text.
Do not commit; the root agent commits. Do not change code in any repo. Scratch probes in
<scratchpad> are fine. Orient and probe through scripts: one call per question,
output at most 150 lines. Budget 40-80 calls and about 250k context.
Do not commit; the root agent commits. Do not change code in any repo. Scratch probes under
<scratchpad>/<agent>/ are fine. Orient and probe through scripts: one call per question,
output at most 150 lines. Wait only through `tools/wait-on.sh` (never `true`,
`echo` or `sleep`), write only under `<scratchpad>/<agent>/`, and use `py -3` for
Python. Budget 40-80 calls and about 250k context.
Rehydrate (recipes: target in force, rulings in force, open questions and
follow-ups, specs with no report, one cut's record for <cuts that constrain
@ -255,7 +261,7 @@ rule; a later write can hide the deleted one.
When a claim is "behaviour unchanged", a value captured from the new code is not
evidence. Pin it with a value computed at the base commit.
Detached scripts: confirm the log starts within 60 s; a script that dies on a
parse error is silent otherwise.
parse error is silent otherwise. Wait on one with `tools/wait-on.sh`.
Shared build caches: record a full path list before building, not only counts,
and delete exactly the new paths afterwards. Counts cannot attribute hardlinked
or rewritten outputs.
@ -312,15 +318,43 @@ one head (SKILL.md, step 3); keep it fitting:
Long jobs: wait for them and finish, and never end your turn before your
report. Each turn end reaches Self as a notification and costs a read of its
whole context. Start the job as one background shell (run_in_background) with
`SUMMARY=<scratchpad>/<job>.log` in front of `tools/stopgap/ygg-verify.sh`, a
`SUMMARY=<scratchpad>/<agent>/<job>.log` in front of `tools/stopgap/ygg-verify.sh`, a
fresh log path per job. Then wait in the foreground, one call at a time:
`tools/stopgap/ygg-verify.sh wait <scratchpad>/<job>.log` blocks for at most
`tools/stopgap/ygg-verify.sh wait <scratchpad>/<agent>/<job>.log` blocks for at most
270 s (run it with a Bash tool `timeout` of at least 280000 ms: the default 120 s backgrounds the call and costs a wake), so your cache stays warm, and prints the bounded summary when the job is
done. If it prints `RUNNING <n>s` (exit 99), call it again. Exit 98 means the
launcher is gone with no verdict: rerun the job, do not guess its outcome. A call
longer than 5 minutes rewrites your whole context. Never run a second watcher
on the same job, never use a timer shorter than 60 s in a loop of your own, and
send no interim message.
Every other wait goes through `tools/wait-on.sh`, never a loop of your own.
Prefer `tools/wait-on.sh log <log> <done-regex>` with `WAIT_PID=<pid>` whenever
the job writes a done line (a build, a Unity batchmode run, a script that echoes
`DONE`): it is the only form that can tell success from a crash, because a dead
pid with no match is exit 98. `tools/wait-on.sh pid <pid> [<log>]` is only for a
process with no completion evidence; it prints `GONE (no completion evidence)`
and its exit 0 means "gone", not "succeeded". `tools/wait-on.sh file <path>` is
for an output file written last. The regex is an ERE; an invalid one is exit 3.
Exit 3 means the regex is invalid: fix it, never retry it unchanged.
Every job gets a fresh log path (put the job's own unique name or a timestamp in
it; for Unity, a new `-logFile` path per launch), and a waiter is never pointed
at a log a previous job wrote: a reused log's old done line reads as success at
once, and an offset cannot fix that because truncating writers (`>`, Unity
`-logFile`) rewrite below it. Same contract as the ygg-verify wait: pass a Bash tool `timeout`
of at least 280000 ms, it blocks at most 270 s, exit 0 is done with a bounded
log tail, `RUNNING <n>s` (exit 99) means call it again, exit 98 means the thing
is gone with no completion evidence. Bare `true`, `echo waiting` and `sleep`
calls are forbidden as polling: on 2026-10-07 and before, Hands and Soul runs
made 974 idle `true`/`echo` calls and 324 `sleep` calls (49 refused by the
harness), each one a model turn that waited on nothing.
Write only under your own scratchpad subdirectory, `<scratchpad>/<agent>/`:
a shared `job.sh` or probe file gets overwritten (one Soul clobbered `job.sh`
and `SoulProbe.cs`, 2026-10-07).
Python: use `py -3`, which works from Git Bash and PowerShell alike. Bare
`python` hit the Windows Store stub in 65 calls (2026-10-07).
Self pastes this wait protocol into a brief verbatim, never as a shortened
"wait mode": abbreviated, agents ended their turns mid-job three times on
2026-10-07.
Mutation tools run on schemata or copies and never edit the tree. A Soul
probe that mutates by hand works on a scratch copy: the stopgap's container
or a throwaway clone, never the working tree.
@ -350,6 +384,11 @@ Verification: <exact builds, tests, captures, negative greps>.
read the summary, and open the full log only at a named failing test, with
`grep -n -A 40`. Hand mutants go in one list to `tools/mutation-table.sh`,
one job for all of them.
- Stryker `--mutate` ranges are character offsets, not lines. Compute them from
the LF content (`git show <rev>:<path>`), never from the CRLF Windows checkout.
A run whose ranges hit nothing tests nothing, and a green with zero mutants
tested is not evidence (line-scoped runs tested nothing, 2026-10-06).
- Aetheria verify jobs set `DOCKER_ARGS='-v /home/gamecultadmin/eureka-verify/pins:/pins:ro -v /home/gamecultadmin/eureka-verify/lfs-aetheria:/lfs:ro'` (jobs that read sibling repos add `-v /home/gamecultadmin/eureka-verify/repos:/repos:ro`).
- Revert incidental asset churn.
Budget: if the cut can't land coherently, stop at a pushed, building commit
@ -396,14 +435,9 @@ below. Admit one verdict and its findings, and nothing else.
- Check the report's per-project test counts against the project files
yourself. A consuming test project missing from the report is a finding,
and you run it.
- Long jobs: wait for them and finish, and never end your turn before your
report. Run the job as one background shell with `SUMMARY=<scratchpad>/<job>.log`
in front of `tools/stopgap/ygg-verify.sh` (a fresh log path per job), and wait
in the foreground with `tools/stopgap/ygg-verify.sh wait <that log>`: pass a Bash tool `timeout` of at least 280000 ms (the default 120 s backgrounds the call and costs a wake); it blocks
at most 270 s and prints the bounded summary when the job is done, or `RUNNING
<n>s` (exit 99) and you call it again. Exit 98 means no verdict: rerun, do not
guess. No second watcher, no timer under 60 s in a loop of your own, and no
interim message: each turn end is a paid wake of Self.
- Long jobs and waits: Self pastes the Hands brief's `Long jobs` block here
verbatim (the canonical wait protocol, including the scratch subdirectory and
`py -3` rules); do not reword or shorten it.
- Bound every observation: no command prints more than about 150 lines, logs go
to files, and you read windows of them.
@ -451,7 +485,7 @@ specifically:
probes into one job script rather than one job each. Run the mutation tool
in parallel (`--jobs 3`), and skip by name any test that a follow-up records
as flaky, so that its flakes cannot count a surviving mutant as killed.
Dump the range once (`git diff <base>..<head> > <scratchpad>/range.diff` plus
Dump the range once (`git diff <base>..<head> > <scratchpad>/<agent>/range.diff` plus
`--stat`) and grep that file instead of re-running `git show` per file. Hand
mutants go in one list to `tools/mutation-table.sh`, never one job each.
@ -530,9 +564,10 @@ what was missing and what it would have prevented.
Dispatch: `subagent_type: eyes`, model Sonnet.
```
Write findings to <scratchpad>/<name>.md and return a
Write findings to <scratchpad>/<agent>/<name>.md and return a
short summary. Budget at most 40 calls: transcripts by script, never whole
reads, and fetches batched in parallel. You read the campaign's mind with query and view only, through
reads, waits only through `tools/wait-on.sh`, writes only under
`<scratchpad>/<agent>/`, Python as `py -3`, and fetches batched in parallel. You read the campaign's mind with query and view only, through
the Rehydrate block (recipes: <as needed>); you never admit.
Sources:

View file

@ -2,6 +2,14 @@
Record each change to the skill together with the evidence that motivated it.
## 2026-10-08: tool-call relief
Self dispatch `hands-toolcall-relief`. Operator, 2026-10-08: "I think Eureka needs some love. We have the forge set up but we're not using it to save tool calls yet. I think we can cut the number by a lot." Then, on the plan: "Dispatch all of those".
Evidence (Eyes, `eureka-toolcall-patterns-2026-10-08.md`, 211 Hands and Soul runs, 25,784 calls): 11% of all calls were waiting. 1,181 were `ygg-verify.sh wait`, 974 idle `true`/`echo waiting` calls, 324 `sleep` calls (49 refused by the harness), 287 process checks and 83 Monitor calls; 198 wait calls timed out. The idle streaks mostly followed waits that `ygg-verify.sh` does not cover: local processes, Unity batchmode, logs. 65 `python` calls hit the Windows Store stub. The 2026-10-07 runs also showed three brief gaps (shared scratch files clobbered, an abbreviated wait protocol, Stryker character offsets) and a Self ruling text that put words in the operator's mouth.
What changed. `tools/wait-on.sh` is one waiter (`pid`, `log` with a done regex, `file`) with `ygg-verify.sh wait`'s contract: at most 270 s, exit 0 with a bounded tail, exit 99 `RUNNING`, exit 98 for a thing gone with no completion evidence; `tools/test-tools.sh` has fixtures for it. `references/briefs.md` sends every non-Yggdrasil wait through it and forbids bare `true`/`echo`/`sleep` polling; names `py -3` as the Python that works from Git Bash and PowerShell; and carries the 2026-10-07 lessons (own scratchpad subdirectory, wait protocol pasted verbatim, Stryker offsets from LF content, Aetheria `DOCKER_ARGS` mounts, rulings quote only the operator). `SKILL.md` records the semantic-merge scar. `agents/life.md` gains the read-only mind tools (`whoami`, `view`, `query`) so Life can rehydrate; it still cannot admit.
## 2026-10-01: dispatches load the faculty charters
Campaign `eureka-substrate`, prose-map step 5 (map `references/faculty-philosophy-cut.md`, sections 5 and 8). Operator go: "go with 2 and 3; we're spending tokens to save tokens."

View file

@ -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=<log>` keeps the full log and prints a bounded summary (verdict, counts, ZERO TESTS RAN, failing tests); `wait <log>` 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 <pid> [<log>]`, `log <log> <done-regex>` (with `WAIT_PID=<pid>` a dead process with no match is exit 98; an invalid ERE is exit 3), `file <path>`. The `pid` form prints `GONE (no completion evidence)`: prefer `log` whenever the job writes a done line. 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 <n>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 <file> [ <file> ]`, jest `FAIL <file>` 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) |

View file

@ -191,4 +191,38 @@ 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"
# F2: the pid form reports GONE, never anything that reads as success
sleep 1 & gp=$!; wait $gp
r=$(WM=4 wo pid $gp); wchk "pid gone" 0 "$r"
grep -q '^GONE (no completion evidence)' "$T/wo.out" || { echo "FAIL wait-on pid: no GONE line"; fail=1; }
# F4: an invalid ERE is a usage error, not a forever RUNNING
r=$(WM=2 wo log "$T/l.log" '(unclosed'); wchk "invalid regex" 3 "$r"
grep -q 'not a valid extended regex' "$T/wo.out" || { echo "FAIL wait-on: no regex message"; fail=1; }
r=$(wo bogus); wchk "bad usage" 2 "$r"
wait
exit $fail

92
tools/wait-on.sh Executable file
View file

@ -0,0 +1,92 @@
#!/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 <n>s (...)`: call it again
# 98 the thing waited on is gone with no completion evidence
# 3 the done regex is not a valid ERE (usage error, nothing was waited on)
# 2 bad usage
# Call it with a Bash tool `timeout` of at least 280000 ms.
#
# Forms:
# wait-on.sh pid <pid> [<log>] process gone: prints `GONE (no completion evidence)`, exit 0
# wait-on.sh log <log> <done-regex> done when a log line matches the ERE
# wait-on.sh file <path> done when the file exists and is non-empty
# `log` accepts WAIT_PID=<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 or crashed: exit 0 there means only "gone",
# never "succeeded". Prefer `log <log> <regex>` with WAIT_PID whenever the job
# writes a done line.
#
# Log paths: every job gets a fresh log path, never one a previous job wrote. The
# waiter greps the whole file, so an old done line reads as success at once. An
# offset cannot fix it: truncating writers (`>`, Unity -logFile) rewrite below it.
#
# Known limitations (Soul pass 1, PR #7; not fixed):
# F5 WAIT_PID naming a launcher that exits while its child keeps writing gives a
# false 98; pass the pid that writes the log, or none.
# F6 `file` is done at the first byte; a writer that is still writing is not
# waited out. Have the job write the file last (rename into place).
# F7 `pid <pid> <log>` prints a tail only; the `log` form greps the
# whole log each 2 s, which is slow on multi-GB logs.
#
# 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 <pid> [<log>] | log <log> <done-regex> | file <path>" >&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 echo "GONE (no completion evidence)"; 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:-}
rc=0; printf '' | grep -Eq -- "$re" 2>/dev/null || rc=$?
[ "$rc" -le 1 ] || { echo "wait-on: usage - /$re/ is not a valid extended regex" >&2; exit 3; }
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