Active with --worktree/--base only; without them prompts, flow and promotion are unchanged (a loop_id is recorded in the checkpoint either way). - Every review round is pinned (B3): the source's HEAD and its working state as a tree built through a temporary index (the real index/status/refs are untouched); a dirty state becomes a ref-less snapshot commit on HEAD (committed as mam-loop, not as the user). An untracked nested repository cannot be pinned and is refused. identity = (loop_id, base, head, tree). - Review isolation (B4): a detached review worktree of the snapshot under TMPDIR (default), or `--review-isolation env` (the source root with env and temp-dir instructions). Failing to open the tree refuses the round - the isolation is never lowered silently. Trees are removed after the round and on any exit (run() wraps the body; release_guard too). - The reviewers' diff comes from the snapshot (B13): diff_collect.sh takes an optional head commit for a commit-to-commit diff; its two-argument form is byte-identical. - Stale rounds (B7/B8): after the round the source is measured again; if it changed, the round's approvals are discarded and the next iteration re-reviews without a correction job (last iteration: rc 1). Only the last non-stale round's PASSes are promoted. - Records (B9/B15): review_rounds in the checkpoint with per-reviewer transport/evidence, and report-<jid>.identity.json next to each promoted report: evidence review-tree | source-tree | diff-only. - Pre-promotion re-check (B10): a source changed after approval is not promoted (rc 1). - Remote reviewers (B14, Creator challenge 91c81095): they cannot reach this host's review tree. Checked before anything is delegated: refused by default; `--remote-review diff-only` admits them with the snapshot SHAs and diff as their only evidence (no local path in their prompt), and refuses a round whose diff is truncated or unavailable. - run_loop.sh accepts both options (usage identical to the runner's); the legacy shell loop refuses them before the lock. Tests tests/test_issue21_loop_b.py L1-L16 (22). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
154 lines
5.5 KiB
Bash
Executable File
154 lines
5.5 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 !!!"
|
|
|
|
MAM_DIFF_MAX_BYTES="${MAM_DIFF_MAX_BYTES:-200000}"
|
|
MAM_DIFF_MAX_LINES="${MAM_DIFF_MAX_LINES:-4000}"
|
|
|
|
_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
|