When only the registry is moved (--registry-dir / MAM_JOBS_DIR) and DELEGATE_JOB_LOGS_DIR is not set, the audit log now goes in the delegate_job_logs next to the registry in use, not in the workspace's .mam/delegate_job_logs. The comment-5 disk-fallback bug is fixed in a separate commit on the same branch. The four problems from the Mac verification are fixed in 75c0042..a7cb90e. No version bump; the version is set at merge.
control_plane.from_env() derives delegate_job_logs_dir from the resolved mam_jobs_dir unless DELEGATE_JOB_LOGS_DIR is set. A session created with only MAM_JOBS_DIR moved no longer records the workspace's old log root.
worktree_prepare.sh passes DELEGATE_JOB_LOGS_DIR on only when the caller set it.
lib.sh and the delegate-job wrapper no longer carry a bash copy of the default. The wrapper had 16 of them.
delegate_publish_cmd, delegate_started_recorded and _logs_dir_abs resolve the root with get_logs_dir(<registry>).
The wrapper fails closed when it cannot resolve the root. Otherwise it exports the root to its children and prints it as submit's last line.
logs takes --registry-dir.
--dry-run keeps its meaning: it registers the job and seeds the audit log. No write-time guard checks that the registry record exists (comment §6).
Worktree panes no longer get the derivable audit-log root (c57e766)
Before this, a worktree creator's pane always got all four binding keys as environment variables, including DELEGATE_JOB_LOGS_DIR=<control root>/.mam/delegate_job_logs. Under "an explicit value wins", submit --registry-dir <tmp> from that pane still wrote the real audit log. This is the scene the issue is about.
control_plane.forwarded_keys() now picks the keys forwarded to the pane. It leaves out delegate_job_logs_dir when it equals the sibling of mam_jobs_dir, since the pane can derive it. An explicitly moved, non-sibling root is still forwarded.
The row still records all four keys; describe(), forward, the whitespace check, stop and status are unchanged.
Create and resume both go through prefix(), so this takes effect whenever a pane is spawned.
A pane that is already running keeps the old variable until it is stopped and resumed (or recreated).
Rows made before this change need no edit: they already hold the sibling root, so a resume does not forward it.
docs/design/worktree-creator-model.md says the derivable audit-log root is recorded, not forwarded.
Both get_job_status implementations (JSON and text) now also look in the delegate_job_logs next to MAM_JOBS_DIR and next to the row's mam_jobs_dir. They do this after the explicit log roots. So a job whose registry record was cleaned up still shows its audit-only terminal status.
The bug:_check_disk_fallback compared the status.json dict from read_logged_status() with the terminal status strings. So a terminal status recorded only in the audit log was never recognised. It now reads the dict's status.
Review P1, found by codex: after that fix, a redispatched record (attempt 2, pending) could be closed by attempt 1's leftover completedstatus.json.
This happens because the audit mirror is written after the registry commits, and a failed mirror leaves the old file in place.
Fix: a versioned record (transition_seq) takes an audit status only when the audit status's sequence is at least as new as the record's.
An audit status without a sequence is ignored for a versioned record.
An unversioned record (never transitioned, or MAM_ENGINE_CAS=0) and a missing record keep the old rule.
A status.json whose sequence is not an integer is ignored with a warning. Before, it raised out of the subscriber loop.
New file tests/test_issue57_logs_follow_registry.py. It covers:
the resolver (L1), registry.py (L2), the wrapper's submit/wait/logs (L3) and publish_event.py (L4);
control_plane.from_env, worktree_prepare and status.sh (JSON and text);
conflict refusals and legacy claims;
the comment-5 fallback and the redispatch case;
the invariant that an explicit DELEGATE_JOB_LOGS_DIR still wins;
pane forwarding: prefix() (function and CLI, sibling and non-sibling), create (row has the sibling, the pane's --env does not, the injected publish commands keep their DELEGATE_JOB_LOGS_DIR= prefix), worktree_prepare, and a forwarded pane whose --registry-dir experiment writes only next to the experiment registry.
Every test ends by asserting that the workspace's own .mam/delegate_job_logs was never created.
Against the pre-fix tree (e32c957): 12 tests fail and 6 teardowns fail the "workspace log root created" assertion. These are behavioral failures (wrong path, workspace root created, missing events), apart from 2 TypeErrors where a test calls the new get_logs_dir(registry) signature. The one test that passes there is the explicit-override invariant, which is expected.
Two existing tests change, as comment §4 allows.test_issue15_registry_isolation::test_logs_dir_follows_the_same_order and test_tier1_unit::test_b9_logs_dir_follows_cwd_changes each gain monkeypatch.delenv("MAM_JOBS_DIR", raising=False).
Why: conftest's autouse _isolate_mam_state_dirs gives every test a MAM_JOBS_DIR, so under the sibling rule the WORKSPACE_ROOT/cwd fallbacks these tests assert would resolve next to that registry instead.
The order they assert (explicit root, workspace, cwd) is unchanged.
Three more existing assertions change, approved by the user (plan §2-B). They pinned all four keys being forwarded:
tests/test_issue21_worktree_prereqs.py (create, and resume re-forwarding);
The Mac run on 63d8164 passed the suite, but two reviewers found four problems. Each fix has a regression test that fails on 63d8164 and passes now.
F1, paho dependency (75c0042). The path rules (default_registry_dir, sibling_logs_dir, get_logs_dir) moved to a new stdlib-only module, lib_py/delegation/logroot.py. mqtt_common re-exports them.
control_plane, both status.sh implementations, lib.sh and the wrapper's _logs_dir_abs import from logroot.
Importing the package also loads service and publisher. So mqtt_common and publisher now import paho optionally, and make_client() raises a clear ImportError when paho is missing.
These now work without paho: binding resolution and the pane prefix (create and resume), status (JSON and text), the audit-root resolution in lib.sh, and the wrapper's path resolver. Talking to the broker still needs paho.
F2, logs default path (151af99). The wrapper's logs passes --registry-dir on only when it is given, using ${reg[@]+"${reg[@]}"} so it is safe under bash 3.2 set -u. Without the flag, logs uses the workspace registry from any cwd again.
R1, disk-fallback dispatch identity (1c598bb)._check_disk_fallback re-reads the record just before it takes an audit status. It takes the status only when dispatch_token, attempt and transition_seq match the first read; otherwise it waits for the next poll.
This is the smallest of the three directions in the comment. It needs no audit schema change and does not hold the registry lock during audit I/O.
Before this, an "A error → redispatch → B completed" sequence between the two reads closed attempt A as a success.
R2, symlinked registry (a7cb90e).sibling_logs_dir() uses the registry's real path, so submit (which uses pwd -P) and logs given the link path find the same audit log. An explicit DELEGATE_JOB_LOGS_DIR still wins.
Round 1: codex returned NOT PASS (P1 and P2 above).
Rounds 2 and 3: claude, agy and codex returned PASS on c982616.
grok could not take part in that loop because of a stale weekly-limit dialog. After the limit reset, grok reviewed c982616 on its own (--review-only --reviewer reviewer-grok-01) and returned PASS.
All four reviewers passed the same head, c982616.
Plan §2-B (stop forwarding the derivable root) had been held for a user decision; it was approved after this PR was opened. Its loop (--base c982616) passed all four reviewers in round 1 on 63d8164. Its one non-blocking nit (forwarded_keys did not normalise the row value) is fixed in a7cb90e.
The Mac fixes were reviewed with --review-only --all-reviewer --base 63d8164. All four reviewers (claude, agy, grok, codex) passed a7cb90e in round 1 with no nits. Each reviewer re-ran the original reproductions on its own.
Full suite (Linux): 1891 passed, 20 skipped, 12 failed; the 12 are the known tests/test_issue10_13_purge.py failures (#28). Same environment as below; ~/.claude/projects and the real .mam/delegate_job_logs were unchanged.
paho-less probe:/usr/bin/python3 -m lib_py.control_plane json (Python 3.14.4, no paho) fails with No module named 'paho' at 63d8164 and prints the binding at a7cb90e.
Not run here: macOS /usr/bin/python3 3.9.6 and bash 3.2. The changed Python parses with the 3.9 grammar, and bash -n passes.
Full suite (Linux): 1885 passed, 20 skipped, 12 failed; the 12 are the known tests/test_issue10_13_purge.py failures (#28).
Run with the agent-pane environment (CLAUDE_PROJECT_DIR set), and with MAM_JOBS_DIR and DELEGATE_JOB_LOGS_DIR unset.
~/.claude/projects and the real .mam/delegate_job_logs were the same before and after.
Real-agent trial, worktree scene (63d8164): a real claude created the way worktree_prepare does it (workspace = a worktree, binding = the control root), no DELEGATE_JOB_LOGS_DIR.
The row records the control root's audit-log root; the pane has WORKSPACE_ROOT and MAM_JOBS_DIR but no DELEGATE_JOB_LOGS_DIR, after create and again after stop → resume.
From its own pane the agent ran submit --dry-run --registry-dir <tmp>/exp/jobs: the job's audit log is in <tmp>/exp/delegate_job_logs only, not in the control root's.
The onboarding and delegated jobs' logs are in the control root's .mam/delegate_job_logs, as before.
Real-agent trial, moved registry (c982616): a real claude on a throwaway herdr session, with only MAM_JOBS_DIR moved and DELEGATE_JOB_LOGS_DIR unset.
create --onboard exited 0 and onboarding completed.
The row's binding recorded both mam_jobs_dir and the log dir next to the moved registry.
A delegate job completed.
The onboarding job's and the delegate job's audit logs are both next to the moved registry.
The pane's DELEGATE_JOB_LOGS_DIR is that sibling.
The tree's own .mam/delegate_job_logs and the real control .mam/delegate_job_logs got no entries.
Static checks:
All changed Python files parse with the Python 3.9 grammar, and no 3.10+ APIs are used.
bash -n passes.
No DELEGATE_JOB_LOGS_DIR:-$WORKDIR default is left in the wrapper.
Mac (user): 1861 passed, 56 skipped, 0 failed (PR comment).
Closes #57
When only the registry is moved (`--registry-dir` / `MAM_JOBS_DIR`) and `DELEGATE_JOB_LOGS_DIR` is not set, the audit log now goes in the `delegate_job_logs` next to the registry in use, not in the workspace's `.mam/delegate_job_logs`. The comment-5 disk-fallback bug is fixed in a separate commit on the same branch. The four problems from the Mac verification are fixed in `75c0042`..`a7cb90e`. No version bump; the version is set at merge.
## Sibling rule and call points (b8bed81, 6d182b3)
- `get_logs_dir(registry_dir)` is the only place that resolves the rule. It lives in `logroot.py` since `75c0042`, and `mqtt_common` re-exports it:
- `DELEGATE_JOB_LOGS_DIR` wins when set;
- otherwise the root is the `delegate_job_logs` next to the registry;
- `sibling_logs_dir()` is the pure sibling rule.
- Every writer and reader that knows its registry passes that root:
- `registry`, `engine` (conflict refusals and legacy claims included), `publish_event`, `remote` and `job_subscriber`;
- `reconcile.sh`;
- the loop runner's fingerprint exclusion.
## Binding, shell defaults and scripts (b7a857d)
- `control_plane.from_env()` derives `delegate_job_logs_dir` from the resolved `mam_jobs_dir` unless `DELEGATE_JOB_LOGS_DIR` is set. A session created with only `MAM_JOBS_DIR` moved no longer records the workspace's old log root.
- `worktree_prepare.sh` passes `DELEGATE_JOB_LOGS_DIR` on only when the caller set it.
- `lib.sh` and the delegate-job wrapper no longer carry a bash copy of the default. The wrapper had 16 of them.
- `delegate_publish_cmd`, `delegate_started_recorded` and `_logs_dir_abs` resolve the root with `get_logs_dir(<registry>)`.
- The wrapper fails closed when it cannot resolve the root. Otherwise it exports the root to its children and prints it as `submit`'s last line.
- `logs` takes `--registry-dir`.
- `--dry-run` keeps its meaning: it registers the job and seeds the audit log. No write-time guard checks that the registry record exists (comment §6).
## Worktree panes no longer get the derivable audit-log root (c57e766)
- Before this, a worktree creator's pane always got all four binding keys as environment variables, including `DELEGATE_JOB_LOGS_DIR=<control root>/.mam/delegate_job_logs`. Under "an explicit value wins", `submit --registry-dir <tmp>` from that pane still wrote the real audit log. This is the scene the issue is about.
- `control_plane.forwarded_keys()` now picks the keys forwarded to the pane. It leaves out `delegate_job_logs_dir` when it equals the sibling of `mam_jobs_dir`, since the pane can derive it. An explicitly moved, non-sibling root is still forwarded.
- The row still records all four keys; `describe()`, `forward`, the whitespace check, stop and status are unchanged.
- Create and resume both go through `prefix()`, so this takes effect whenever a pane is spawned.
- A pane that is already running keeps the old variable until it is stopped and resumed (or recreated).
- Rows made before this change need no edit: they already hold the sibling root, so a resume does not forward it.
- `docs/design/worktree-creator-model.md` says the derivable audit-log root is recorded, not forwarded.
## status (cbd8773)
Both `get_job_status` implementations (JSON and text) now also look in the `delegate_job_logs` next to `MAM_JOBS_DIR` and next to the row's `mam_jobs_dir`. They do this after the explicit log roots. So a job whose registry record was cleaned up still shows its audit-only terminal status.
## Comment-5 bug: the disk fallback (a159a79, f299034, c982616)
- **The bug:** `_check_disk_fallback` compared the `status.json` dict from `read_logged_status()` with the terminal status strings. So a terminal status recorded only in the audit log was never recognised. It now reads the dict's `status`.
- **Review P1, found by codex:** after that fix, a redispatched record (attempt 2, `pending`) could be closed by attempt 1's leftover `completed` `status.json`.
- This happens because the audit mirror is written after the registry commits, and a failed mirror leaves the old file in place.
- Fix: a versioned record (`transition_seq`) takes an audit status only when the audit status's sequence is at least as new as the record's.
- An audit status without a sequence is ignored for a versioned record.
- An unversioned record (never transitioned, or `MAM_ENGINE_CAS=0`) and a missing record keep the old rule.
- A `status.json` whose sequence is not an integer is ignored with a warning. Before, it raised out of the subscriber loop.
## Tests (813cfd3, 63d8164 and the review fixes)
- New file `tests/test_issue57_logs_follow_registry.py`. It covers:
- the resolver (L1), `registry.py` (L2), the wrapper's submit/wait/logs (L3) and `publish_event.py` (L4);
- `control_plane.from_env`, `worktree_prepare` and `status.sh` (JSON and text);
- conflict refusals and legacy claims;
- the comment-5 fallback and the redispatch case;
- the invariant that an explicit `DELEGATE_JOB_LOGS_DIR` still wins;
- pane forwarding: `prefix()` (function and CLI, sibling and non-sibling), create (row has the sibling, the pane's `--env` does not, the injected publish commands keep their `DELEGATE_JOB_LOGS_DIR=` prefix), `worktree_prepare`, and a forwarded pane whose `--registry-dir` experiment writes only next to the experiment registry.
- Every test ends by asserting that the workspace's own `.mam/delegate_job_logs` was never created.
- **Against the pre-fix tree (e32c957):** 12 tests fail and 6 teardowns fail the "workspace log root created" assertion. These are behavioral failures (wrong path, workspace root created, missing events), apart from 2 `TypeError`s where a test calls the new `get_logs_dir(registry)` signature. The one test that passes there is the explicit-override invariant, which is expected.
- **Two existing tests change, as comment §4 allows.** `test_issue15_registry_isolation::test_logs_dir_follows_the_same_order` and `test_tier1_unit::test_b9_logs_dir_follows_cwd_changes` each gain `monkeypatch.delenv("MAM_JOBS_DIR", raising=False)`.
- Why: conftest's autouse `_isolate_mam_state_dirs` gives every test a `MAM_JOBS_DIR`, so under the sibling rule the WORKSPACE_ROOT/cwd fallbacks these tests assert would resolve next to that registry instead.
- The order they assert (explicit root, workspace, cwd) is unchanged.
- **Three more existing assertions change, approved by the user (plan §2-B).** They pinned all four keys being forwarded:
- `tests/test_issue21_worktree_prereqs.py` (create, and resume re-forwarding);
- `tests/test_issue21_acceptance.py::test_a2_pane_env_carries_the_binding_not_the_agent_args`.
- They now compare `FORWARDED_ENV = CP_ENV[:3]`. `CP_ENV` and the negative assertions are unchanged; "not forwarded" is asserted positively in the new file.
- No other existing test changed.
## Fixes from the Mac verification (75c0042, 151af99, 1c598bb, a7cb90e)
The Mac run on `63d8164` passed the suite, but two reviewers found four problems. Each fix has a regression test that fails on `63d8164` and passes now.
- **F1, paho dependency (75c0042).** The path rules (`default_registry_dir`, `sibling_logs_dir`, `get_logs_dir`) moved to a new stdlib-only module, `lib_py/delegation/logroot.py`. `mqtt_common` re-exports them.
- `control_plane`, both `status.sh` implementations, `lib.sh` and the wrapper's `_logs_dir_abs` import from `logroot`.
- Importing the package also loads `service` and `publisher`. So `mqtt_common` and `publisher` now import paho optionally, and `make_client()` raises a clear `ImportError` when paho is missing.
- These now work without paho: binding resolution and the pane prefix (create and resume), status (JSON and text), the audit-root resolution in `lib.sh`, and the wrapper's path resolver. Talking to the broker still needs paho.
- **F2, `logs` default path (151af99).** The wrapper's `logs` passes `--registry-dir` on only when it is given, using `${reg[@]+"${reg[@]}"}` so it is safe under bash 3.2 `set -u`. Without the flag, `logs` uses the workspace registry from any cwd again.
- **R1, disk-fallback dispatch identity (1c598bb).** `_check_disk_fallback` re-reads the record just before it takes an audit status. It takes the status only when `dispatch_token`, attempt and `transition_seq` match the first read; otherwise it waits for the next poll.
- This is the smallest of the three directions in the comment. It needs no audit schema change and does not hold the registry lock during audit I/O.
- Before this, an "A error → redispatch → B completed" sequence between the two reads closed attempt A as a success.
- **R2, symlinked registry (a7cb90e).** `sibling_logs_dir()` uses the registry's real path, so `submit` (which uses `pwd -P`) and `logs` given the link path find the same audit log. An explicit `DELEGATE_JOB_LOGS_DIR` still wins.
- **Non-blocking (a7cb90e).**
- `forwarded_keys` normalises both sides of the comparison with `_abs`, so both sides now use the real path.
- The status row in `docs/design/worktree-creator-model.md` lists the lookup next to the registry.
| Regression test (`tests/test_issue57_logs_follow_registry.py`) | 63d8164 | a7cb90e |
|---|---|---|
| `test_control_plane_binding_works_without_paho` | fail | pass |
| `test_status_works_without_paho` (json, text) | fail | pass |
| `test_logs_without_registry_dir_follows_the_workspace_from_any_cwd` | fail | pass |
| `test_disk_fallback_rejects_a_terminal_that_belongs_to_a_later_dispatch` | fail | pass |
| `test_a_symlinked_registry_dir_resolves_to_one_audit_log` | fail | pass |
- The paho-less tests put a `paho` package that fails to import first on `PYTHONPATH`, so the real import path runs as on an interpreter without paho-mqtt.
- The R1 test wraps the audit read and runs real engine transitions between the two reads.
- The whole file has 22 tests, and all pass at `a7cb90e`.
- No other existing test changed in this increment.
## Review
- `/multi-agent-mux-loop --plan --all-reviewer --max-loop 3`.
- Round 1: codex returned NOT PASS (P1 and P2 above).
- Rounds 2 and 3: claude, agy and codex returned PASS on `c982616`.
- grok could not take part in that loop because of a stale weekly-limit dialog. After the limit reset, grok reviewed `c982616` on its own (`--review-only --reviewer reviewer-grok-01`) and returned PASS.
- All four reviewers passed the same head, `c982616`.
- Plan §2-B (stop forwarding the derivable root) had been held for a user decision; it was approved after this PR was opened. Its loop (`--base c982616`) passed all four reviewers in round 1 on `63d8164`. Its one non-blocking nit (`forwarded_keys` did not normalise the row value) is fixed in `a7cb90e`.
- The Mac fixes were reviewed with `--review-only --all-reviewer --base 63d8164`. All four reviewers (claude, agy, grok, codex) passed `a7cb90e` in round 1 with no nits. Each reviewer re-ran the original reproductions on its own.
## Verification by the orchestrator, at a7cb90e
- **Full suite (Linux):** 1891 passed, 20 skipped, 12 failed; the 12 are the known `tests/test_issue10_13_purge.py` failures (#28). Same environment as below; `~/.claude/projects` and the real `.mam/delegate_job_logs` were unchanged.
- **paho-less probe:** `/usr/bin/python3 -m lib_py.control_plane json` (Python 3.14.4, no paho) fails with `No module named 'paho'` at `63d8164` and prints the binding at `a7cb90e`.
- **Not run here:** macOS `/usr/bin/python3` 3.9.6 and bash 3.2. The changed Python parses with the 3.9 grammar, and `bash -n` passes.
## Verification by the orchestrator, at 63d8164
- **Full suite (Linux):** 1885 passed, 20 skipped, 12 failed; the 12 are the known `tests/test_issue10_13_purge.py` failures (#28).
- Run with the agent-pane environment (`CLAUDE_PROJECT_DIR` set), and with `MAM_JOBS_DIR` and `DELEGATE_JOB_LOGS_DIR` unset.
- `~/.claude/projects` and the real `.mam/delegate_job_logs` were the same before and after.
- **Real-agent trial, worktree scene (63d8164):** a real claude created the way `worktree_prepare` does it (workspace = a worktree, binding = the control root), no `DELEGATE_JOB_LOGS_DIR`.
- The row records the control root's audit-log root; the pane has `WORKSPACE_ROOT` and `MAM_JOBS_DIR` but no `DELEGATE_JOB_LOGS_DIR`, after create and again after stop → resume.
- From its own pane the agent ran `submit --dry-run --registry-dir <tmp>/exp/jobs`: the job's audit log is in `<tmp>/exp/delegate_job_logs` only, not in the control root's.
- The onboarding and delegated jobs' logs are in the control root's `.mam/delegate_job_logs`, as before.
- **Real-agent trial, moved registry (c982616):** a real claude on a throwaway herdr session, with only `MAM_JOBS_DIR` moved and `DELEGATE_JOB_LOGS_DIR` unset.
- `create --onboard` exited 0 and onboarding completed.
- The row's binding recorded both `mam_jobs_dir` and the log dir next to the moved registry.
- A delegate job completed.
- The onboarding job's and the delegate job's audit logs are both next to the moved registry.
- The pane's `DELEGATE_JOB_LOGS_DIR` is that sibling.
- The tree's own `.mam/delegate_job_logs` and the real control `.mam/delegate_job_logs` got no entries.
- **Static checks:**
- All changed Python files parse with the Python 3.9 grammar, and no 3.10+ APIs are used.
- `bash -n` passes.
- No `DELEGATE_JOB_LOGS_DIR:-$WORKDIR` default is left in the wrapper.
- **Mac (user):** 1861 passed, 56 skipped, 0 failed (PR comment).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
get_logs_dir(registry_dir) resolves the audit-log root as the
delegate_job_logs next to the registry in use when DELEGATE_JOB_LOGS_DIR
is not set; sibling_logs_dir() is the pure sibling rule. Every writer and
reader that knows its registry (registry, engine, publish_event, remote,
job_subscriber, reconcile.sh, the loop runner's fingerprint exclusion)
passes that root instead of falling back to the workspace default.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
from_env() fills delegate_job_logs_dir with the delegate_job_logs next to
the resolved mam_jobs_dir unless DELEGATE_JOB_LOGS_DIR is set, so a session
created with only MAM_JOBS_DIR moved no longer records the workspace's old
log root. worktree_prepare.sh passes DELEGATE_JOB_LOGS_DIR on only when the
caller set it. lib.sh and the delegate-job wrapper no longer carry a bash
copy of the default: delegate_publish_cmd, delegate_started_recorded and
_logs_dir_abs resolve it with get_logs_dir(<registry>), the wrapper fails
closed when it cannot, exports the resolved root to its children, prints
it as submit's last line, and logs takes --registry-dir.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both get_job_status implementations (JSON and text) try the
delegate_job_logs next to MAM_JOBS_DIR and the row's mam_jobs_dir after the
explicit log roots, so a job whose registry record was cleaned up still
shows its audit-only terminal status when the registry was moved.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
read_logged_status() returns the status.json dict; _check_disk_fallback
compared that dict with the terminal status strings, so a terminal status
recorded only in the audit log was never recognised (Issue #57, comment 5).
The dispatch-token and attempt checks are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
New file tests/test_issue57_logs_follow_registry.py: the resolver (L1),
registry.py (L2), the wrapper's submit/wait/logs (L3), publish_event.py
(L4), control_plane.from_env, worktree_prepare, status.sh, the comment-5
disk fallback, and the invariant that an explicit DELEGATE_JOB_LOGS_DIR
still wins. Every test ends by asserting the workspace's own
.mam/delegate_job_logs was never created.
Two existing tests gain monkeypatch.delenv("MAM_JOBS_DIR"): conftest's
autouse _isolate_mam_state_dirs gives every test a MAM_JOBS_DIR, and under
the sibling rule the WORKSPACE_ROOT/cwd fallbacks they assert would
otherwise resolve next to that registry. The order they assert (explicit
root, workspace, cwd) is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
engine._flush computed the registry's audit-log root but did not pass it to
the conflict_refused append, and _pick_pending_legacy did the same for its
status_changed append, so both landed in the workspace default when the
registry was moved and no override was set (review P2, Issue #57).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The audit mirror is written after the registry commits, so a redispatched
record (attempt 2, pending) can sit next to attempt 1's completed
status.json, and a failed mirror leaves it there. The fallback validated the
token and attempt against the record, which say nothing about the audit
status's provenance, and closed attempt 2 with attempt 1's completion
(review P1, Issue #57). A versioned record (transition_seq) now takes the
audit status only when its transition_seq is at least as new; an audit
status without a sequence cannot prove which attempt it describes and is
ignored by a versioned record. An unversioned record (never transitioned,
or MAM_ENGINE_CAS=0) and a missing record keep the old rule.
Regressions: the redispatch/flush interval, the failed-mirror leftover, the
unversioned audit status, a newer audit-only terminal, and conflict
refusals and legacy claims next to a moved registry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
_check_disk_fallback converted transition_seq outside its try, so a
status.json whose sequence is not an integer would raise out of the
subscriber's main loop; such a status is now ignored with a warning, like a
stale one. delegate_started_recorded returned 1 before looking at the
registry's events.log when the audit-log root could not be resolved; it now
skips only the audit check.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A worktree creator's binding always differs from its --workspace default,
so prefix() put DELEGATE_JOB_LOGS_DIR=<control root>/.mam/delegate_job_logs
into every such pane for good, and the env-first rule then kept an
experiment's --registry-dir from moving its audit log. forwarded_keys()
leaves that key out when it equals the sibling of mam_jobs_dir; a
non-sibling (explicit) root is forwarded as before. describe(), the forward
and whitespace checks and the row's four keys are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
New: prefix()/CLI forward three keys for a sibling binding and four for an
explicit root; create records the sibling on the row, omits it from the
pane's --env and still pins it in the injected publish commands; the
issue's scene (a pane with WORKSPACE_ROOT and MAM_JOBS_DIR only) moves its
audit log with --registry-dir and leaves the control root alone;
worktree_prepare's pane --env carries no DELEGATE_JOB_LOGS_DIR.
The three existing four-key forwarding assertions (user-approved) compare
FORWARDED_ENV = CP_ENV[:3]; CP_ENV and the negative assertions are kept.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
즉 이전 시도(A)는 실패했는데 호출자는 성공으로 끝납니다. seq 는 최신성만 보여 줄 뿐, dispatch 의 identity 를 보장하지 않습니다.
수정 방향(셋 중 하나):
감사 상태를 채택하기 직전에 레지스트리를 다시 읽어, token/attempt/seq 가 같은 관찰에 속하는지 확인합니다. 다르면 거부하거나 다시 시도합니다.
레지스트리 lock 아래에서 일관된 관찰을 보장합니다.
감사 상태에 dispatch identity 를 기록하고 검증합니다.
회귀 테스트: 위 interleaving.
R2 [P2] 레지스트리 디렉터리 자체가 심볼릭 링크이면 submit 과 logs 의 로그 경로가 갈라진다 (codex)
sibling_logs_dir 는 abspath 만 씁니다.
반면 래퍼 submit 의 _abs_registry_dir 는 cd -P/pwd -P 로 링크를 해석하고, control_plane 은 realpath 로 정규화합니다.
logs 는 받은 링크 경로를 그대로 넘깁니다.
재현 (<tmp>/alias/jobs -> <tmp>/actual/jobs):
submit --dry-run --registry-dir <tmp>/alias/jobs 는 rc 0 이고, 감사 로그는 <tmp>/actual/delegate_job_logs/<id> 에 생깁니다.
같은 인수로 logs <id> --registry-dir <tmp>/alias/jobs 를 실행하면 rc 1 입니다(no audit log ... under <tmp>/alias/delegate_job_logs).
/tmp -> /private/tmp 처럼 부모 전체를 가리키는 별칭은 문제없습니다. jobs 디렉터리 자체가 다른 부모 아래로 연결되는 링크일 때만 생깁니다.
수정 방향:
중앙 resolver 에서 레지스트리의 realpath 를 기준으로 형제 경로를 구합니다. DELEGATE_JOB_LOGS_DIR 명시값이 우선하는 것은 그대로 둡니다.
회귀 테스트: 같은 심볼릭 링크 인수로 submit --dry-run 한 뒤 logs.
비차단 메모
docs/design/worktree-creator-model.md 의 status 행에 새로 추가된 "레지스트리 옆 감사 로그" 조회 후보가 빠져 있습니다.
control_plane.forwarded_keys 는 로그 쪽 값을 정규화하지 않고 비교합니다.
실제 흐름에서는 양쪽이 이미 realpath 라 문제가 없고, 어긋나도 전달하는 쪽(기존 동작)이라 안전합니다.
양쪽에 _abs 를 걸면 의도가 더 분명해집니다.
수정 커밋을 fix/issue-57 에 push 해 주시면, 같은 절차로 다시 검증하겠습니다: 4인 리뷰 → Mac 전체 스위트 → 버전 올림 → 병합.
## 병합 전 검증 결과 (macOS · Python 3.9.6 · bash 3.2): 수정 필요 4건
head `63d8164` 기준으로 검증했습니다.
**전체 스위트 (이 Mac): 1861 passed, 56 skipped, 0 failed**
- skip 은 Python 3.9 의 `tomllib` 부재와 worktree 의 서브모듈 부재로 생긴 것들이며, 기존과 같습니다.
- `test_issue10_13_purge.py`(#28)는 이 Mac 에서 통과했습니다.
- 실제 herdr 서버 pane 목록과 `~/.claude/projects` 는 실행 전후가 같습니다.
- 실제 `.mam/delegate_job_logs` 에 새로 생긴 것은 리뷰 잡의 감사 로그뿐이고, 모두 레지스트리 레코드가 짝으로 있습니다.
**4인 리뷰: agy PASS, grok PASS, claude NOT PASS, codex NOT PASS**
- 아래 4건은 스위트가 다루지 않는 경우라, 리뷰어들이 탐침으로 재현했습니다.
- 수정 전 코드(base)와 비교해 확인했습니다.
- 4건 모두 설계를 바꿀 필요 없는 국소 수정입니다.
### F1 [차단] 경로 헬퍼 때문에 create/resume 바인딩과 status 가 paho 를 요구하게 됐다 (claude)
- `lib_py/control_plane.py` 가 `from lib_py.delegation import mqtt_common` 을 새로 import 합니다. base 의 control_plane 은 표준 라이브러리만 썼습니다.
- `multi-agent-mux-status/scripts/status.sh` 의 두 `get_job_status` 도 `mqtt_common.sibling_logs_dir` 를 import 합니다.
- 그런데 `mqtt_common` 은 모듈 맨 위에서 `paho.mqtt.client` 를 import 합니다.
- 이 경로들의 인터프리터는 `_delegate_py_bin` 이 고르고, 마지막 폴백이 PATH 의 `python3` 입니다(README 에도 적혀 있음). macOS 의 `/usr/bin/python3` 3.9.6 에는 paho 가 없습니다.
- 재현 (`AGENT_PYTHON_BIN=/usr/bin/python3`):
- `mam_control_plane_json <wt>`: base rc=0, head rc=1 (`ModuleNotFoundError: No module named 'paho'`)
- status.sh 의 `get_job_status` (위임 잡이 있는 행): base `('<id>', 'unknown')`, head `ModuleNotFoundError`
- 영향:
- 이 폴백에서 create/resume 의 바인딩 계산이 실패합니다. worktree 대상이면 exit 2 로 거부되고, 제어 루트면 `control_plane` 없이 기록됩니다.
- 위임 잡이 걸린 행이 하나라도 있으면 읽기 전용 `status.sh` 가 예외로 죽습니다.
- 수정 방향:
- `sibling_logs_dir` 를 paho 를 import 하지 않는 모듈(예: `lib_py/delegation/logroot.py`)로 옮깁니다.
- `mqtt_common` 은 그것을 재노출하고, `control_plane`·`status.sh` 는 그 모듈에서 import 합니다. 또는 `mqtt_common` 의 paho import 를 지연시켜도 됩니다.
- 회귀 테스트: paho 없는 인터프리터에서 `control_plane json` 과 `get_job_status` 가 동작하는지.
### F2 [차단] 래퍼 `logs` 의 기본 경로가 바뀌었다: 레지스트리를 옮기지 않은 평소 실행 (claude)
- `cmd_logs` 가 이제 항상 `--registry-dir "$reg"` 를 넘깁니다. 그런데 기본값 `REGISTRY_DIR_DEFAULT="${MAM_JOBS_DIR:-.mam/jobs}"` 는 cwd 기준입니다.
- base 는 `--registry-dir` 없이 `registry.py logs` 를 불렀습니다. 그래서 `default_registry_dir()` 가 `$WORKSPACE_ROOT` 를 따랐습니다.
- 재현 (`WORKSPACE_ROOT=ws` 에 잡 등록, cwd=`elsewhere`):
- base: `logs --list` 가 그 잡을 나열합니다.
- head: `(no logged jobs under <elsewhere>/.mam/delegate_job_logs)`
- 수정 방향:
- `--registry-dir` 가 주어졌을 때만 넘깁니다(`local reg=()` 배열 + `${reg[@]+"${reg[@]}"}`).
- 회귀 테스트: cwd 가 워크스페이스가 아닐 때의 `logs`.
### R1 [P1] 디스크 폴백이 다른 dispatch 의 완료를 이전 dispatch 의 성공으로 받아들인다 (codex)
- `job_subscriber._check_disk_fallback` 은 레지스트리 레코드(status/token/attempt/transition_seq)를 먼저 읽고, 나중에 읽은 감사 `status.json` 의 seq 가 그 이상이면 채택합니다.
- 하지만 토큰/attempt 검증은 처음 읽은 레코드로만 합니다. 감사 상태에는 토큰/attempt 가 기록되지 않습니다.
- 재현: codex 가 실제 engine 전이로 아래 순서를 결정적으로 만들었습니다.
1. 토큰 A 로 running 인 잡에 대해 expected token A 로 폴백을 실행합니다.
2. 레지스트리 읽기와 감사 읽기 사이에 다음 전이가 일어납니다: A `error` → `redispatch(attempt_id=2)` → 새 토큰 B 로 `completed`.
3. 결과: `pending=[]`, `terminal={<id>: "completed"}`, `completed [disk-fallback]`
- 즉 이전 시도(A)는 실패했는데 호출자는 성공으로 끝납니다. seq 는 최신성만 보여 줄 뿐, dispatch 의 identity 를 보장하지 않습니다.
- 수정 방향(셋 중 하나):
- 감사 상태를 채택하기 직전에 레지스트리를 다시 읽어, token/attempt/seq 가 같은 관찰에 속하는지 확인합니다. 다르면 거부하거나 다시 시도합니다.
- 레지스트리 lock 아래에서 일관된 관찰을 보장합니다.
- 감사 상태에 dispatch identity 를 기록하고 검증합니다.
- 회귀 테스트: 위 interleaving.
### R2 [P2] 레지스트리 디렉터리 자체가 심볼릭 링크이면 submit 과 logs 의 로그 경로가 갈라진다 (codex)
- `sibling_logs_dir` 는 `abspath` 만 씁니다.
- 반면 래퍼 submit 의 `_abs_registry_dir` 는 `cd -P`/`pwd -P` 로 링크를 해석하고, `control_plane` 은 realpath 로 정규화합니다.
- `logs` 는 받은 링크 경로를 그대로 넘깁니다.
- 재현 (`<tmp>/alias/jobs -> <tmp>/actual/jobs`):
- `submit --dry-run --registry-dir <tmp>/alias/jobs` 는 rc 0 이고, 감사 로그는 `<tmp>/actual/delegate_job_logs/<id>` 에 생깁니다.
- 같은 인수로 `logs <id> --registry-dir <tmp>/alias/jobs` 를 실행하면 rc 1 입니다(`no audit log ... under <tmp>/alias/delegate_job_logs`).
- `/tmp -> /private/tmp` 처럼 부모 전체를 가리키는 별칭은 문제없습니다. jobs 디렉터리 자체가 다른 부모 아래로 연결되는 링크일 때만 생깁니다.
- 수정 방향:
- 중앙 resolver 에서 레지스트리의 realpath 를 기준으로 형제 경로를 구합니다. `DELEGATE_JOB_LOGS_DIR` 명시값이 우선하는 것은 그대로 둡니다.
- 회귀 테스트: 같은 심볼릭 링크 인수로 `submit --dry-run` 한 뒤 `logs`.
### 비차단 메모
- `docs/design/worktree-creator-model.md` 의 status 행에 새로 추가된 "레지스트리 옆 감사 로그" 조회 후보가 빠져 있습니다.
- `control_plane.forwarded_keys` 는 로그 쪽 값을 정규화하지 않고 비교합니다.
- 실제 흐름에서는 양쪽이 이미 realpath 라 문제가 없고, 어긋나도 전달하는 쪽(기존 동작)이라 안전합니다.
- 양쪽에 `_abs` 를 걸면 의도가 더 분명해집니다.
수정 커밋을 `fix/issue-57` 에 push 해 주시면, 같은 절차로 다시 검증하겠습니다: 4인 리뷰 → Mac 전체 스위트 → 버전 올림 → 병합.
control_plane and status.sh imported sibling_logs_dir from mqtt_common,
which imports paho at module level, so create/resume's binding and status
died on an interpreter without paho-mqtt (_delegate_py_bin's last fallback
is the PATH's python3). The registry/audit-log path rules move to
lib_py/delegation/logroot.py (standard library only) and mqtt_common
re-exports them. The package's own import chain also reached paho through
service -> publish_event -> publisher, so mqtt_common and publisher import
paho tolerantly and make_client() refuses with a clear error when a broker
is actually needed. control_plane, status.sh, lib.sh's delegate_publish_cmd
and delegate_logs_dir, and the wrapper's _logs_dir_abs import from logroot.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cmd_logs always passed the wrapper's cwd-relative default registry, so the
usual run (registry not moved, cwd not the workspace) no longer found its
jobs. Without the option registry.py's own default decides, as before:
MAM_JOBS_DIR, then WORKSPACE_ROOT, then the cwd.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The record (status, token, attempt, transition_seq) was read before the
audit status.json; if A error -> redispatch (attempt 2) -> B completed
happened in between, the audit terminal was B's while the token and attempt
validated were A's, and A's caller was told "completed". The audit status
is now taken only when a fresh read of the record still shows the same
token, attempt and transition_seq; otherwise the job stays pending and the
next round reads both afresh. The no-record case is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A registry directory that is itself a symlink got two audit logs: submit
resolved --registry-dir with pwd -P and wrote next to the target, while logs
derived the sibling from the link path. sibling_logs_dir() now takes the
registry's realpath; an explicit DELEGATE_JOB_LOGS_DIR still wins.
Also: forwarded_keys() normalises both sides of its comparison, and the
design doc's status row lists the audit log next to the registry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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
Godopu
merged commit 26e2c43753 into main2026-10-15 13:52:10 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #57
When only the registry is moved (
--registry-dir/MAM_JOBS_DIR) andDELEGATE_JOB_LOGS_DIRis not set, the audit log now goes in thedelegate_job_logsnext to the registry in use, not in the workspace's.mam/delegate_job_logs. The comment-5 disk-fallback bug is fixed in a separate commit on the same branch. The four problems from the Mac verification are fixed in75c0042..a7cb90e. No version bump; the version is set at merge.Sibling rule and call points (
b8bed81,6d182b3)get_logs_dir(registry_dir)is the only place that resolves the rule. It lives inlogroot.pysince75c0042, andmqtt_commonre-exports it:DELEGATE_JOB_LOGS_DIRwins when set;delegate_job_logsnext to the registry;sibling_logs_dir()is the pure sibling rule.registry,engine(conflict refusals and legacy claims included),publish_event,remoteandjob_subscriber;reconcile.sh;Binding, shell defaults and scripts (
b7a857d)control_plane.from_env()derivesdelegate_job_logs_dirfrom the resolvedmam_jobs_dirunlessDELEGATE_JOB_LOGS_DIRis set. A session created with onlyMAM_JOBS_DIRmoved no longer records the workspace's old log root.worktree_prepare.shpassesDELEGATE_JOB_LOGS_DIRon only when the caller set it.lib.shand the delegate-job wrapper no longer carry a bash copy of the default. The wrapper had 16 of them.delegate_publish_cmd,delegate_started_recordedand_logs_dir_absresolve the root withget_logs_dir(<registry>).submit's last line.logstakes--registry-dir.--dry-runkeeps its meaning: it registers the job and seeds the audit log. No write-time guard checks that the registry record exists (comment §6).Worktree panes no longer get the derivable audit-log root (
c57e766)DELEGATE_JOB_LOGS_DIR=<control root>/.mam/delegate_job_logs. Under "an explicit value wins",submit --registry-dir <tmp>from that pane still wrote the real audit log. This is the scene the issue is about.control_plane.forwarded_keys()now picks the keys forwarded to the pane. It leaves outdelegate_job_logs_dirwhen it equals the sibling ofmam_jobs_dir, since the pane can derive it. An explicitly moved, non-sibling root is still forwarded.describe(),forward, the whitespace check, stop and status are unchanged.prefix(), so this takes effect whenever a pane is spawned.docs/design/worktree-creator-model.mdsays the derivable audit-log root is recorded, not forwarded.status (
cbd8773)Both
get_job_statusimplementations (JSON and text) now also look in thedelegate_job_logsnext toMAM_JOBS_DIRand next to the row'smam_jobs_dir. They do this after the explicit log roots. So a job whose registry record was cleaned up still shows its audit-only terminal status.Comment-5 bug: the disk fallback (
a159a79,f299034,c982616)_check_disk_fallbackcompared thestatus.jsondict fromread_logged_status()with the terminal status strings. So a terminal status recorded only in the audit log was never recognised. It now reads the dict'sstatus.pending) could be closed by attempt 1's leftovercompletedstatus.json.transition_seq) takes an audit status only when the audit status's sequence is at least as new as the record's.MAM_ENGINE_CAS=0) and a missing record keep the old rule.status.jsonwhose sequence is not an integer is ignored with a warning. Before, it raised out of the subscriber loop.Tests (
813cfd3,63d8164and the review fixes)tests/test_issue57_logs_follow_registry.py. It covers:registry.py(L2), the wrapper's submit/wait/logs (L3) andpublish_event.py(L4);control_plane.from_env,worktree_prepareandstatus.sh(JSON and text);DELEGATE_JOB_LOGS_DIRstill wins;prefix()(function and CLI, sibling and non-sibling), create (row has the sibling, the pane's--envdoes not, the injected publish commands keep theirDELEGATE_JOB_LOGS_DIR=prefix),worktree_prepare, and a forwarded pane whose--registry-direxperiment writes only next to the experiment registry..mam/delegate_job_logswas never created.e32c957): 12 tests fail and 6 teardowns fail the "workspace log root created" assertion. These are behavioral failures (wrong path, workspace root created, missing events), apart from 2TypeErrors where a test calls the newget_logs_dir(registry)signature. The one test that passes there is the explicit-override invariant, which is expected.test_issue15_registry_isolation::test_logs_dir_follows_the_same_orderandtest_tier1_unit::test_b9_logs_dir_follows_cwd_changeseach gainmonkeypatch.delenv("MAM_JOBS_DIR", raising=False)._isolate_mam_state_dirsgives every test aMAM_JOBS_DIR, so under the sibling rule the WORKSPACE_ROOT/cwd fallbacks these tests assert would resolve next to that registry instead.tests/test_issue21_worktree_prereqs.py(create, and resume re-forwarding);tests/test_issue21_acceptance.py::test_a2_pane_env_carries_the_binding_not_the_agent_args.FORWARDED_ENV = CP_ENV[:3].CP_ENVand the negative assertions are unchanged; "not forwarded" is asserted positively in the new file.Fixes from the Mac verification (
75c0042,151af99,1c598bb,a7cb90e)The Mac run on
63d8164passed the suite, but two reviewers found four problems. Each fix has a regression test that fails on63d8164and passes now.75c0042). The path rules (default_registry_dir,sibling_logs_dir,get_logs_dir) moved to a new stdlib-only module,lib_py/delegation/logroot.py.mqtt_commonre-exports them.control_plane, bothstatus.shimplementations,lib.shand the wrapper's_logs_dir_absimport fromlogroot.serviceandpublisher. Somqtt_commonandpublishernow import paho optionally, andmake_client()raises a clearImportErrorwhen paho is missing.lib.sh, and the wrapper's path resolver. Talking to the broker still needs paho.logsdefault path (151af99). The wrapper'slogspasses--registry-diron only when it is given, using${reg[@]+"${reg[@]}"}so it is safe under bash 3.2set -u. Without the flag,logsuses the workspace registry from any cwd again.1c598bb)._check_disk_fallbackre-reads the record just before it takes an audit status. It takes the status only whendispatch_token, attempt andtransition_seqmatch the first read; otherwise it waits for the next poll.a7cb90e).sibling_logs_dir()uses the registry's real path, sosubmit(which usespwd -P) andlogsgiven the link path find the same audit log. An explicitDELEGATE_JOB_LOGS_DIRstill wins.a7cb90e).forwarded_keysnormalises both sides of the comparison with_abs, so both sides now use the real path.docs/design/worktree-creator-model.mdlists the lookup next to the registry.tests/test_issue57_logs_follow_registry.py)63d8164a7cb90etest_control_plane_binding_works_without_pahotest_status_works_without_paho(json, text)test_logs_without_registry_dir_follows_the_workspace_from_any_cwdtest_disk_fallback_rejects_a_terminal_that_belongs_to_a_later_dispatchtest_a_symlinked_registry_dir_resolves_to_one_audit_logpahopackage that fails to import first onPYTHONPATH, so the real import path runs as on an interpreter without paho-mqtt.a7cb90e.Review
/multi-agent-mux-loop --plan --all-reviewer --max-loop 3.c982616.c982616on its own (--review-only --reviewer reviewer-grok-01) and returned PASS.c982616.--base c982616) passed all four reviewers in round 1 on63d8164. Its one non-blocking nit (forwarded_keysdid not normalise the row value) is fixed ina7cb90e.--review-only --all-reviewer --base 63d8164. All four reviewers (claude, agy, grok, codex) passeda7cb90ein round 1 with no nits. Each reviewer re-ran the original reproductions on its own.Verification by the orchestrator, at
a7cb90etests/test_issue10_13_purge.pyfailures (#28). Same environment as below;~/.claude/projectsand the real.mam/delegate_job_logswere unchanged./usr/bin/python3 -m lib_py.control_plane json(Python 3.14.4, no paho) fails withNo module named 'paho'at63d8164and prints the binding ata7cb90e./usr/bin/python33.9.6 and bash 3.2. The changed Python parses with the 3.9 grammar, andbash -npasses.Verification by the orchestrator, at
63d8164tests/test_issue10_13_purge.pyfailures (#28).CLAUDE_PROJECT_DIRset), and withMAM_JOBS_DIRandDELEGATE_JOB_LOGS_DIRunset.~/.claude/projectsand the real.mam/delegate_job_logswere the same before and after.63d8164): a real claude created the wayworktree_preparedoes it (workspace = a worktree, binding = the control root), noDELEGATE_JOB_LOGS_DIR.WORKSPACE_ROOTandMAM_JOBS_DIRbut noDELEGATE_JOB_LOGS_DIR, after create and again after stop → resume.submit --dry-run --registry-dir <tmp>/exp/jobs: the job's audit log is in<tmp>/exp/delegate_job_logsonly, not in the control root's..mam/delegate_job_logs, as before.c982616): a real claude on a throwaway herdr session, with onlyMAM_JOBS_DIRmoved andDELEGATE_JOB_LOGS_DIRunset.create --onboardexited 0 and onboarding completed.mam_jobs_dirand the log dir next to the moved registry.DELEGATE_JOB_LOGS_DIRis that sibling..mam/delegate_job_logsand the real control.mam/delegate_job_logsgot no entries.bash -npasses.DELEGATE_JOB_LOGS_DIR:-$WORKDIRdefault is left in the wrapper.🤖 Generated with Claude Code
New file tests/test_issue57_logs_follow_registry.py: the resolver (L1), registry.py (L2), the wrapper's submit/wait/logs (L3), publish_event.py (L4), control_plane.from_env, worktree_prepare, status.sh, the comment-5 disk fallback, and the invariant that an explicit DELEGATE_JOB_LOGS_DIR still wins. Every test ends by asserting the workspace's own .mam/delegate_job_logs was never created. Two existing tests gain monkeypatch.delenv("MAM_JOBS_DIR"): conftest's autouse _isolate_mam_state_dirs gives every test a MAM_JOBS_DIR, and under the sibling rule the WORKSPACE_ROOT/cwd fallbacks they assert would otherwise resolve next to that registry. The order they assert (explicit root, workspace, cwd) is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>병합 전 검증 결과 (macOS · Python 3.9.6 · bash 3.2): 수정 필요 4건
head
63d8164기준으로 검증했습니다.전체 스위트 (이 Mac): 1861 passed, 56 skipped, 0 failed
tomllib부재와 worktree 의 서브모듈 부재로 생긴 것들이며, 기존과 같습니다.test_issue10_13_purge.py(#28)는 이 Mac 에서 통과했습니다.~/.claude/projects는 실행 전후가 같습니다..mam/delegate_job_logs에 새로 생긴 것은 리뷰 잡의 감사 로그뿐이고, 모두 레지스트리 레코드가 짝으로 있습니다.4인 리뷰: agy PASS, grok PASS, claude NOT PASS, codex NOT PASS
F1 [차단] 경로 헬퍼 때문에 create/resume 바인딩과 status 가 paho 를 요구하게 됐다 (claude)
lib_py/control_plane.py가from lib_py.delegation import mqtt_common을 새로 import 합니다. base 의 control_plane 은 표준 라이브러리만 썼습니다.multi-agent-mux-status/scripts/status.sh의 두get_job_status도mqtt_common.sibling_logs_dir를 import 합니다.mqtt_common은 모듈 맨 위에서paho.mqtt.client를 import 합니다._delegate_py_bin이 고르고, 마지막 폴백이 PATH 의python3입니다(README 에도 적혀 있음). macOS 의/usr/bin/python33.9.6 에는 paho 가 없습니다.AGENT_PYTHON_BIN=/usr/bin/python3):mam_control_plane_json <wt>: base rc=0, head rc=1 (ModuleNotFoundError: No module named 'paho')get_job_status(위임 잡이 있는 행): base('<id>', 'unknown'), headModuleNotFoundErrorcontrol_plane없이 기록됩니다.status.sh가 예외로 죽습니다.sibling_logs_dir를 paho 를 import 하지 않는 모듈(예:lib_py/delegation/logroot.py)로 옮깁니다.mqtt_common은 그것을 재노출하고,control_plane·status.sh는 그 모듈에서 import 합니다. 또는mqtt_common의 paho import 를 지연시켜도 됩니다.control_plane json과get_job_status가 동작하는지.F2 [차단] 래퍼
logs의 기본 경로가 바뀌었다: 레지스트리를 옮기지 않은 평소 실행 (claude)cmd_logs가 이제 항상--registry-dir "$reg"를 넘깁니다. 그런데 기본값REGISTRY_DIR_DEFAULT="${MAM_JOBS_DIR:-.mam/jobs}"는 cwd 기준입니다.--registry-dir없이registry.py logs를 불렀습니다. 그래서default_registry_dir()가$WORKSPACE_ROOT를 따랐습니다.WORKSPACE_ROOT=ws에 잡 등록, cwd=elsewhere):logs --list가 그 잡을 나열합니다.(no logged jobs under <elsewhere>/.mam/delegate_job_logs)--registry-dir가 주어졌을 때만 넘깁니다(local reg=()배열 +${reg[@]+"${reg[@]}"}).logs.R1 [P1] 디스크 폴백이 다른 dispatch 의 완료를 이전 dispatch 의 성공으로 받아들인다 (codex)
job_subscriber._check_disk_fallback은 레지스트리 레코드(status/token/attempt/transition_seq)를 먼저 읽고, 나중에 읽은 감사status.json의 seq 가 그 이상이면 채택합니다.error→redispatch(attempt_id=2)→ 새 토큰 B 로completed.pending=[],terminal={<id>: "completed"},completed [disk-fallback]R2 [P2] 레지스트리 디렉터리 자체가 심볼릭 링크이면 submit 과 logs 의 로그 경로가 갈라진다 (codex)
sibling_logs_dir는abspath만 씁니다._abs_registry_dir는cd -P/pwd -P로 링크를 해석하고,control_plane은 realpath 로 정규화합니다.logs는 받은 링크 경로를 그대로 넘깁니다.<tmp>/alias/jobs -> <tmp>/actual/jobs):submit --dry-run --registry-dir <tmp>/alias/jobs는 rc 0 이고, 감사 로그는<tmp>/actual/delegate_job_logs/<id>에 생깁니다.logs <id> --registry-dir <tmp>/alias/jobs를 실행하면 rc 1 입니다(no audit log ... under <tmp>/alias/delegate_job_logs)./tmp -> /private/tmp처럼 부모 전체를 가리키는 별칭은 문제없습니다. jobs 디렉터리 자체가 다른 부모 아래로 연결되는 링크일 때만 생깁니다.DELEGATE_JOB_LOGS_DIR명시값이 우선하는 것은 그대로 둡니다.submit --dry-run한 뒤logs.비차단 메모
docs/design/worktree-creator-model.md의 status 행에 새로 추가된 "레지스트리 옆 감사 로그" 조회 후보가 빠져 있습니다.control_plane.forwarded_keys는 로그 쪽 값을 정규화하지 않고 비교합니다._abs를 걸면 의도가 더 분명해집니다.수정 커밋을
fix/issue-57에 push 해 주시면, 같은 절차로 다시 검증하겠습니다: 4인 리뷰 → Mac 전체 스위트 → 버전 올림 → 병합.