mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
e9a210edd5
check-corpus-freshness.sh, check-compile-health.sh, check-compile-oscillation.sh used 'find ... -printf %T@ %p | sort | awk' to pick the newest file by mtime. -printf is GNU-only; BSD/macOS find errors. corpus-freshness (set -uo) silently false-SKIPs even when snapshots exist; compile-health/oscillation (set -euo pipefail) crash via pipefail when the overnight-defrag fallback path is hit. Same class as age-iue5 (#850) but the harder mtime-sort form. No bats coverage -> not in the failing-12; found by class-sweep. Fix (after a cross-family pawl refuted the first 'find -exec ls -t {} + | head' attempt — that is NOT a global sort when find batches across multiple ls calls, and an added -type f dropped symlinked snapshots): emit '<mtime>\t<path>' per match via portable stat (BSD 'stat -f %m' / GNU 'stat -c %Y' fallback), then a single global 'sort -n | tail -n 1' (tail consumes all input, avoiding the head+pipefail SIGPIPE trap). corpus-freshness keeps the original symlink- inclusive match (no -type f). Integer-second precision (vs the original's fractional %T@) is adequate for these seconds-apart dirs. Verified on real BSD find+stat: global-newest correct across distinct mtimes, zero-match -> empty, corpus-freshness now PASSES (was false-SKIP), health/ oscillation exit 0, shellcheck clean, no -printf left in scripts/.
90 lines
3.9 KiB
Bash
Executable File
90 lines
3.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# check-compile-health.sh — Gate: Compile defrag report is fresh and stale count is acceptable.
|
|
#
|
|
# Exit 0 = PASS, Exit 1 = FAIL
|
|
#
|
|
# Environment overrides:
|
|
# COMPILE_OUTPUT_DIR Directory where ao defrag writes (default: $AGENTS_DIR)
|
|
# AGENTS_DIR .agents base dir (default: .agents)
|
|
# COMPILE_MAX_AGE_HOURS Max age of latest defrag report in hours (default: 26)
|
|
# COMPILE_MAX_STALE Max allowed stale learning count (default: 5)
|
|
set -euo pipefail
|
|
|
|
AGENTS_DIR="${AGENTS_DIR:-.agents}"
|
|
MAX_AGE_HOURS="${COMPILE_MAX_AGE_HOURS:-26}"
|
|
MAX_STALE="${COMPILE_MAX_STALE:-5}"
|
|
|
|
# COMPILE_OUTPUT_DIR overrides AGENTS_DIR prefix (used in CI where defrag writes to /tmp/)
|
|
DEFRAG_LATEST="${COMPILE_OUTPUT_DIR:-$AGENTS_DIR}/defrag/latest.json"
|
|
|
|
# Gate 1: defrag report must exist.
|
|
# Fall back to the most recent Dream overnight preview when no canonical
|
|
# report is present and COMPILE_OUTPUT_DIR is unset — Dream writes a
|
|
# dry-run defrag at .agents/overnight/<run>/defrag/latest.json every night,
|
|
# and that report exposes .timestamp + .prune.stale_count in the same shape
|
|
# this gate consumes. Only apply the fallback for the canonical AGENTS_DIR
|
|
# path so CI overrides remain strict.
|
|
if [[ ! -f "$DEFRAG_LATEST" && -z "${COMPILE_OUTPUT_DIR:-}" ]]; then
|
|
overnight_root="$AGENTS_DIR/overnight"
|
|
if [[ -d "$overnight_root" ]]; then
|
|
# `-printf` is GNU-only; on BSD/macOS find errors and (under set -euo
|
|
# pipefail) the suppressed failure propagates -> this assignment crashes
|
|
# the script when the fallback path is hit. Portable TRUE-global mtime
|
|
# sort: emit "<mtime>\t<path>" per match (BSD `stat -f %m` / GNU `stat -c
|
|
# %Y` fallback), then a single global `sort -n | tail` (ascending + tail
|
|
# = newest; tail consumes all input, avoiding the head+pipefail SIGPIPE
|
|
# trap; no per-batch mis-sort). Zero matches -> empty fallback.
|
|
fallback="$(find "$overnight_root" -path '*/defrag/latest.json' -type f 2>/dev/null \
|
|
| while IFS= read -r _f; do
|
|
printf '%s\t%s\n' "$(stat -f %m "$_f" 2>/dev/null || stat -c %Y "$_f" 2>/dev/null)" "$_f"
|
|
done | sort -n | tail -n 1 | cut -f2-)"
|
|
if [[ -n "$fallback" && -f "$fallback" ]]; then
|
|
echo "INFO: $DEFRAG_LATEST not found; falling back to overnight preview $fallback"
|
|
DEFRAG_LATEST="$fallback"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ ! -f "$DEFRAG_LATEST" ]]; then
|
|
echo "FAIL: $DEFRAG_LATEST not found — run 'ao defrag' first"
|
|
exit 1
|
|
fi
|
|
|
|
# Gate 2: defrag report must be recent
|
|
ts=$(jq -r '.timestamp' "$DEFRAG_LATEST" 2>/dev/null || echo "")
|
|
if [[ -z "$ts" || "$ts" == "null" ]]; then
|
|
echo "FAIL: could not read .timestamp from $DEFRAG_LATEST"
|
|
exit 1
|
|
fi
|
|
|
|
# Parse timestamp — support both Linux (date -d) and macOS (date -j)
|
|
if date --version >/dev/null 2>&1; then
|
|
# GNU date (Linux)
|
|
ts_epoch=$(date -d "$ts" +%s 2>/dev/null) || { echo "FAIL: could not parse timestamp '$ts'"; exit 1; }
|
|
else
|
|
# BSD date (macOS) — strip trailing Z, handle both RFC3339 and ISO8601
|
|
ts_clean="${ts%Z}"
|
|
ts_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$ts_clean" +%s 2>/dev/null) \
|
|
|| ts_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$ts_clean" +%s 2>/dev/null) \
|
|
|| { echo "FAIL: could not parse timestamp '$ts'"; exit 1; }
|
|
fi
|
|
|
|
now_epoch=$(date +%s)
|
|
age_seconds=$(( now_epoch - ts_epoch ))
|
|
age_hours=$(( age_seconds / 3600 ))
|
|
|
|
if [[ $age_hours -gt $MAX_AGE_HOURS ]]; then
|
|
echo "FAIL: last defrag was ${age_hours}h ago (max: ${MAX_AGE_HOURS}h) — run 'ao defrag'"
|
|
exit 1
|
|
fi
|
|
|
|
# Gate 3: stale learning count must be within budget
|
|
stale=$(jq -r '.prune.stale_count // 0' "$DEFRAG_LATEST" 2>/dev/null || echo "0")
|
|
if [[ "$stale" -gt "$MAX_STALE" ]]; then
|
|
echo "FAIL: $stale stale learnings (max: $MAX_STALE) — run 'ao defrag --prune'"
|
|
exit 1
|
|
fi
|
|
|
|
echo "PASS: Compile health OK (defrag ${age_hours}h ago, stale=${stale}/${MAX_STALE})"
|
|
exit 0
|