# 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 -p`s 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]