workflow-fix: apply the Fix-A silent-exit ladder to the sibling cron wrappers
Highlight text on any card (body, plan, or an event) to anchor a comment, or leave a whole-task comment here. Mention @claude to summon a reply.
No comments yet.
Surfaced by the Claude code-reviewer's bug-class sweep during #2322 review
round 1, then spot-verified before filing.
The bug class
A cron wrapper of this project's standard shape does its work inside a brace
group redirected to a per-day log, then checks rc afterwards and Telegram-
pushes on failure:
{
cd "$PROJECT_DIR" || exit 1 # <-- the bug
...
rc=$?
} >> "$LOG_FILE" 2>&1
if [ "${rc:-0}" -ne 0 ]; then ... "$TELEGRAM_PUSH" "$MSG" ... fi
The brace group is not a subshell, so exit 1 inside it terminates the
whole script — before the rc != 0 alert arm. On this VM there is no MTA
and the crontab lines redirect stderr to a log nobody reads, so that path is a
completely silent failure. It defeats exactly the alerting the wrapper was
written to provide.
#2322 fixed this in scripts/cron_codex_auto_upgrade.sh by replacing the
cd ... || exit 1 with an if ! cd / elif / else ladder that logs FATAL,
sets rc=1, skips the (cwd-relative) payload invocation, and falls through to
the existing alert arm. That is the pattern to apply here.
Sites
Claude's sweep named three other alert-arm-bearing wrappers carrying the same shape — these are the ones where the bug actually bypasses an alert:
scripts/cron_step9c_ledger_refresh.sh:70— spot-verified:cd "$PROJECT_DIR" || exit 1at:70, with aTELEGRAM_PUSHalert arm at:114. Confirmed same class.scripts/cron_lesson_consolidate.sh:90— reported by the sweep. Note its alert helper is defined early (around:65), so verify whether theexitactually bypasses it before changing anything; it may already be safe, or safe for a different reason.scripts/cron_uv_cache_prune.sh:51— spot-verified:cd "$PROJECT_DIR" || exit 1at:51. Confirm whether it has an alert arm to bypass; if it has none, it belongs in the lower-priority group below.
The sweep additionally reported ~7 more wrappers with the cd-exit but no
alert arm to bypass. Those are lower priority — the exit is not silencing an
alert that exists — but they are the same latent shape and are worth
normalizing while the pattern is fresh. Enumerate them rather than trusting
this count.
Second site class, from the same #2322 review
The identical class appears at a second site inside the #2322 wrapper itself, and was left as a Minor rather than fixed in that round:
scripts/cron_codex_auto_upgrade.sh:43-48— thefor bin in uv npm codexpreflight runsexit 1at:46whileLOG_DIR(:52),TELEGRAM_PUSH(:55),SIDECAR(:56) andSENTINEL(:57) are not yet defined, so it structurally cannot alert — while its own comment says "Fail LOUD if still missing".scripts/cron_codex_auto_upgrade.sh:60—mkdir -p "$LOG_DIR" "$SENTINEL_DIR"is unchecked. An uncreatable log dir proceeds into the brace-group redirect, the redirect fails, the group never runs,rcis never assigned, and${rc:-0}converts that into success.
Whether #2322 fixes these in its own round or defers them here depends on the reconciler's severity ruling on that task; check #2322's outcome first and do not duplicate work it already landed.
Suggested shape
Centralize alerting into a function defined before the preflight and before
any cd, so every failure path — missing prerequisites, unwritable log dir,
failed cd, non-zero payload rc — reaches one alert path that does not itself
depend on the thing that failed. Initialize failure state before the
redirection. Keep the per-date sentinel dedup and the audit-sidecar row.
Mechanizable check
A shell lint would generalize this: in a wrapper that has an alert arm keyed on
rc, no exit may appear between the log brace group and that arm. Worth
adding alongside the fixes so the shape cannot come back — the sweep found four
independent instances, which is the signal that prose guidance alone has not
held.
Acceptance
- Each named site either fixed with the Fix-A ladder or explicitly dispositioned as already-safe with the reason.
- A behavioral test per fixed wrapper, in the shape #2322 used: operate on a
sed-rewritten copy intmp_pathwith the failure induced, redirect theEPS_*log/sentinel/sidecar/telegram env knobs intotmp_path, and assert the Telegram stub is called. Never touch the live log dirs, the real audit sidecars, or the real Telegram channel. - The full wrapper inventory enumerated, so the "~7 more with no alert arm" figure is replaced by a verified list.