From a886d4524aed2a68d634c15cd315f7e6a213fba6 Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 8 Oct 2026 14:15:45 +0200 Subject: [PATCH 1/9] Add tools/wait-on.sh: one waiter for pids, logs and files Same contract as ygg-verify.sh wait (270 s cap, bounded tail, RUNNING exit 99, exit 98 for a thing gone with no completion evidence). Windows PIDs are found through tasklist, since kill -0 only knows Git Bash's own. Fixtures in tools/test-tools.sh; INDEX row added. Co-Authored-By: Claude Opus 5.5 --- tools/INDEX.md | 1 + tools/test-tools.sh | 27 +++++++++++++++++ tools/wait-on.sh | 74 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 tools/wait-on.sh 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 -- 2.47.3 From 18b160c44fe11056a741be93a86143ebba25b4ff Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 8 Oct 2026 14:15:48 +0200 Subject: [PATCH 2/9] Briefs: wait through wait-on.sh, py -3, and the 2026-10-07 lessons Hands and Soul long jobs name tools/wait-on.sh for every non-Yggdrasil wait and forbid bare true/echo/sleep polling (974 idle calls, 324 sleeps). py -3 is the Python that works from Git Bash and PowerShell (65 Store-stub hits). Lessons: own scratchpad subdirectory, wait protocol pasted verbatim, Stryker character offsets from LF content, Aetheria DOCKER_ARGS mounts, rulings quote only the operator. Co-Authored-By: Claude Opus 5.5 --- references/briefs.md | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/references/briefs.md b/references/briefs.md index 3005a46..fc57a5f 100644 --- a/references/briefs.md +++ b/references/briefs.md @@ -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 @@ -255,7 +257,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. @@ -321,6 +323,25 @@ launcher is gone with no verdict: rerun the job, do not guess its outcome. A cal 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: +`wait-on.sh pid []` for a detached process, `wait-on.sh log +` (with `WAIT_PID=` to notice a dead process) for a build, a +Unity batchmode run or a script that writes a done line, `wait-on.sh file +` for an output file. It has the same contract: 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 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, `//`: +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 +371,11 @@ Verification: . 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 :`), 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` to mount `/pins` and `/lfs-aetheria`. - Revert incidental asset churn. Budget: if the cut can't land coherently, stop at a pushed, building commit @@ -403,7 +429,14 @@ below. Admit one verdict and its findings, and nothing else. at most 270 s and prints the bounded summary when the job is done, or `RUNNING 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. + interim message: each turn end is a paid wake of Self. Every wait that is not + a Yggdrasil job goes through `tools/wait-on.sh` (`pid []`, + `log `, `file `; same 280000 ms timeout rule, same + `RUNNING`/exit 99 and exit 98 contract); bare `true`, `echo` or `sleep` + polling is forbidden. Write only under `//`: a shared + `job.sh` or probe file gets overwritten (one Soul clobbered `job.sh` and + `SoulProbe.cs`, 2026-10-07). Python is `py -3` (works from Git Bash and + PowerShell; bare `python` hit the Windows Store stub in 65 calls, 2026-10-07). - Bound every observation: no command prints more than about 150 lines, logs go to files, and you read windows of them. -- 2.47.3 From ebc746e93705245cbc9d35426b22ec63cb4d15df Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 8 Oct 2026 14:15:51 +0200 Subject: [PATCH 3/9] SKILL: a long-lived branch merge can break semantically with no conflict Co-Authored-By: Claude Opus 5.5 --- SKILL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SKILL.md b/SKILL.md index ceae7f2..91dae7b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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. -- 2.47.3 From c28d92815089b55bec1aca0922017f32fd4eae81 Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 8 Oct 2026 14:15:53 +0200 Subject: [PATCH 4/9] Life: read-only mind tools so it can rehydrate whoami, view and query; no admit. Follow-up eureka-substrate:follow_up:gap-life-has-no-mind-tools. Co-Authored-By: Claude Opus 5.5 --- agents/life.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/agents/life.md b/agents/life.md index 6cf7237..9dd72e7 100644 --- a/agents/life.md +++ b/agents/life.md @@ -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: -- 2.47.3 From df590aa2c72332ba7c78a551e7334f2a5ed4594c Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 8 Oct 2026 14:15:55 +0200 Subject: [PATCH 5/9] Changelog: tool-call relief Co-Authored-By: Claude Opus 5.5 --- references/changelog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/references/changelog.md b/references/changelog.md index 703e391..dae4dbb 100644 --- a/references/changelog.md +++ b/references/changelog.md @@ -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." -- 2.47.3 From 30e199cf551c42c622f9c8f0dda809f7cb1a0ac7 Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 8 Oct 2026 15:04:20 +0200 Subject: [PATCH 6/9] wait-on: WAIT_FROM start marker, GONE line for pid form, regex validation, executable bit F1: log form searches only bytes after WAIT_FROM, so a stale done line in a reused log is ignored (also makes the per-check cost independent of log size). F2: pid form prints GONE (no completion evidence). F4: invalid ERE is exit 3. Mode 100755 recorded for wait-on.sh; other tools/ scripts are still 100644. F5, F6, F7 recorded as known limitations in the header. Fixtures added. Co-Authored-By: Claude Opus 5.5 --- tools/INDEX.md | 2 +- tools/test-tools.sh | 19 +++++++++++++++++++ tools/wait-on.sh | 42 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 57 insertions(+), 6 deletions(-) mode change 100644 => 100755 tools/wait-on.sh diff --git a/tools/INDEX.md b/tools/INDEX.md index 616bd28..1cf2c0e 100644 --- a/tools/INDEX.md +++ b/tools/INDEX.md @@ -9,7 +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/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; `WAIT_FROM=` ignores a reused log's stale lines; an invalid ERE is exit 3), `file `. 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 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 12d0056..856fc75 100644 --- a/tools/test-tools.sh +++ b/tools/test-tools.sh @@ -216,6 +216,25 @@ 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" +# F1: a stale done line in a reused log is ignored from WAIT_FROM on; a new one is seen +printf 'old run +ALL DONE +' >"$T/r.log"; off=$(wc -c <"$T/r.log") +r=$(WAIT_FROM=$off WM=2 wo log "$T/r.log" 'ALL DONE'); wchk "stale done before WAIT_FROM ignored" 99 "$r" +printf 'new run +ALL DONE +' >>"$T/r.log" +r=$(WAIT_FROM=$off WM=4 wo log "$T/r.log" 'ALL DONE'); wchk "new done after WAIT_FROM seen" 0 "$r" +printf 'short +' >"$T/r2.log" +r=$(WAIT_FROM=9999 WM=2 wo log "$T/r2.log" 'short'); wchk "truncated log searched whole" 0 "$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 diff --git a/tools/wait-on.sh b/tools/wait-on.sh old mode 100644 new mode 100755 index 907abd8..50852f6 --- a/tools/wait-on.sh +++ b/tools/wait-on.sh @@ -5,16 +5,35 @@ # 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 +# 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 [] done when the process is gone (exit 0) +# wait-on.sh pid [] process gone: prints `GONE (no completion evidence)`, 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. +# process from one that never existed or crashed: exit 0 there means only "gone", +# never "succeeded". Prefer `log ` with WAIT_PID whenever the job +# writes a done line. +# +# Reused log paths: `log` accepts WAIT_FROM=; only bytes after it are +# searched, so a stale done line from an earlier job on the same path is ignored. +# Capture it at launch: `WAIT_FROM=$(wc -c < "$log" 2>/dev/null || echo 0)`. If the +# file is now shorter than WAIT_FROM (truncated and rewritten) the whole file is +# searched. Reading only from the offset also keeps each 2 s check cheap on a +# multi-GB log. Without WAIT_FROM the whole log is searched. +# +# 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 ` prints a tail only; without WAIT_FROM 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 @@ -35,6 +54,17 @@ alive() { esac return 1 } +# scan : does the log (from WAIT_FROM, when set and not past EOF) match? +scan() { + [ -f "$1" ] || return 1 + local from=${WAIT_FROM:-0} + case "$from" in ''|*[!0-9]*) from=0 ;; esac + if [ "$from" -gt 0 ] && [ "$(wc -c <"$1")" -ge "$from" ]; then + tail -c +"$((from + 1))" "$1" 2>/dev/null | grep -Eq -- "$2" + else + grep -Eq -- "$2" "$1" 2>/dev/null + fi +} 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 )); } @@ -43,18 +73,20 @@ case "$mode" in pid) pid=${2:-}; log=${3:-}; [ -n "$pid" ] || usage while :; do - if ! alive "$pid"; then tail60 "$log"; exit 0; fi + 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 scan "$log" "$re"; 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 + if scan "$log" "$re"; 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 -- 2.47.3 From de35a72fc3e9be8768b60c9b0d892e5f6ddc3128 Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 8 Oct 2026 15:04:22 +0200 Subject: [PATCH 7/9] briefs: one canonical wait block, fresh/offset log paths, pid-form caveat, consistent scratch paths F1/F2: Hands block says prefer log+WAIT_PID, pid form means gone not succeeded, fresh log per job or WAIT_FROM at launch. F3: exact Aetheria DOCKER_ARGS mounts. F8: Soul points at the Hands block instead of rewording it; Imagination and Eyes templates name wait-on.sh, // and py -3; all examples use //. Co-Authored-By: Claude Opus 5.5 --- references/briefs.md | 71 ++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/references/briefs.md b/references/briefs.md index fc57a5f..a3664c0 100644 --- a/references/briefs.md +++ b/references/briefs.md @@ -109,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 `//`, 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 @@ -138,9 +140,11 @@ Admit only cut_specs, their superseding resolutions, and questions. In the prose map at , 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 - 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 +// 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 `//`, 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 /.log` in front of `tools/stopgap/ygg-verify.sh`, a +`SUMMARY=//.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 /.log` blocks for at most +`tools/stopgap/ygg-verify.sh wait //.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 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: -`wait-on.sh pid []` for a detached process, `wait-on.sh log -` (with `WAIT_PID=` to notice a dead process) for a build, a -Unity batchmode run or a script that writes a done line, `wait-on.sh file -` for an output file. It has the same contract: 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 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. +Every other wait goes through `tools/wait-on.sh`, never a loop of your own. +Prefer `tools/wait-on.sh log ` with `WAIT_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 []` 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 ` is +for an output file written last. The regex is an ERE; an invalid one is exit 3. +Use a fresh log path per job. If a log path is reused (Unity `-logFile`, a +re-run script), record where it ended at launch and pass it, so a stale done +line from the earlier job does not count: `WAIT_FROM=$(wc -c < 2>/dev/null +|| echo 0)` captured before the launch, then `WAIT_FROM= tools/wait-on.sh log + `. 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 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, `//`: a shared `job.sh` or probe file gets overwritten (one Soul clobbered `job.sh` and `SoulProbe.cs`, 2026-10-07). @@ -375,7 +387,7 @@ Verification: . the LF content (`git show :`), 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` to mount `/pins` and `/lfs-aetheria`. +- 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 @@ -422,21 +434,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=/.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 `: 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 - 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. Every wait that is not - a Yggdrasil job goes through `tools/wait-on.sh` (`pid []`, - `log `, `file `; same 280000 ms timeout rule, same - `RUNNING`/exit 99 and exit 98 contract); bare `true`, `echo` or `sleep` - polling is forbidden. Write only under `//`: a shared - `job.sh` or probe file gets overwritten (one Soul clobbered `job.sh` and - `SoulProbe.cs`, 2026-10-07). Python is `py -3` (works from Git Bash and - PowerShell; bare `python` hit the Windows Store stub in 65 calls, 2026-10-07). +- 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. @@ -484,7 +484,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 .. > /range.diff` plus + Dump the range once (`git diff .. > //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. @@ -563,9 +563,10 @@ what was missing and what it would have prevented. Dispatch: `subagent_type: eyes`, model Sonnet. ``` -Write findings to /.md and return a +Write findings to //.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 +`//`, 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: ); you never admit. Sources: -- 2.47.3 From d9e45c8740d38464f246637fbd32a6cff08bfb65 Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 8 Oct 2026 15:21:58 +0200 Subject: [PATCH 8/9] wait-on: delete WAIT_FROM; reused log paths are forbidden instead Truncating writers (`>`, Unity -logFile) rewrite below any offset, so an offset turned a stale success into a silent hang. The option, the scan branch, its docs and its fixtures are gone; the header states the fresh-log-path rule. Co-Authored-By: Claude Opus 5.5 --- tools/INDEX.md | 2 +- tools/test-tools.sh | 12 ------------ tools/wait-on.sh | 28 +++++++--------------------- 3 files changed, 8 insertions(+), 34 deletions(-) diff --git a/tools/INDEX.md b/tools/INDEX.md index 1cf2c0e..9e0f8ab 100644 --- a/tools/INDEX.md +++ b/tools/INDEX.md @@ -9,7 +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; `WAIT_FROM=` ignores a reused log's stale lines; an invalid ERE is exit 3), `file `. 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 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/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; an invalid ERE is exit 3), `file `. 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 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 856fc75..92b6334 100644 --- a/tools/test-tools.sh +++ b/tools/test-tools.sh @@ -216,18 +216,6 @@ 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" -# F1: a stale done line in a reused log is ignored from WAIT_FROM on; a new one is seen -printf 'old run -ALL DONE -' >"$T/r.log"; off=$(wc -c <"$T/r.log") -r=$(WAIT_FROM=$off WM=2 wo log "$T/r.log" 'ALL DONE'); wchk "stale done before WAIT_FROM ignored" 99 "$r" -printf 'new run -ALL DONE -' >>"$T/r.log" -r=$(WAIT_FROM=$off WM=4 wo log "$T/r.log" 'ALL DONE'); wchk "new done after WAIT_FROM seen" 0 "$r" -printf 'short -' >"$T/r2.log" -r=$(WAIT_FROM=9999 WM=2 wo log "$T/r2.log" 'short'); wchk "truncated log searched whole" 0 "$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" diff --git a/tools/wait-on.sh b/tools/wait-on.sh index 50852f6..fd46aaf 100755 --- a/tools/wait-on.sh +++ b/tools/wait-on.sh @@ -20,20 +20,17 @@ # never "succeeded". Prefer `log ` with WAIT_PID whenever the job # writes a done line. # -# Reused log paths: `log` accepts WAIT_FROM=; only bytes after it are -# searched, so a stale done line from an earlier job on the same path is ignored. -# Capture it at launch: `WAIT_FROM=$(wc -c < "$log" 2>/dev/null || echo 0)`. If the -# file is now shorter than WAIT_FROM (truncated and rewritten) the whole file is -# searched. Reading only from the offset also keeps each 2 s check cheap on a -# multi-GB log. Without WAIT_FROM the whole log is searched. +# 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 ` prints a tail only; without WAIT_FROM the `log` form -# greps the whole log each 2 s, which is slow on multi-GB logs. +# F7 `pid ` 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 @@ -54,17 +51,6 @@ alive() { esac return 1 } -# scan : does the log (from WAIT_FROM, when set and not past EOF) match? -scan() { - [ -f "$1" ] || return 1 - local from=${WAIT_FROM:-0} - case "$from" in ''|*[!0-9]*) from=0 ;; esac - if [ "$from" -gt 0 ] && [ "$(wc -c <"$1")" -ge "$from" ]; then - tail -c +"$((from + 1))" "$1" 2>/dev/null | grep -Eq -- "$2" - else - grep -Eq -- "$2" "$1" 2>/dev/null - fi -} 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 )); } @@ -83,10 +69,10 @@ case "$mode" in 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 scan "$log" "$re"; then tail60 "$log"; exit 0; fi + 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 scan "$log" "$re"; then tail60 "$log"; exit 0; fi + 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 -- 2.47.3 From 6ab91ead9cb3d9d501566c5e5f1e98ceec1c2fdd Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 8 Oct 2026 15:22:00 +0200 Subject: [PATCH 9/9] briefs: canonical wait block drops WAIT_FROM, states fresh log path and exit 3 Co-Authored-By: Claude Opus 5.5 --- references/briefs.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/references/briefs.md b/references/briefs.md index a3664c0..1181190 100644 --- a/references/briefs.md +++ b/references/briefs.md @@ -335,11 +335,12 @@ pid with no match is exit 98. `tools/wait-on.sh pid []` 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 ` is for an output file written last. The regex is an ERE; an invalid one is exit 3. -Use a fresh log path per job. If a log path is reused (Unity `-logFile`, a -re-run script), record where it ended at launch and pass it, so a stale done -line from the earlier job does not count: `WAIT_FROM=$(wc -c < 2>/dev/null -|| echo 0)` captured before the launch, then `WAIT_FROM= tools/wait-on.sh log - `. Same contract as the ygg-verify wait: pass a Bash tool `timeout` +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 s` (exit 99) means call it again, exit 98 means the thing is gone with no completion evidence. Bare `true`, `echo waiting` and `sleep` -- 2.47.3