forked from mosaicstack/stack
has_match/count_lines in _wake-common.sh: rc 0 -> match, rc 1 -> no-match, anything else -> loud abort naming the call site, the raw exit code, and the arguments — an error never becomes a verdict. The abort escapes subshells, pipelines, and $() substitutions by signalling the suite's main shell, so the suite dies verdict-less; loudness survives call-site 2>/dev/null via a wake_assert_init-saved stderr fd (dynamically allocated >= 10, no collision with the wake lock fd 8 or detector run-loop fd 9). Validate instrumentation (evidence only, inert in production): WAKE_ASSERT_LEDGER appends "<helper> <file>:<line>" BEFORE the grep runs, so an aborting site's row has already landed and the counted ledger can never go quiet in the one condition it exists to report. WAKE_ASSERT_FORCE_GREP_ERROR_AT=<file>:<line> routes exactly that call site through a REAL grep driven onto its real error path (rc 2) and emits a positive ARMED confirmation first, so arm-never-matched (no ARMED line) and error-path-broken (ARMED line, no abort) are separable on stderr alone. validate-973/microtest-wake-assert.sh: nine checks (C1..C9) proving the instrument before any conversion trusts it — ledger set equality across two files, truncation detection behind an early exit, per-shape abort proofs (subshell, 2>/dev/null canary shape, $() count capture, pipeline tail), count-of-zero on grep rc 1, env-prefix reaching the grep child, and a no-match arm loud by omission. All nine pass; C1 pins BASH_LINENO's first-physical-line convention for backslash continuations, matching the denominator artifact's coordinates. Production tools (store.sh, ack.sh) source this file but call none of the new helpers. Written-by: pepper (sb-it-1-dt) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NsKce8iZuSuRnu3gVMCBKB