Make the recovery scripts say why they failed, not just print it

The three helper scripts were only ever dry-run, so the no-silent-failures hook
never saw them until they were committed. Six handlers reported the reason to
stdout via _report() and nothing else — invisible to the log, and easy to miss
when the script runs as one step of finish_2026_08_24.sh amid a wall of other
output. These mutate benefit and sick-day state, so a quiet failure there reads
as success.

Log at error/warning alongside the stdout line, saying what was not done:
the bonus was not banked, the revocation was not persisted, the credit gate
cannot tell and so fails closed.

Verified by feeding the gate a corrupt log: it now logs the reason with a
traceback and exits 1, instead of printing one line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJmPLyT8NU4DzDJPV1KEX5
This commit is contained in:
Krzysztof Rudnicki 2026-08-24 13:10:53 +02:00
parent 16ada6e5a2
commit f4850670ee
3 changed files with 39 additions and 0 deletions

View File

@ -28,6 +28,7 @@ from __future__ import annotations
import argparse
from datetime import datetime
import json
import logging
from pathlib import Path
import sys
@ -37,6 +38,12 @@ EXTRA_BENEFITS_FILE = REPO_ROOT / "screen_locker" / "extra_benefits_state.json"
DEFAULT_BONUS_HOURS = 2
# Failures go to the log (stderr) as well as stdout: this script runs as one
# step of finish_2026_08_24.sh, where a bare stdout line scrolls past in a wall
# of other output, and it mutates benefit state — the exact case where a quiet
# failure looks like success.
_logger = logging.getLogger(__name__)
def _report(message: str) -> None:
"""Write one line to stdout (matches the other scripts in this directory)."""
@ -57,6 +64,7 @@ def _load(path: Path) -> dict:
try:
return json.loads(path.read_text())
except (OSError, json.JSONDecodeError) as exc:
_logger.warning("Could not read %s: %s — treating as empty", path, exc)
_report(f"WARN: could not read {path}: {exc} — treating as empty")
return {}
@ -119,6 +127,10 @@ def main() -> int:
try:
Path(args.benefits_file).write_text(json.dumps(benefits, indent=2) + "\n")
except OSError as exc:
_logger.exception(
"Could not write %s — the bonus was NOT banked",
args.benefits_file,
)
_report(f"FAIL: could not write {args.benefits_file}: {exc}")
return 1

View File

@ -24,6 +24,7 @@ from __future__ import annotations
import argparse
from datetime import datetime, timezone
import json
import logging
from pathlib import Path
import sys
@ -31,6 +32,11 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
SICK_HISTORY_FILE = REPO_ROOT / "screen_locker" / "sick_history.json"
LOG_FILE = REPO_ROOT / "screen_locker" / "log.json"
# Failures are logged as well as printed: this script rewrites sick-day
# history, so a read/write error that only reaches stdout can leave the
# revocation half-applied while the surrounding script reports success.
_logger = logging.getLogger(__name__)
def _report(message: str) -> None:
"""Write one line to stdout (matches the other scripts in this directory)."""
@ -112,6 +118,7 @@ def main() -> int:
try:
history = json.loads(history_path.read_text())
except (OSError, json.JSONDecodeError) as exc:
_logger.exception("Could not read %s — no sick day was revoked", history_path)
_report(f"FAIL: could not read {history_path}: {exc}")
return 1
@ -119,6 +126,10 @@ def main() -> int:
try:
log = json.loads(log_path.read_text()) if log_path.is_file() else {}
except (OSError, json.JSONDecodeError) as exc:
_logger.exception(
"Could not read %s — cannot cite a workout id, so nothing was revoked",
log_path,
)
_report(f"FAIL: could not read {log_path}: {exc}")
return 1
@ -133,6 +144,10 @@ def main() -> int:
try:
history_path.write_text(json.dumps(history, indent=2) + "\n")
except OSError as exc:
_logger.exception(
"Could not write %s — the revocation was NOT persisted",
history_path,
)
_report(f"FAIL: could not write {history_path}: {exc}")
return 1

View File

@ -21,6 +21,7 @@ from __future__ import annotations
import argparse
import json
import logging
from pathlib import Path
import sys
@ -38,6 +39,12 @@ COUNTED_WORKOUT_TYPES = frozenset(
)
# This script is a gate: it decides whether the rest of the recovery may
# proceed. A read failure that only reaches stdout can be mistaken for a plain
# "not credited" verdict, so it is logged loudly as well.
_logger = logging.getLogger(__name__)
def _report(message: str) -> None:
"""Write one line to stdout (matches the other scripts in this directory)."""
sys.stdout.write(f"{message}\n")
@ -58,6 +65,11 @@ def main() -> int:
try:
log = json.loads(log_path.read_text())
except (OSError, json.JSONDecodeError) as exc:
_logger.exception(
"Could not read %s — cannot tell whether the workout was "
"credited, so this gate fails closed",
log_path,
)
_report(f"FAIL: could not read {log_path}: {exc}")
return 1