Merge-workspace review loop (--all-reviewer): round 1 (@a93c9bf) 4/4
PASS. Full suite at a93c9bf on macOS / Python 3.9 / herdr 0.9.3 with
WORKSPACE_ROOT set to an empty sentinel: 1752 passed / 0 failed / 38
skipped; the sentinel stayed empty, the review worktree had no .mam, and
the main checkout's shim was not touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
7.7 KiB
Review 7eab3b75 — tests must not take the checkout loop lock
Head a93c9bf29279f65ce0b9c9fb2c1aeea90a659420 (tree 741c19ba4c17601768b06684d5843d218a72beb1) gives the two checkout run_loop.sh runs that reach the lock a marker under the test's tmp_path. The new isolation test passes, and the same command without that marker leaves an empty .mam/ behind.
Snapshot /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261008T234013Z-4f5d21/1 only. Base 97c2946ee0e11eabcecafe6407755c33c0289218 (control-plane main, still that commit, porcelain empty). One commit, a93c9bf test(loop-lock): the b13 runs of the checkout's run_loop.sh take a loop lock of their own. Three files, +55, all under tests/. git diff --check is clean. .agents/skills and VERSIONS.md are outside the diff. No version bump.
Interpreter: worktree .venv Python 3.9.6, bash 3.2.57. The suite set AGENT_PYTHON_BIN to that interpreter and put its bin first on PATH.
What the commit does
run_loop.sh:15 sets MAM_REAL_ROOT to the checkout that contains the script. run_loop.sh:181 takes ${MAM_LOOP_MARKER:-$MAM_REAL_ROOT/.mam/loop-guard-active} after the arguments parse. loop_lock.sh:66 mkdir -ps the marker's parent. The trap at run_loop.sh:264 removes the marker file on the way out and leaves the directory.
tests/test_o3_scoped_guard.py:391 and :470 set MAM_LOOP_MARKER to tmp_path / "loop-guard-active" for test_b13_reexec_preserves_original_argv and test_b13_no_freeze_switch_disables_reexec. Both still run the checkout script with cwd at the repo root and --creator nonexistent-agent. The marker's parent is the test directory, so the mkdir -p does not create the checkout's .mam.
The lock-behavior tests keep their own paths. test_o2_12_run_loop_exits_on_lock_failure (tests/test_o2_race_free_lock.py:205) still points MAM_LOOP_MARKER at the marker the test is holding. The o3 scripts that call mam_acquire_loop_lock directly (test_z9_* around :177, :228, :266, :298, and test_b13_release_guard_cleans_up_and_releases_lock at :438) still use tmp_path as REPO_ROOT. Nothing in conftest.py sets the marker, and it is not autouse.
tests/test_loop_marker_isolation.py adds L1. It copies this checkout, runs the seven nodes below with WORKSPACE_ROOT naming the copy, and requires the copy to have no .mam at all. tests/test_issue17_workspace_root_isolation.py:71 adds the same directory check to W1. W1 already required .mam to contain no files; an empty directory left by mkdir -p passed that check.
Survey
A checkout run_loop.sh reaches mam_acquire_loop_lock (run_loop.sh:248) only after argument parsing. --help and usage exit at :66. A missing --creator/--task, --target-agent, --planner without --plan, a bad --max-rebut, and the legacy-shell refusal of the Python-only options (:136-141) all exit before the lock. bash -n and a read_text do not execute it. A script copied under tmp_path or mam_sandbox has its own MAM_REAL_ROOT.
The two b13 runs were the only executions of this checkout's run_loop.sh that got as far as the lock without MAM_LOOP_MARKER or a temp MAM_REAL_ROOT. The other runs that do reach the lock already name a marker.
| Location | Why this is not the checkout lock |
|---|---|
test_o3_scoped_guard.py:391, :470 |
this commit sets MAM_LOOP_MARKER under tmp_path |
test_issue5_phase3_loop.py:163, :181 |
MAM_LOOP_MARKER is tmp_path / "loop-marker" |
test_loop_cli.py:53 |
MAM_LOOP_MARKER is tmp_path / "loop-guard" |
test_loop_cli.py:100 |
_loop_env sets the marker and MAM_REAL_ROOT to mam_sandbox |
test_o2_race_free_lock.py:205 |
the marker is the one the test holds, on purpose |
test_issue25_plan_reuse.py:370 |
marker is set; MAM_LOOP_PY=0 plus --plan-file exits at run_loop.sh:140 before acquire |
test_issue21_worktree_prereqs.py:404 |
marker is set on mam_sandbox; the same refusal exits before acquire |
test_issue21_loop_b.py:238 |
marker and WORKSPACE_ROOT are tmp_path; the same refusal exits before acquire |
test_issue21_acceptance.py:298 |
the script is the temp repo's copy, and the marker is set |
test_issue36_prompt_transport.py:539 |
copied skills tree; marker is ctl/.mam/loop-marker |
test_tier4_e2e.py:433 |
the script lives under mam_sandbox, so MAM_REAL_ROOT is that sandbox |
test_o1_rebuttal.py:72, :76 |
--help, then --max-rebut abc; both exit in the parser |
test_loop_cli.py:36, :45, :63, :71 |
--help, missing args, --target-agent, --planner without --plan |
test_issue5_phase3_loop.py:151, test_issue5_phase4_runtime.py:141, test_issue25_plan_reuse.py:355, test_o3_scoped_guard.py:422 |
--help |
test_o2_race_free_lock.py:232, test_b7_diff_untracked.py:191 |
bash -n |
test_b7_diff_untracked.py:174 |
sources diff_collect.sh, which sources lib_int.sh |
test_deploy_freshness.py:473, test_issue5_phase0_contracts.py:481, test_o3_scoped_guard.py:137 |
the file is read as text |
test_o3_scoped_guard.py:177 and the sibling z9 scripts |
loop_lock.sh directly, REPO_ROOT is tmp_path |
lib_py/loop/runner.py:1226 releases MAM_LOOP_MARKER or self.mam_dir / "loop-guard-active". The acquire lives in run_loop.sh. L1's node list also names test_p7b_the_legacy_shell_loop_refuses_the_option, which exits before the lock. That node still belongs in the regression: it runs this checkout's script.
Requirement 3 — the unfixed command leaves .mam/
Product run_loop.sh is unchanged, so the command test_b13_no_freeze_switch_disables_reexec used to run is the pre-fix case. The script tree was a copy of the snapshot's .agents under /tmp/7eabpre, not the snapshot and not the control-plane checkout. WORKSPACE_ROOT pointed at a side directory, so lib.sh's shim was not the thing creating the copy's .mam. Inherited HERDR_*, MAM_*, MQTT_*, and SKS_* were removed.
Without MAM_LOOP_MARKER, bash run_loop.sh --creator nonexistent-agent --task "test goal" exited 1 and left /tmp/7eabpre/old-checkout/.mam/ as an empty directory. The marker file was gone. The shim landed in the side directory (.mam/shim/herdr).
The same command with MAM_LOOP_MARKER set to /tmp/7eabpre/own-marker/loop-guard-active exited 1 and left the second copy with no .mam. The marker file was released. The snapshot's .mam stayed absent, and the control-plane lock stayed mtime=1791502813 size=70.
Requirement 4 — suite
AGENT_PYTHON_BIN and PATH pointed at the worktree .venv. Inherited HERDR_*, MAM_*, MQTT_*, SKS_*, and WORKSPACE_ROOT were cleared in the parent. PYTHONDONTWRITEBYTECODE=1, -p no:cacheprovider. Basetemp directories are /tmp/7eabA.n4k8Qw and /tmp/7eabB.p2m7Lx. Neither path contains mam-review.
The four files are tests/test_issue17_workspace_root_isolation.py, tests/test_loop_marker_isolation.py, tests/test_o2_race_free_lock.py, and tests/test_o3_scoped_guard.py.
(a) WORKSPACE_ROOT unset: 57 passed in 47.60s, exit 0.
(b) WORKSPACE_ROOT=/tmp/7eabsent.t8c3Vr (the directory existed): 57 passed in 47.43s, exit 0. The sentinel was empty afterwards.
pytest_configure drops a WORKSPACE_ROOT whose real path is some other directory, so the outer sentinel staying empty is that drop. L1 and W1 keep a root that names the copied checkout, and both require that copy to have no .mam. Both passed inside (a) and (b).
After the pair: snapshot .mam absent, source worktree .mam absent, snapshot and worktree porcelain empty, control-plane main still 97c2946 with empty porcelain, control-plane loop-guard-active still mtime=1791502813 size=70, ~/.config/herdr/config.toml mtime 1784456394, directory mtime 1791382452.
[VERDICT: PASS]