Merge-workspace review loop (--all-reviewer): round 1 (@d198dd5) claude agy PASS, grok codex NOT PASS (the same inherited WORKSPACE_ROOT in eleven more modules; test_orc_onboard's import-time env); fixed in8b47cc7. Round 2 claude grok NOT PASS (test_o2_race_free_lock, test_o1_rebuttal missed), agy codex PASS; fixed inac48cb7. Round 3 (@ac48cb7) 4/4 PASS. Full suite atac48cb7on macOS / Python 3.9 / herdr 0.9.3 with WORKSPACE_ROOT set to an empty sentinel: 1751 passed / 0 failed / 38 skipped; the sentinel stayed empty 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
9.4 KiB
Review 35f0c94f — Issue #17 follow-up, inherited WORKSPACE_ROOT
Head 8b47cc76eed4582b1848b2eec529523a61497320 (tree bfe5fcbbefbde9562334515b4a9803bee96f84fd) closes the writers named in the previous review and the new isolation tests pass. tests/test_o1_rebuttal.py:68 still runs the checkout run_loop.sh with the process environment, and that script sources lib.sh before it parses --help, so an agent pane whose WORKSPACE_ROOT names this checkout still gets .mam/shim/herdr rewritten.
Snapshot /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261008T133858Z-9e7f5b/2 only. Base 6b00b6e06f55299cb60f156ccef09ede4bae5bdd (control-plane main, unchanged). Branch commits: d198dd5 then 8b47cc7. Fifteen files, +212/−18, git diff --check clean. Product code under .agents/skills is unchanged. No version bump.
Interpreter: worktree .venv Python 3.9.6, bash 3.2.57. lib.sh picks that interpreter when AGENT_PYTHON_BIN is set (lib.sh:2050); otherwise it falls through to /usr/bin/python3, which has neither paho nor yaml.
What the branch does
tests/conftest.py:181-192 adds an opt-in fixture own_workspace_root. It creates tmp_path/.mam-test-state/workspace and sets WORKSPACE_ROOT there. It is not autouse, because in-process code also reads WORKSPACE_ROOT. Modules whose shells inherit the process environment opt in with pytestmark = pytest.mark.usefixtures("own_workspace_root").
On this pytest, a test that requests both the mark and mam_sandbox finishes with mam_sandbox's value. A one-test probe (usefixtures plus a mam_sandbox parameter) left os.environ["WORKSPACE_ROOT"] as the sandbox path. The orc-onboard tests, which use mam_sandbox and pass that directory into _scrubbed_env, passed in the suite below, so a test that names its own root still keeps it.
tests/test_issue17_item4_decisions.py:224-233 drops an inherited WORKSPACE_ROOT and sets the helper's tmp_path. Unset is the wrong fix: lib.sh:26 derives the checkout that contains the script, and lib.sh:3088 calls _init_herdr_isolation (lib.sh:143), which rewrites $WORKSPACE_ROOT/.mam/shim/herdr. pytest_configure (tests/conftest.py:144-146) drops WORKSPACE_ROOT only when its real path is a different checkout, so a value that names this checkout survives collection. That is why the item-4 helpers missed the autouse state fixture: they never request mam_sandbox.
tests/test_orc_onboard.py:19-30 still copies os.environ at import, and that copy now drops WORKSPACE_ROOT. Every source path rebuilds the env at call time from the current root (_scrubbed_env). tests/test_issue21_worktree_prereqs.py:547 sets WORKSPACE_ROOT to the temp tree inside _mutate. tests/test_workspace_scope.py:32 sets it to that test's workspace.
The same mark is on test_b19_headless_reconcile_fixes.py, test_b8_send_keys_verification.py, test_issue6_fail_closed.py, test_version_consistency.py, test_loop_cli.py, test_issue5_phase3_loop.py, test_issue5_phase4_runtime.py, test_o3_scoped_guard.py, test_issue25_plan_reuse.py, and test_orc_onboard.py.
Requirement 3 — the new tests fail on the pre-fix code
tests/test_issue17_workspace_root_isolation.py adds W1 and W2. W1 copies the checkout, runs one node from each previously inheriting module with WORKSPACE_ROOT naming that copy (conftest keeps a root that names the checkout), and requires the copy to have no .mam. W2 imports test_orc_onboard while WORKSPACE_ROOT is a sentinel, then points the process env at a later directory and calls run_find_uuid.
Pre-fix probe, snapshot lib.sh, outside the tree:
- Base
tests/test_orc_onboard.pyfrom6b00b6e, loaded from a temp tree whose.agentsis the snapshot's, imported withWORKSPACE_ROOTset to a sentinel, thenos.environpointed at another directory andrun_find_uuidcalled: the sentinel received.mam,.mam/shim,.mam/shim/herdr. The later directory received nothing. Return code 0. - The same probe against the head module: the sentinel stayed empty, and the later directory received
.mam/shim/herdr. Return code 0.
bash run_loop.sh --help with WORKSPACE_ROOT set to /tmp/35f0o1sent.q8w2 also wrote .mam/shim/herdr there (usage exits 1). Source is run_loop.sh:17, before the option parser. The snapshot shim was absent before and after that probe.
W1 and W2 are in the passing suite below. The item-4 sentinel test test_c_helpers_write_nothing_to_an_inherited_workspace is in the same file as the _env fix and passed with it.
Requirement 4 — suite
AGENT_PYTHON_BIN and PATH pointed at the worktree .venv. Inherited HERDR_*, MAM_*, MQTT_*, SKS_*, and WORKSPACE_ROOT were unset in the parent. PYTHONDONTWRITEBYTECODE=1, -p no:cacheprovider. Basetemp directories are under /tmp and do not contain mam-review.
(a) WORKSPACE_ROOT unset, basetemp /tmp/35f0C.m2k8Qn, the 14 modified test modules plus tests/test_issue33_budget.py, tests/test_issue5_phase1_delegation.py, and tests/test_tier1_unit.py: 643 passed in 837.78s, exit 0.
(b) WORKSPACE_ROOT=/tmp/35f0sent2.w9c3Tb (the directory existed), basetemp /tmp/35f0D.h6p4Js, the 14 modified modules: 490 passed in 672.56s, exit 0. The sentinel was empty afterwards.
pytest_configure removes a foreign WORKSPACE_ROOT before collection, so an outer sentinel stays empty even if a helper is wrong. The runs that keep a checkout-named root are W1 (it sets WORKSPACE_ROOT to the copied checkout) and the item-4 test (it monkeypatches a sentinel after configure). Both passed inside (a) and (b).
A first pair of runs without AGENT_PYTHON_BIN reported 22 failed / 621 passed and 22 failed / 468 passed. Those failures are /usr/bin/python3 missing paho and yaml (mqtt_common.py and atomic_yaml.py via _delegate_py_bin). The same nodes pass once AGENT_PYTHON_BIN is the worktree interpreter, which is the command the brief specifies. They are not a defect of 8b47cc7.
After both pairs: snapshot porcelain empty, snapshot .mam/shim/herdr absent, control-plane main still 6b00b6e with empty porcelain, control-plane shim mtime 1791475620, ~/.config/herdr/config.toml mtime 1784456394, directory mtime 1791382452.
The writer this head leaves in place
tests/test_o1_rebuttal.py:68 and :72 (test_v3_rebut_option_parsing_and_budget) run bash on the checkout run_loop.sh (--help, and --max-rebut abc) with no env. The module has no own_workspace_root mark. run_loop.sh:17 sources lib.sh before option parsing, and lib.sh:3088 rewrites $WORKSPACE_ROOT/.mam/shim/herdr. With the variable unset, lib.sh:26 derives this checkout. W1's node list does not name this test, so the new regression does not catch it.
Add pytestmark = pytest.mark.usefixtures("own_workspace_root") to tests/test_o1_rebuttal.py, the same opt-in test_loop_cli.py uses, and add tests/test_o1_rebuttal.py::test_v3_rebut_option_parsing_and_budget to NODES in tests/test_issue17_workspace_root_isolation.py. Passing WORKSPACE_ROOT set to a temp directory on those two subprocess.run calls is the same fix at the call site.
Survey — same pattern, already pinned
These execute lib.sh, run_loop.sh, or a skill script that sources lib.sh, and they already give that shell a temp root or they do not execute the source. They stay as they are.
| Location | Why the checkout shim is not the target |
|---|---|
test_issue17_item4_decisions.py:224 |
_env sets WORKSPACE_ROOT to the helper tmp |
test_issue21_worktree_prereqs.py:547 |
_mutate sets the temp tree; test_t2_8 sets mam_sandbox |
test_workspace_scope.py:32 |
subprocess env sets the test workspace |
test_issue33_budget.py:47 |
helper home |
test_issue18_liveness_contract.py:58 |
helper cwd; _submit uses that env |
test_issue36_prompt_transport.py:61 and :537 |
helper home, and the shell loop sets ctl |
test_issue21_phase2_loop_a.py:49 |
temp root |
test_issue21_loop_b.py:236 |
tmp_path |
test_issue6_turn_handshake.py:29 |
cwd |
test_issue14_delivery_ack.py:36 |
cwd |
test_issue26_envfile.py:76, test_issue26_ints.py:124 |
helper home |
test_c1_tui_readiness.py:19 |
temp directory |
test_layout.py:318 |
the script exports the test workspace |
test_issue19_subscriber_daemon_lifecycle.py:138 |
the only _run_helper caller uses mam_sandbox |
test_issue34_job_tree.py:590 |
copies the process env after mam_sandbox has set the root |
test_issue10_13_purge.py:149 |
_run_onboard callers request mam_sandbox, so the copied env already names the sandbox |
test_issue20_state_isolation.py:201 |
sets WORKSPACE_ROOT to the temp repo the test is about |
test_issue9_server_env.py, test_issue45_restore.py |
callers use mam_sandbox / rig, which has already set the root |
test_issue30_stall_watch.py:124 |
_env itself does not assign the root; wrapper calls go through mam_sandbox or test_issue18._submit |
test_o2_race_free_lock.py:229 |
bash -n does not execute run_loop.sh |
test_issue5_phase2a_loop.py:754 |
the source line is text inside an assertion |
test_tier3_integration.py:337 |
the source is a debug string, and that env sets the temp root |
test_deploy_freshness.py:473 |
reads run_loop.sh as text |
Marked modules listed above are covered by own_workspace_root. test_issue21_acceptance.py runs a run_loop.sh inside a temp git repo from its ctl fixture.
[VERDICT: NOT PASS]