Files
multi-agent-mux/.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh
T
GodopuandClaude Opus 5.5 fb1b93c665 fix(followups): F4b - bash integer knobs that reach [ -ge/-le/-gt ] or sleep go through mam_int_knob
A non-numeric MAM_READY_EMPTY_GIVEUP, SKS_EMPTY_GIVEUP, SKS_IDLE_RETRIES,
SKS_IDLE_RETRY_BACKOFF_S, MAM_LOCK_STEAL_RETRIES, MAM_DIFF_MAX_BYTES,
MAM_DIFF_MAX_LINES or RECONCILE_POLL_INTERVAL was a `[` error or a sleep
error at the point of use (PR #43 review claude R3). They now go through
mam_int_knob: decimal, a warning and the default for a bad value. Minimums
are in test_issue26_ints.py INT_KNOBS: 1 for the give-up counts (0 acts as
1), the diff caps (0 truncates every diff) and the poll interval (0 is a
busy loop); 0 for the retry counts (one attempt) and the retry backoff.

SKS_QUIESCENT_INTERVAL stays raw: its default is 0.5 s and sleep's
fraction is its meaning; NOT_ARITHMETIC says so.

loop_lock.sh and diff_collect.sh also run without lib.sh (the Python runner
and the tests source them alone), so mam_int / mam_int_knob move verbatim
to lib_int.sh, which lib.sh sources and those two source when the function
is missing. remove.sh's no-manifest fallback list names the new file
(install copies every file under .agents/skills). test_issue21_loop_b l15,
which builds a control root holding only diff_collect.sh, copies it too.

Tests: the table guard now reads every shell file; scripts sourced alone
read their knobs as decimal; _pane_quiescent with SKS_EMPTY_GIVEUP=abc
gives up with a warning and no `[` error (14 of the new cases fail at
0051fd6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-02 08:23:31 +09:00

160 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# diff_collect.sh — Helper module for collecting git changes and untracked files (B-7)
# Single source of truth for collecting review diffs.
MAM_DIFF_UNAVAILABLE_MARKER="!!! CHANGE SET UNAVAILABLE !!!"
MAM_DIFF_TRUNCATED_MARKER="!!! DIFF TRUNCATED !!!"
MAM_DIFF_NESTED_MARKER="!!! NOT EXPANDED IN THIS DIFF !!!"
# These caps bound the evidence a reviewer is shown, nothing else. The prompt
# reaches the agent by file (Issue #36), so delivery has no size cap of its own:
# only a diff over these limits is truncated (and marked).
# The runner sources this file without lib.sh; the integer rule is lib_int.sh's.
# shellcheck source=../../lib_int.sh
declare -F mam_int_knob >/dev/null || source "$(dirname "${BASH_SOURCE[0]}")/../../lib_int.sh"
MAM_DIFF_MAX_BYTES="$(mam_int_knob MAM_DIFF_MAX_BYTES 200000 1)"
MAM_DIFF_MAX_LINES="$(mam_int_knob MAM_DIFF_MAX_LINES 4000 1)"
_mam_nested_dir_diff() {
local d="$1" outer_top="$2" inner_top=""
inner_top=$(git -C "$d" rev-parse --show-toplevel 2>/dev/null || printf '')
if [ -n "$inner_top" ] && [ "$inner_top" != "$outer_top" ]; then
printf '%s %s\n' "$MAM_DIFF_NESTED_MARKER" "$d"
printf 'Untracked NESTED GIT REPOSITORY. Its contents are listed through its own\n'
printf 'git, so its .gitignore applies; the outer diff cannot describe it.\n\n'
git -C "$d" diff 2>/dev/null || true
git -C "$d" ls-files -o --exclude-standard -z 2>/dev/null | while IFS= read -r -d '' s; do
[ -n "$s" ] || continue
git -C "$d" diff --no-index -- /dev/null "$s" 2>/dev/null || true
done
printf '\n'
return 0
fi
printf '%s %s (untracked directory)\n' "$MAM_DIFF_NESTED_MARKER" "$d"
git ls-files -o --exclude-standard -z -- "$d" 2>/dev/null | while IFS= read -r -d '' s; do
[ -n "$s" ] || continue
[ -f "$s" ] && [ ! -L "$s" ] || continue
git diff --no-index -- /dev/null "$s" 2>/dev/null || true
done
}
_mam_untracked_diff() {
local outer_top
outer_top=$(git rev-parse --show-toplevel 2>/dev/null || printf '%s' "$PWD")
git ls-files -o --exclude-standard -z 2>/dev/null | while IFS= read -r -d '' f; do
[ -n "$f" ] || continue
if [ -L "$f" ]; then
printf '%s %s -> %s (symlink, not expanded)\n' \
"$MAM_DIFF_NESTED_MARKER" "$f" "$(readlink "$f" 2>/dev/null)"
elif [ -d "$f" ]; then
_mam_nested_dir_diff "$f" "$outer_top"
elif [ -f "$f" ]; then
git diff --no-index -- /dev/null "$f" 2>/dev/null || true
else
printf '%s %s (unreadable entry, not shown)\n' "$MAM_DIFF_NESTED_MARKER" "$f"
fi
done
}
mam_collect_changes_diff() {
local repo_root="${1:-}"
local base_commit="${2:-}"
# Optional third argument (Issue #21 item 5): a commit - the review
# snapshot - to diff against base, commit to commit. The snapshot already
# holds the untracked files, so no untracked scan; nothing falls back to
# the live tree.
local head_commit="${3:-}"
if [ -z "$repo_root" ]; then
printf '%s\nreason: repo_root parameter is required\n' "$MAM_DIFF_UNAVAILABLE_MARKER"
return 2
fi
if [ ! -d "$repo_root" ]; then
printf '%s\nreason: repo_root directory %s does not exist\n' "$MAM_DIFF_UNAVAILABLE_MARKER" "$repo_root"
return 2
fi
local raw_diff="" rc=0
raw_diff=$(
cd -P "$repo_root" 2>/dev/null || exit 3
git rev-parse --git-dir >/dev/null 2>&1 || exit 4
if [ -n "$head_commit" ]; then
git cat-file -e "${base_commit}^{commit}" 2>/dev/null || exit 6
git cat-file -e "${head_commit}^{commit}" 2>/dev/null || exit 6
git diff "$base_commit" "$head_commit" || exit 5
exit 0
fi
if [ -n "$base_commit" ] && git cat-file -e "${base_commit}^{commit}" 2>/dev/null; then
git diff "$base_commit" || exit 5
else
git diff || exit 5
fi
_mam_untracked_diff
) || rc=$?
if [ "$rc" -ne 0 ]; then
local reason="not a git repository or git command failed (exit code $rc)"
if [ "$rc" -eq 3 ]; then
reason="cannot cd into repo_root $repo_root"
elif [ "$rc" -eq 4 ]; then
reason="not a git repository: $repo_root"
elif [ "$rc" -eq 5 ]; then
reason="git diff command failed"
elif [ "$rc" -eq 6 ]; then
reason="base '$base_commit' or head '$head_commit' is not a commit"
fi
printf '%s\nreason: %s\n' "$MAM_DIFF_UNAVAILABLE_MARKER" "$reason"
return 2
fi
if [ -z "$(printf '%s' "$raw_diff" | tr -d '[:space:]')" ]; then
printf '(no changes since base commit)\n'
return 0
fi
local byte_cnt line_cnt
byte_cnt=$(printf '%s' "$raw_diff" | wc -c | tr -d '[:space:]')
line_cnt=$(printf '%s' "$raw_diff" | wc -l | tr -d '[:space:]')
if [ "$byte_cnt" -gt "$MAM_DIFF_MAX_BYTES" ] || [ "$line_cnt" -gt "$MAM_DIFF_MAX_LINES" ]; then
local stat_summary=""
stat_summary=$(
cd -P "$repo_root" 2>/dev/null || exit 0
if [ -n "$head_commit" ]; then
git diff "$base_commit" "$head_commit" --stat 2>/dev/null || true
exit 0
fi
if [ -n "$base_commit" ] && git cat-file -e "${base_commit}^{commit}" 2>/dev/null; then
git diff "$base_commit" --stat 2>/dev/null || true
else
git diff --stat 2>/dev/null || true
fi
printf '\nUntracked files:\n'
git ls-files -o --exclude-standard 2>/dev/null || true
)
printf '%s\n' "$MAM_DIFF_TRUNCATED_MARKER"
printf 'The change set is %s bytes / %s lines, over the review limit (%s bytes / %s lines).\n' \
"$byte_cnt" "$line_cnt" "$MAM_DIFF_MAX_BYTES" "$MAM_DIFF_MAX_LINES"
printf 'Only the file-level summary is shown below. You have NOT been shown the\n'
if [ -n "$head_commit" ]; then
printf 'full change set -- inspect the review snapshot (%s) directly before voting.\n\n' "$head_commit"
else
printf 'full change set -- inspect the working tree directly before voting.\n\n'
fi
printf '%s\n\n' "$stat_summary"
printf '%s\n' "$MAM_DIFF_TRUNCATED_MARKER"
return 0
fi
printf '%s\n' "$raw_diff"
return 0
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
mam_collect_changes_diff "$@"
fi