release(v4.9.4): the audit log follows a moved registry (Issue #57, PR #58) - archive the review reports and bump the lockstep version

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
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-15 22:52:04 +09:00
1 parent a7cb90e63a
commit 7cf728e9f8
28 files changed
+619 -26

No files matched your search

@@ -0,0 +1,50 @@
# 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))`, and `delegate_job_logs_dir` is absent from `forwarded_keys`.
- `get_logs_dir` of the realpathed jobs directory equals that stored path as a string.
- `from_env` of a workspace under `/tmp`, with cwd `/` and no `MAM_JOBS_DIR`, equals `defaults`. `get_logs_dir()` is the same directory as that binding.
- A relative `MAM_JOBS_DIR` is stored absolute. After `chdir("/")` 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_dir` of the symlink path is a different directory, because it uses `abspath` and does not resolve the link. The wrapper physicalizes the registry with `cd -P` before it resolves logs, and the pane receives the realpathed `MAM_JOBS_DIR`, so those two writers agree. A direct `get_logs_dir` of 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]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261015T123502Z-1b9df6",
"base_sha": "e32c9574644a9185813a4dc881812694d62b6204",
"head_sha": "a7cb90e63abf454ccf22f6ff887cc25393755402",
"tree_sha": "96c5c5f82cb89e4383011d52fb1dcb21d4e787e6",
"review_commit": "a7cb90e63abf454ccf22f6ff887cc25393755402",
"reviewer": "reviewer-grok-01",
"job_id": "d6ae03a9",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-15T13:18:35Z"
}
@@ -0,0 +1,47 @@
# Review d6ae03a9 — PR #58, second pass on the four Mac findings
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261015T123502Z-1b9df6/1` (`loop-20261015T123502Z-1b9df6`). Head `a7cb90e63abf454ccf22f6ff887cc25393755402`, tree `96c5c5f82cb89e4383011d52fb1dcb21d4e787e6`, base `e32c9574644a9185813a4dc881812694d62b6204` is an ancestor. Fifteen commits, 23 files, +908/−131. The four commits after the first-round head `63d8164` are `75c0042`, `151af99`, `1c598bb`, and `a7cb90e`. `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.
Host tools: `/usr/bin/python3` 3.9.6 has no `paho`. The control-plane venv is also 3.9.6 and does have `paho`. `/bin/bash` is 3.2.57. The changed modules parse with `ast` on 3.9.6. `bash -n` accepts the wrapper.
## F1 — path rules import without paho
`lib_py/delegation/logroot.py` is standard library only. `sibling_logs_dir`, `get_logs_dir`, and `default_registry_dir` live there. `mqtt_common.py:33-40` imports `paho` inside `try/except` and re-exports the logroot names, so `mqtt_common.sibling_logs_dir` is the same function. `publisher.py:17-20` does the same for its own `paho` import. `control_plane.py:28` and both `status.sh` copies (`status.sh:171`, and the text-mode copy) import `sibling_logs_dir` from `logroot`, not from `mqtt_common`.
`/usr/bin/python3` imports `lib_py.delegation`, `lib_py.control_plane`, and `logroot` in one process. Sourcing `lib.sh` with `AGENT_PYTHON_BIN=/usr/bin/python3` and `mam_control_plane_json` on a temporary workspace exits 0 and returns a `delegate_job_logs_dir` under that workspace's `.mam`. `make_client` still raises `ImportError` when a broker is actually required (`mqtt_common.py:286-288`).
`lib_py.delegation.logroot` is not named in `eager.CORE_MODULES`. Importing `lib_py.delegation.mqtt_common`, which eager load does, loads `logroot` as a dependency. On this interpreter that module is in `sys.modules` after that import.
## F2 — `logs` keeps the workspace default
`cmd_logs` (`multi-agent-mux-delegate-job:1096-1113`) passes `--registry-dir` only when the caller gave it. The empty array uses `${reg[@]+"${reg[@]}"}`. On bash 3.2 with `set -u`, an empty array expands to nothing and a two-element array expands to both words. `registry.py` then applies its own default: `MAM_JOBS_DIR`, then `WORKSPACE_ROOT`, then the cwd.
With `WORKSPACE_ROOT` set to a temporary workspace and the cwd set to a different directory, `registry.py register` wrote the job under the workspace `.mam/jobs` and the audit log under the workspace `.mam/delegate_job_logs`. `logs --list` from that other cwd printed the job id and exited 0. The other directory gained no `.mam`.
## R1 — a later dispatch is not the earlier one's success
`_check_disk_fallback` (`job_subscriber.py:121-138`) re-reads the registry record after it has read the audit status and before it adopts that status. It adopts only when `dispatch_token`, `attempt_id` (falling back to `iteration`), and `transition_seq` are unchanged. A changed record leaves the job pending for the next poll, which reads both again. The extra read is one `load_job` per non-terminal job on the audit path. The token check below still uses the first snapshot, which this comparison has just required to match the second read, and there is no further I/O between them.
The regression test interleaves `error`, then `redispatch` to attempt 2, then `completed`, inside `read_logged_status`. The caller waiting on attempt 1's token gets an empty terminal set. The next call, waiting on attempt 2's token, gets `completed`. A third call with attempt 1's token stays pending. On an archive of `63d8164` the same test fails with `B's completion was attributed to A`.
## R2 — a symlinked registry has one audit log
`sibling_logs_dir` (`logroot.py:51`) uses `os.path.realpath` of the registry, then the sibling `delegate_job_logs`. A relative registry still resolves from the cwd. `forwarded_keys` (`control_plane.py:82-84`) compares `_abs` of both sides, so a `/tmp` path and its `/private/tmp` real path still count as the same sibling and the derivable root stays off the pane.
A probe under `/tmp` (`/tmp` is a symlink to `/private/tmp`): `from_env` with no `MAM_JOBS_DIR` equals `defaults`; `get_logs_dir()` is that same directory; a jobs path given through `/tmp` is not forwarded; a jobs directory that is itself a symlink to another parent pairs with the target's sibling, and the binding matches that directory and is not forwarded.
`submit --dry-run --registry-dir <tmp>/alias/jobs`, where that path links to `<tmp>/actual/jobs`, wrote `meta.json` under `<tmp>/actual/delegate_job_logs` and printed that directory. `<tmp>/alias/delegate_job_logs` was not created, and neither was the workspace audit root. `logs --registry-dir` of the alias printed `registered`.
## Tests
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mamd6-d6ae03a9-pytest`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, and `PYTHONPATH` were dropped. `AGENT_PYTHON_BIN` pointed at that venv.
On the snapshot, 25 tests passed in 50.76s: all of `tests/test_issue57_logs_follow_registry.py`, including the four new repros (control-plane without paho, status json and text without paho, logs from another cwd, the interleaved redispatch, and the symlinked registry), plus `test_t3_0_control_plane_module`, `test_logs_dir_follows_the_same_order`, and `test_b9_logs_dir_follows_cwd_changes`.
The six new test cases copied onto an archive of `63d8164` failed in 2.55s: the control-plane import traceback, both status modes, `logs` reading `<cwd>/.mam/delegate_job_logs`, the redispatch attributed to the earlier attempt, and `ImportError` for `logroot`. Scratch directories were removed. Snapshot porcelain stayed empty.
The full suite was left to the orchestrator. `SKILL.md`'s Audit Logs sentence still names `.mam/delegate_job_logs/<job_id>/`, which is the directory this rule produces when the registry is `.mam/jobs`. The design-doc status row now lists the sibling lookup (`docs/design/worktree-creator-model.md:128`).
The pane herdr config file is newer than the mtime seen 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]