feat(issue-21): item 5 review identity - per-round snapshot and snapshot diff, detached review worktree (--review-isolation), stale-round re-review, identity records with evidence, pre-promotion re-check; remote reviewers refused or diff-only (--remote-review)

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
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-14 20:54:53 +00:00
1 parent 4ad622e372
commit e57663ac4c
4 files changed
+748 -17

No files matched your search

@@ -55,6 +55,11 @@ _mam_untracked_diff() {
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"
@@ -70,6 +75,12 @@ mam_collect_changes_diff() {
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
@@ -86,6 +97,8 @@ mam_collect_changes_diff() {
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
@@ -104,6 +117,10 @@ mam_collect_changes_diff() {
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
@@ -117,7 +134,11 @@ mam_collect_changes_diff() {
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'
printf 'full change set -- inspect the working tree directly before voting.\n\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
@@ -35,6 +35,8 @@ PLANNER_SESSION_OVERRIDE=""
LOOP_WORKTREE=""
LOOP_BASE=""
LOOP_REVIEW_ONLY=0
LOOP_REVIEW_ISOLATION=""
LOOP_REMOTE_REVIEW=""
# Print usage instructions
usage() {
@@ -53,6 +55,8 @@ usage() {
echo " --worktree <path> Source worktree for diff/base/head (control plane stays here)"
echo " --base <ref> Pin the review diff base (must be an ancestor of the source HEAD)"
echo " --review-only Skip implementation; review <base>..HEAD (requires --base)"
echo " --review-isolation <v> Review tree with --worktree/--base: worktree (default) or env"
echo " --remote-review <v> Remote reviewers with --worktree/--base: refuse (default) or diff-only"
exit 1
}
@@ -92,6 +96,8 @@ while [[ "$#" -gt 0 ]]; do
--worktree) LOOP_WORKTREE="$2"; shift 2 ;;
--base) LOOP_BASE="$2"; shift 2 ;;
--review-only) LOOP_REVIEW_ONLY=1; shift ;;
--review-isolation) LOOP_REVIEW_ISOLATION="$2"; shift 2 ;;
--remote-review) LOOP_REMOTE_REVIEW="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
@@ -117,8 +123,9 @@ fi
# diffs the checkout it runs in; the worktree/base options are Python-only.
# Checked before the freeze and the loop lock (log_* are not defined yet).
if [ "${MAM_LOOP_PY:-1}" = "0" ] && { [ -n "$LOOP_WORKTREE" ] || [ -n "$LOOP_BASE" ] \
|| [ "$LOOP_REVIEW_ONLY" = "1" ] || [ -n "${MAM_LOOP_WORKTREE:-}" ]; }; then
echo "ERROR: --worktree/--base/--review-only require the Python loop (MAM_LOOP_PY=1)" >&2
|| [ "$LOOP_REVIEW_ONLY" = "1" ] || [ -n "${MAM_LOOP_WORKTREE:-}" ] \
|| [ -n "$LOOP_REVIEW_ISOLATION" ] || [ -n "$LOOP_REMOTE_REVIEW" ]; }; then
echo "ERROR: --worktree/--base/--review-only/--review-isolation/--remote-review require the Python loop (MAM_LOOP_PY=1)" >&2
exit 1
fi