feat(issue-21): loop runner --worktree/--base/--review-only with a fail-closed empty change-set guard; control plane stays in the main checkout

- --worktree <path> (CLI over MAM_LOOP_WORKTREE): the source root. Only the
  diff, base, head and the empty-set check use it; the wrapper, registry,
  checkpoint, loop guard, promotion and cleanup stay on the control root. The
  collector is the control root's diff_collect.sh (never the worktree's
  unreviewed copy), its arguments shlex-quoted. Validation: an existing
  directory, the top of its working tree, the same git common dir as the
  control root (".git" is resolved against the directory it came from).
- --base <ref> (CLI only, no env - a stale sha would silently widen the next
  range): resolved to a full sha, must be an ancestor of the source HEAD.
- --review-only: requires --base, refuses --plan; Phase 2 is skipped and
  logged; correction and comply jobs still go to the creator.
- With --worktree or --base every delegated prompt starts with a context
  block (path, branch, base and its origin, head, base..head commit count,
  uncommitted changes); without them prompts are byte-identical. All call
  sites in run() go through one _dispatch helper.
- Fail-closed guard (only with --worktree/--base): before each review round,
  self-review included, an empty change set or one git cannot determine
  (exit codes, never output parsing) ends the loop with rc 1 instead of
  asking for reviews. This closes the fail-open where a loop started after
  an out-of-loop commit sent reviewers "(no changes since base commit)".
- The runner honours MAM_JOBS_DIR and passes --registry-dir to the wrapper.
- run_loop.sh accepts the three options; the legacy shell loop
  (MAM_LOOP_PY=0) refuses them before the freeze and the loop lock.

Tests T2-1..T2-14.

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 03:21:43 +00:00
1 parent d3d3a76c83
commit d907359746
4 files changed
+539 -37

No files matched your search

@@ -32,6 +32,9 @@ TARGET_AGENT=""
TASK=""
REVIEWER_LIST=""
PLANNER_SESSION_OVERRIDE=""
LOOP_WORKTREE=""
LOOP_BASE=""
LOOP_REVIEW_ONLY=0
# Print usage instructions
usage() {
@@ -47,6 +50,9 @@ usage() {
echo " --max-rebut N Max rebuttal attempts per review iteration (default: 1, 0: disabled)"
echo " --verbose Print detailed execution timeline traces"
echo " --cleanup Purge temporary job directories upon success"
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)"
exit 1
}
@@ -83,6 +89,9 @@ while [[ "$#" -gt 0 ]]; do
;;
--planner) PLANNER_SESSION_OVERRIDE="$2"; shift 2 ;;
--task) TASK="$2"; shift 2 ;;
--worktree) LOOP_WORKTREE="$2"; shift 2 ;;
--base) LOOP_BASE="$2"; shift 2 ;;
--review-only) LOOP_REVIEW_ONLY=1; shift ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
@@ -104,6 +113,15 @@ if [ -n "${PLANNER_SESSION_OVERRIDE:-}" ] && [ "$PLAN_MODE" = false ]; then
exit 1
fi
# Issue #21 (D12): the legacy shell loop assumes a cwd-relative .mam/jobs and
# 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
exit 1
fi
# Anchor Python interpreter before freeze so freeze child processes find the project venv (O-4)
if [ -z "${AGENT_PYTHON_BIN:-}" ]; then
if [ -x "$REPO_ROOT/.venv/bin/python" ]; then