Review loop (--review-only, --all-reviewer, focused on this Mac: macOS, /usr/bin/python3 3.9.6, bash 3.2): round 1 (@63d8164) agy and grok PASS, claude and codex NOT PASS (F1 paho via the /usr/bin/python3 fallback, F2 logs default path, R1 disk-fallback redispatch interleaving, R2 symlinked registry), sent as a PR comment and fixed by the author in four commits; round 2 (@a7cb90e) 4/4 PASS. Full suite on macOS / Python 3.9.6 @a7cb90e: 1867 passed, 56 skipped, 1 error (the known #55 false positive: WAL/SHM of an agy conversation running in another workspace). Real herdr panes and ~/.claude/projects unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
7.8 KiB
Review 5e7e2324 — PR #58, Issue #57 follow-up
Reviewed only the snapshot /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261014T152849Z-f1a415/1 (loop-20261014T152849Z-f1a415). Head 63d816468e6d48d01bd96f31d8a25eeadfbaab60, tree e8f162f74b7ce8ee8cc6ba7954b7d28dfa3f89f6, base e32c9574644a9185813a4dc881812694d62b6204 is an ancestor. Eleven commits, 21 files, +659/−95. git diff --check is clean. Snapshot porcelain is empty and the snapshot has no .mam. Control-plane main is still e32c9574644a9185813a4dc881812694d62b6204, porcelain empty. No product file was edited. The source worktree was not used.
Host tools: /usr/bin/python3 3.9.6, the control-plane venv is also 3.9.6, and /bin/bash is 3.2.57. The changed Python modules parse with ast on 3.9.6. bash -n accepts lib.sh, the delegate-job wrapper, worktree_prepare.sh, status.sh, reconcile.sh, and stop_session.sh. Added lines do not use match, tomllib, or X | Y unions.
The previous holes are closed
The rule is mqtt_common.get_logs_dir(registry_dir) (mqtt_common.py:192-208). A non-blank DELEGATE_JOB_LOGS_DIR is returned unchanged. Otherwise the audit root is sibling_logs_dir (mqtt_common.py:186-189): delegate_job_logs next to os.path.abspath of the registry, or of default_registry_dir() when the caller does not pass one. Writers and readers that know a registry pass it: engine._flush, registry.py register/pick/logs, publish_event.record_outcome, remote.publish_dispatch, the legacy status mirror (mqtt_common.py:528), reconcile.sh, and every job_subscriber append including the reconnect line. runner._fingerprint (runner.py:1087) excludes that same root. Path treats the absolute result as absolute, so workspace_root / logs stays the logs directory.
control_plane.from_env (control_plane.py:54-57) records _abs(sibling_logs_dir(mam_jobs_dir)) when DELEGATE_JOB_LOGS_DIR is blank. forwarded_keys (control_plane.py:77-84) drops delegate_job_logs_dir when that stored path equals the same expression, and prefix emits only those keys. An explicit non-sibling root stays in the prefix. With no overrides, from_env still equals defaults, so the usual binding is not forwarded.
worktree_prepare.sh:239-240 appends DELEGATE_JOB_LOGS_DIR only when the caller already set it. stop_session.sh:154-158 still exports the row's delegate_job_logs_dir when the environment variable is unset. A row written by the new from_env stores the sibling, so that export points at the sibling.
Both get_job_status copies in status.sh (:168-173 and :380-385) search the explicit logs dir and the row's logs dir, then sibling_logs_dir of MAM_JOBS_DIR and of the row's mam_jobs_dir. The script was not executed against a real checkout. The sandbox test below runs it.
job_subscriber._check_disk_fallback (job_subscriber.py:96-121) reads status from the audit dict. A versioned registry record adopts that status only when transition_seq is present and int(aud_seq) >= int(rec_seq). A missing or non-numeric audit sequence is treated as stale for a versioned record. An unversioned record, and a missing record, keep the old rule. A registry status that is already terminal is left as it is. Token and attempt checks still run after that.
The wrapper removes the sixteen $WORKDIR/delegate_job_logs defaults. LOGS_DIR_ABS is set to "" before cmd_submit (multi-agent-mux-delegate-job:203). After cd and cd -P of the registry, cmd_submit assigns LOGS_DIR_ABS="$(_logs_dir_abs)" and exits 1 if that command fails (:281), then exports it (:284). Later prints in that command use $LOGS_DIR_ABS. cmd_logs (:1090-1109) parses --registry-dir itself and passes it to registry.py. It does not read LOGS_DIR_ABS. delegate_logs_dir in lib.sh:2375 is the same one-liner. On failure, delegate_started_recorded skips only the audit file and still reads the registry events.log (lib.sh:2386-2389). delegate_wait_started resolves the root once (lib.sh:2446).
This Mac
/tmp is a symlink to /private/tmp. A probe under /tmp (deleted afterward; no herdr, no real .mam) called from_env and forwarded_keys with the jobs directory given as the /tmp path and again with the /var form of a /private/var path:
- The stored logs path equals
_abs(sibling_logs_dir(mam_jobs_dir)), anddelegate_job_logs_diris absent fromforwarded_keys. get_logs_dirof the realpathed jobs directory equals that stored path as a string.from_envof a workspace under/tmp, with cwd/and noMAM_JOBS_DIR, equalsdefaults.get_logs_dir()is the same directory as that binding.- A relative
MAM_JOBS_DIRis stored absolute. Afterchdir("/")the derivable root is still not forwarded. - An explicit custom logs directory is forwarded.
- A jobs directory that is itself a symlink to another parent is recorded next to the target, and that binding is not forwarded.
sibling_logs_dirof the symlink path is a different directory, because it usesabspathand does not resolve the link. The wrapper physicalizes the registry withcd -Pbefore it resolves logs, and the pane receives the realpathedMAM_JOBS_DIR, so those two writers agree. A directget_logs_dirof the symlink path follows the path as given.
logs --registry-dir with no value, under set -euo pipefail, exits 1 with an unbound $2. The same shift pattern is how parse_opts reads every flag. The process exits before it writes. A failing command substitution assigned with || is caught on this bash.
Tests
Pytest used the control-plane venv (Python 3.9.6), PYTHONDONTWRITEBYTECODE=1, -p no:cacheprovider, and --basetemp /tmp/mam57-5e7e2324-pytest. Inherited HERDR_*, MAM_*, MQTT_*, SKS_*, WORKSPACE_ROOT, DELEGATE_JOB_LOGS_DIR, PYTHONPATH, HOME_DIR, CLAUDE_PROJECT_DIR, and CODEX_HOME were dropped. AGENT_PYTHON_BIN and DELEGATE_JOB_PYTHON pointed at that venv.
On the snapshot, 23 tests passed in 122.75s: tests/test_issue57_logs_follow_registry.py (resolver, registry CLI, wrapper dry-run and unreachable broker, publish, binding, explicit override, worktree prepare, status json and text, conflict and legacy claim, redispatched-attempt audit, prefix, create, and the pane that moves logs with --registry-dir), plus test_logs_dir_follows_the_same_order, test_t3_0_control_plane_module, test_t3_1_create_persists_and_forwards_a_foreign_binding, test_t3_2_default_binding_is_recorded_not_forwarded, test_t3_3_4_stop_keeps_the_binding_and_resume_reforwards_it, test_a2_pane_env_carries_the_binding_not_the_agent_args, and test_b9_logs_dir_follows_cwd_changes.
The same four new tests copied onto an archive of base e32c957 failed in 0.16s: get_logs_dir() rejected the registry argument, from_env kept the workspace logs directory, the disk fallback did not adopt an audit-only terminal status, and the prefix test did not see the sibling on the row. The archive and both basetemps were removed. Snapshot porcelain stayed empty.
The full suite was left to the orchestrator. tests/test_issue10_13_purge.py was not run.
Documentation left as written
SKILL.md:139-141 and registry.md:152-154 state the sibling rule. SKILL.md:175 (Audit Logs) still names only .mam/delegate_job_logs/<job_id>/, which is the directory the rule produces when the registry is .mam/jobs. docs/design/worktree-creator-model.md:126 lists the status candidates and does not mention the sibling lookup that status.sh now performs. README.md is outside this diff and still names the workspace directory. Those sentences do not change where the code writes.
The pane herdr config file is newer than the mtime recorded at the end of the previous review. This review did not start herdr and did not write that file. ~/.config/herdr/config.toml is unchanged. That mtime is not part of the verdict.
[VERDICT: PASS]