diff --git a/.claude/verify.sh b/.claude/verify.sh index 9351158..cd7c9b1 100755 --- a/.claude/verify.sh +++ b/.claude/verify.sh @@ -4440,7 +4440,35 @@ c61_mkrepo(){ # chmod +x "$1/.claude/verify.sh" } c61_store(){ printf '%s' "$1/home/.config/agents/verify-trust"; } -c61_trust(){ HOME="$1/home" ./bin/vstack trust "$1" --yes >/dev/null 2>&1; } +# Keeps stderr and the exit code instead of discarding them. This check went red on Alpine in CI +# and nowhere else, and all three lanes reported only "vstack trust failed" because the command +# was run with `>/dev/null 2>&1` -- a check that knows something is wrong and has thrown away the +# only sentence saying what. Two reproduction attempts against a byte-identical image failed to +# make it fail, which is exactly the case where the swallowed stderr is the whole investigation. +# VSTACK_DIR is set for the same reason HOME is overridden, and the two interact. HOME is +# redirected so each lane gets its own trust store instead of writing the operator's. That also +# hides ~/.vstack and the global gitconfig, and bin/vstack resolves its repo from $VSTACK_DIR, +# then ~/.vstack, then `git rev-parse --show-toplevel`. Under a foreign HOME the first two are +# gone, and inside the Alpine CI container the third fails too -- actions/checkout writes its +# safe.directory exemption into the runner HOME's gitconfig, which the override hides, so git +# refuses the checkout as dubiously owned. bin/vstack then exits 1 before `trust` runs at all. +# +# PROVEN: the three lanes reported `no vstack repo found (checked $VSTACK_DIR, ~/.vstack, and +# this script's git root)`, rc=1, on Alpine only. INFERRED: that the git-root lane failed +# specifically on dubious ownership; the message does not say which of the three lost. Naming +# $VSTACK_DIR settles it either way, and it is what the error text itself tells you to do. +# +# This is the check's own environment, not a defect in what it measures: a user does not run +# vstack under a substituted HOME. It cost a red on one platform and three destroyed tags' +# worth of noise because the invocation discarded stderr. +c61_last="" +c61_trust(){ + c61_last=$(HOME="$1/home" VSTACK_DIR="$PWD" ./bin/vstack trust "$1" --yes 2>&1 >/dev/null) + c61_rc=$? + [ -n "$c61_last" ] || c61_last="(command printed nothing on stderr)" + c61_last="rc=$c61_rc, stderr: $(printf '%s' "$c61_last" | tr '\n' ' ')" + return "$c61_rc" +} # Lane 2 -- positive control. A repo whose gate runs ./scripts/ci.sh must get ci.sh recorded, and # that path is in no hardcoded list anywhere: it can only come from reading verify.sh. @@ -4450,7 +4478,7 @@ if c61_trust "$c61_a"; then grep -q '/scripts/ci\.sh$' "$(c61_store "$c61_a")" \ || c61_errs="$c61_errs\nvstack trust did not record scripts/ci.sh, a path only verify.sh names -- the scan is not reading the gate" else - c61_errs="$c61_errs\nvstack trust failed on a synthetic repo naming ./scripts/ci.sh" + c61_errs="$c61_errs\nvstack trust failed on a synthetic repo naming ./scripts/ci.sh -- $c61_last" fi # Lane 3 -- negative control. A gate that runs nothing else gets one entry. Without this, lane 2 @@ -4463,13 +4491,15 @@ if c61_trust "$c61_b"; then [ "$c61_n" = 1 ] \ || c61_errs="$c61_errs\nvstack trust recorded $c61_n entries for a gate that names no other script; it should record 1" else - c61_errs="$c61_errs\nvstack trust failed on a synthetic repo naming nothing" + c61_errs="$c61_errs\nvstack trust failed on a synthetic repo naming nothing -- $c61_last" fi # Lane 4 -- this repository. The two files the narrow writer missed, named because they are the # ones verify.sh actually executes, not because they are a general category. c61_c="$c61_d/c"; mkdir -p "$c61_c/home" -if HOME="$c61_c/home" ./bin/vstack trust "$PWD" --yes >/dev/null 2>&1; then +if c61_last=$(HOME="$c61_c/home" VSTACK_DIR="$PWD" ./bin/vstack trust "$PWD" --yes 2>&1 >/dev/null); c61_rc=$?; \ + c61_last="rc=$c61_rc, stderr: $(printf '%s' "${c61_last:-(nothing on stderr)}" | tr '\n' ' ')"; \ + [ "$c61_rc" -eq 0 ]; then for c61_f in install.sh overlay.sh; do grep -q "/$c61_f\$" "$(c61_store "$c61_c")" \ || c61_errs="$c61_errs\nthe trust store for this repo does not record $c61_f, which .claude/verify.sh executes" @@ -4477,7 +4507,7 @@ if HOME="$c61_c/home" ./bin/vstack trust "$PWD" --yes >/dev/null 2>&1; then c61_tot=$(grep -c . "$(c61_store "$c61_c")" || true) else c61_tot=0 - c61_errs="$c61_errs\nvstack trust failed on this repository" + c61_errs="$c61_errs\nvstack trust failed on this repository -- $c61_last" fi rm -rf "$c61_d" diff --git a/tests/container-matrix.sh b/tests/container-matrix.sh index 1279677..f360e5e 100755 --- a/tests/container-matrix.sh +++ b/tests/container-matrix.sh @@ -81,8 +81,8 @@ fi cat > "$ROOT/bootstrap-apk.sh" <<'EOF' #!/bin/sh set -e -apk add --no-cache bash git jq curl ca-certificates shellcheck 2>&1 || \ - apk add --no-cache bash git jq curl ca-certificates 2>&1 +apk add --no-cache bash git jq curl ca-certificates shellcheck python3 2>&1 || \ + apk add --no-cache bash git jq curl ca-certificates python3 2>&1 EOF cat > "$ROOT/bootstrap-apt.sh" <<'EOF' @@ -90,8 +90,8 @@ cat > "$ROOT/bootstrap-apt.sh" <<'EOF' set -e export DEBIAN_FRONTEND=noninteractive apt-get update -qq -apt-get install -y -qq --no-install-recommends git jq curl ca-certificates shellcheck || \ - apt-get install -y -qq --no-install-recommends git jq curl ca-certificates +apt-get install -y -qq --no-install-recommends git jq curl ca-certificates shellcheck python3 || \ + apt-get install -y -qq --no-install-recommends git jq curl ca-certificates python3 EOF # --- the in-container assertion runner ---------------------------------------------------------- @@ -193,10 +193,20 @@ if [ -x "$DOCTOR" ]; then # of .claude/verify.sh fails if either stops reading it or if the list grows. _pt_file="/work/repo/tests/pretag-findings.sh" if [ -f "$_pt_file" ]; then . "$_pt_file"; else PRETAG_ALLOWED_FINDING=""; fi - _dr_x=$(grep '✖' /tmp/doctor.out | grep -vF "$PRETAG_ALLOWED_FINDING") + # bin/doctor's closing line is `DRIFT ✖` for ANY non-zero FAIL count -- it is the summary + # banner, not a drift finding, and it name-collides with one. Left in the residual it made the + # carve-out unreachable by construction: with the allowed finding as the SOLE failure, _dr_x + # was still non-empty, holding a line with no diagnostic content, and every image reported + # `doctor exit=1; DRIFT ✖`. Dropping it does not soften anything -- each individual finding + # prints its own ✖ line, so a second, real failure still lands in the residual. What is added + # below is the guard that the banner was accidentally providing: the allowed finding must + # actually be present, or a doctor that failed while printing no findings at all would pass. + _dr_x=$(grep '✖' /tmp/doctor.out | grep -vF "$PRETAG_ALLOWED_FINDING" | grep -vxF 'DRIFT ✖') if [ "$rc" -eq 0 ]; then res PASS "2-doctor-exit0" "doctor exit=$rc" elif [ -z "$_pt_file" ] || [ ! -f "$_pt_file" ]; then res FAIL "2-doctor-exit0" "doctor exit=$rc and $_pt_file is missing, so the pre-tag carve-out cannot be applied; $(grep '✖' /tmp/doctor.out | tr '\n' ';')" + elif ! grep -qF "$PRETAG_ALLOWED_FINDING" /tmp/doctor.out; then + res FAIL "2-doctor-exit0" "doctor exit=$rc but did not report '$PRETAG_ALLOWED_FINDING' at all, so the carve-out is excusing a failure it cannot see; $(grep '✖' /tmp/doctor.out | tr '\n' ';')" elif [ -z "$_dr_x" ]; then res PASS "2-doctor-exit0" "doctor exit=$rc, only finding is '$PRETAG_ALLOWED_FINDING', a note in a commit-triggered lane" else res FAIL "2-doctor-exit0" "doctor exit=$rc; $(printf '%s' "$_dr_x" | tr '\n' ';')"; fi