Tool-call relief: wait-on.sh, python, brief lessons, Life reads the mind #7
7 changed files with 196 additions and 19 deletions
3
SKILL.md
3
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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
92
tools/wait-on.sh
Executable 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
|
||||
Loading…
Reference in a new issue