fix(agent-repo): harden AR-21 diff-verify against set-e/pipefail abort

The 2026-05-08 attempt-2 dogfood batch had 8/8 tasks "succeed" with
zero branches pushed. Root cause: my AR-21 diff-verification block was
running under set -euo pipefail without explicit error handling. A
single non-zero exit anywhere in the `git diff | tr | sed` pipeline
killed finalize.sh before the metadata write or push ran.

Specific risk: `git diff <REF_HEAD>..HEAD` returns non-zero when the
SHA is unreachable (e.g., shallow clone with init.sh fork-fallback
where upstream-ref wasn't fetched). pipefail then kills the pipeline,
set -e kills the script.

Fix: wrap the entire AR-21 block in `set +eo pipefail` (with explicit
`set -eo pipefail` restore at the end). Also:
- Use `${arr[@]:-}` instead of `${arr[@]}` for set -u safety on empty
  arrays
- Add `|| true` to git command substitutions (belt-and-braces)
- Use `printf` instead of `echo` for the comma-wrap (more portable)

Verified locally: when `/workspace/reference/main/.git` is absent the
block correctly skips with the existing fallback; when present and
upstream-ref is reachable, the block runs and reports DIFF_VERIFIED.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul O'Reilly
2026-05-08 16:31:33 +12:00
parent 07309567d5
commit 9cf016fa1c

View File

@@ -270,25 +270,37 @@ fi
# appear in the diff. Catches the inverse: an agent silently destroying or # appear in the diff. Catches the inverse: an agent silently destroying or
# refactoring files outside the task scope (real incident: 2026-05-08 task # refactoring files outside the task scope (real incident: 2026-05-08 task
# 4a2f2988 — agent stripped 9 unrelated functions; gotchas item 17/21). # 4a2f2988 — agent stripped 9 unrelated functions; gotchas item 17/21).
#
# Whole block runs with set +e (and pipefail off) to ensure no diagnostic
# pipeline failure aborts finalize before metadata can be written. We
# explicitly check exit codes where they matter.
DIFF_VERIFIED=true DIFF_VERIFIED=true
DIFF_MISMATCH="" DIFF_MISMATCH=""
DIFF_SUMMARY="" DIFF_SUMMARY=""
set +eo pipefail
if [ -d /workspace/reference/main/.git ]; then if [ -d /workspace/reference/main/.git ]; then
# All files actually changed in the agent's commit (vs upstream HEAD). REF_HEAD=$(git -C /workspace/reference/main rev-parse HEAD 2>/dev/null)
REF_HEAD=$(git -C /workspace/reference/main rev-parse HEAD 2>/dev/null || echo "")
if [ -n "$REF_HEAD" ]; then if [ -n "$REF_HEAD" ]; then
# Files modified/added/deleted by this commit. # Files modified/added/deleted by the agent vs the upstream HEAD seed.
DIFF_SUMMARY=$(git diff --name-only "$REF_HEAD"..HEAD 2>/dev/null | tr '\n' ',' | sed 's/,$//') # Use git diff (working-tree style) plus committed changes — the
# agent commits via finalize.sh later, so the diff against REF_HEAD
# reflects total scope. `|| true` belt-and-braces against unreachable
# SHAs (e.g., if init.sh fell back to fork main without seeding from
# upstream).
DIFF_RAW=$(git diff --name-only "$REF_HEAD"..HEAD 2>/dev/null || true)
# If the .. range fails (rev-parse error) the substitution returns "".
# Fall back to the simpler diff against working-tree HEAD-1 (no good
# answer; just emit empty).
DIFF_SUMMARY=$(printf '%s\n' "$DIFF_RAW" | tr '\n' ',' | sed 's/,$//' || true)
# Required files must each appear in DIFF_SUMMARY.
if [ -n "${AGENT_EXPECTED_CHANGED_FILES:-}" ]; then if [ -n "${AGENT_EXPECTED_CHANGED_FILES:-}" ]; then
echo "Verifying required changed files: $AGENT_EXPECTED_CHANGED_FILES" echo "Verifying required changed files: $AGENT_EXPECTED_CHANGED_FILES"
IFS=',' read -ra _REQUIRED <<< "$AGENT_EXPECTED_CHANGED_FILES" IFS=',' read -ra _REQUIRED <<< "$AGENT_EXPECTED_CHANGED_FILES"
for required in "${_REQUIRED[@]}"; do for required in "${_REQUIRED[@]:-}"; do
required="${required#"${required%%[![:space:]]*}"}" required="${required#"${required%%[![:space:]]*}"}"
required="${required%"${required##*[![:space:]]}"}" required="${required%"${required##*[![:space:]]}"}"
[ -z "$required" ] && continue [ -z "$required" ] && continue
if ! echo ",$DIFF_SUMMARY," | grep -qF ",$required,"; then if ! printf ',%s,' "$DIFF_SUMMARY" | grep -qF ",$required,"; then
echo "ERROR: Required change to '$required' missing from agent's diff" echo "ERROR: Required change to '$required' missing from agent's diff"
DIFF_VERIFIED=false DIFF_VERIFIED=false
DIFF_MISMATCH="$DIFF_MISMATCH missing:$required" DIFF_MISMATCH="$DIFF_MISMATCH missing:$required"
@@ -296,15 +308,14 @@ if [ -d /workspace/reference/main/.git ]; then
done done
fi fi
# Forbidden files must NOT appear in DIFF_SUMMARY.
if [ -n "${AGENT_FORBIDDEN_CHANGED_FILES:-}" ]; then if [ -n "${AGENT_FORBIDDEN_CHANGED_FILES:-}" ]; then
echo "Verifying forbidden files unchanged: $AGENT_FORBIDDEN_CHANGED_FILES" echo "Verifying forbidden files unchanged: $AGENT_FORBIDDEN_CHANGED_FILES"
IFS=',' read -ra _FORBIDDEN <<< "$AGENT_FORBIDDEN_CHANGED_FILES" IFS=',' read -ra _FORBIDDEN <<< "$AGENT_FORBIDDEN_CHANGED_FILES"
for forbidden in "${_FORBIDDEN[@]}"; do for forbidden in "${_FORBIDDEN[@]:-}"; do
forbidden="${forbidden#"${forbidden%%[![:space:]]*}"}" forbidden="${forbidden#"${forbidden%%[![:space:]]*}"}"
forbidden="${forbidden%"${forbidden##*[![:space:]]}"}" forbidden="${forbidden%"${forbidden##*[![:space:]]}"}"
[ -z "$forbidden" ] && continue [ -z "$forbidden" ] && continue
if echo ",$DIFF_SUMMARY," | grep -qF ",$forbidden,"; then if printf ',%s,' "$DIFF_SUMMARY" | grep -qF ",$forbidden,"; then
echo "ERROR: Forbidden file '$forbidden' was modified by agent" echo "ERROR: Forbidden file '$forbidden' was modified by agent"
DIFF_VERIFIED=false DIFF_VERIFIED=false
DIFF_MISMATCH="$DIFF_MISMATCH forbidden:$forbidden" DIFF_MISMATCH="$DIFF_MISMATCH forbidden:$forbidden"
@@ -321,6 +332,7 @@ if [ -d /workspace/reference/main/.git ]; then
else else
echo "Note: /workspace/reference/main not present; skipping AR-21 diff verification" echo "Note: /workspace/reference/main not present; skipping AR-21 diff verification"
fi fi
set -eo pipefail
# AR-19: Push with retry — attempt up to PUSH_RETRIES+1 times (default 2: initial + 1 retry) # AR-19: Push with retry — attempt up to PUSH_RETRIES+1 times (default 2: initial + 1 retry)
echo "Pushing branch $BRANCH to $REPO_URL..." echo "Pushing branch $BRANCH to $REPO_URL..."