fix(delegation): the sibling rule pairs with the registry's real path (Issue #57, Mac R2)

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>
This commit is contained in:
GodopuandClaude Fable 5.1 committed 2026-10-15 00:06:30 +00:00
1 parent 1c598bb546
commit a7cb90e63a
4 files changed
+45 -5

No files matched your search

+2 -1
View File
@@ -79,7 +79,8 @@ def forwarded_keys(desc: dict) -> list:
when it is the sibling of the registry and so derivable there (Issue #57). when it is the sibling of the registry and so derivable there (Issue #57).
An explicit, non-sibling root is forwarded as before.""" An explicit, non-sibling root is forwarded as before."""
keys = list(KEYS) keys = list(KEYS)
if desc.get("delegate_job_logs_dir") == _abs(sibling_logs_dir(desc["mam_jobs_dir"])): logs = desc.get("delegate_job_logs_dir")
if logs and _abs(logs) == _abs(sibling_logs_dir(desc["mam_jobs_dir"])):
keys.remove("delegate_job_logs_dir") keys.remove("delegate_job_logs_dir")
return keys return keys
+10 -3
View File
@@ -39,9 +39,16 @@ def default_registry_dir() -> str:
def sibling_logs_dir(registry_dir: str) -> str: def sibling_logs_dir(registry_dir: str) -> str:
"""The audit-log root that pairs with ``registry_dir``: its sibling """The audit-log root that pairs with ``registry_dir``: the
``delegate_job_logs``. Pure; reads no environment. Issue #57.""" ``delegate_job_logs`` next to it. Pure; reads no environment. Issue #57.
return os.path.join(os.path.dirname(os.path.abspath(registry_dir)), "delegate_job_logs")
The registry is taken by its real path: a registry directory that is itself
a symlink pairs with the audit log next to its target, which is what the
wrapper (``pwd -P``) and the control-plane binding (``realpath``) already
see, so submit and logs agree (Mac review R2). A relative registry resolves
against the cwd, as registry.py opens it.
"""
return os.path.join(os.path.dirname(os.path.realpath(registry_dir)), "delegate_job_logs")
def get_logs_dir(registry_dir: Optional[str] = None) -> str: def get_logs_dir(registry_dir: Optional[str] = None) -> str:
+1 -1
View File
@@ -126,7 +126,7 @@ bash "$MAIN/.agents/skills/multi-agent-mux-stop/scripts/worktree_remove.sh" --pa
| create | env 에서 결정 → 행 `control_plane` 에 항상 영속. `--workspace` 기본값과 다르면 새 pane 의 생성 호출(`pane split`/`workspace create`)에 바인딩(유도 가능한 감사 로그 루트 제외)을 `--env` 로 전파한다. 전파가 필요하면 idle pane 을 재사용하지 않는다(`agent start -- <args>` 는 `--` 뒤를 에이전트 인자로 넘기므로 `env K=V` 를 쓸 수 없다). 전파가 필요한데 경로에 공백이 있으면 exit 2. `~/.local/bin` wrapper 경로는 기록만 하고 경고 | pane.cwd = `--workspace`. 바인딩 계산 실패 시 워크트리면 exit 2, 제어 루트면 경고 | — (루프 A: 즉시 reconcile·watchdog 은 제어 루트 코드와 `WORKSPACE_ROOT` 로 기동, prepare 가 `MAM_NO_SUBSCRIBER_DAEMON=1`) | | create | env 에서 결정 → 행 `control_plane` 에 항상 영속. `--workspace` 기본값과 다르면 새 pane 의 생성 호출(`pane split`/`workspace create`)에 바인딩(유도 가능한 감사 로그 루트 제외)을 `--env` 로 전파한다. 전파가 필요하면 idle pane 을 재사용하지 않는다(`agent start -- <args>` 는 `--` 뒤를 에이전트 인자로 넘기므로 `env K=V` 를 쓸 수 없다). 전파가 필요한데 경로에 공백이 있으면 exit 2. `~/.local/bin` wrapper 경로는 기록만 하고 경고 | pane.cwd = `--workspace`. 바인딩 계산 실패 시 워크트리면 exit 2, 제어 루트면 경고 | — (루프 A: 즉시 reconcile·watchdog 은 제어 루트 코드와 `WORKSPACE_ROOT` 로 기동, prepare 가 `MAM_NO_SUBSCRIBER_DAEMON=1`) |
| resume | 행 바인딩 우선(호출자 env 와 다르면 stderr 경고) 재전파. 바인딩 없는 기존 행은 호출자 바인딩으로 채움. 이미 살아 있는 세션에도 바인딩을 영속한다. 계산 실패 시 워크트리면 exit 2 | 원래 cwd 로만: 경로 불일치·부재 시 exit 2 | 워크트리 삭제 시 재개 불가(대화 보존, 같은 경로에 다시 만들면 재개) | | resume | 행 바인딩 우선(호출자 env 와 다르면 stderr 경고) 재전파. 바인딩 없는 기존 행은 호출자 바인딩으로 채움. 이미 살아 있는 세션에도 바인딩을 영속한다. 계산 실패 시 워크트리면 exit 2 | 원래 cwd 로만: 경로 불일치·부재 시 exit 2 | 워크트리 삭제 시 재개 불가(대화 보존, 같은 경로에 다시 만들면 재개) |
| stop | `MAM_JOBS_DIR` > 행의 `mam_jobs_dir` > `$WORKSPACE_ROOT/.mam/jobs` 순으로 잡 조회·발행. 감사 로그도 `DELEGATE_JOB_LOGS_DIR` > 행의 `delegate_job_logs_dir` 순 | 행 cwd 로 uuid 캡처·purge. 워크트리 해체는 `worktree_remove.sh` 가 판정(잠금·세션·잡·청결·서브모듈) 뒤에만 정지·제거, 서브모듈 워크트리는 검증 후 `--force` 1회 | — | | stop | `MAM_JOBS_DIR` > 행의 `mam_jobs_dir` > `$WORKSPACE_ROOT/.mam/jobs` 순으로 잡 조회·발행. 감사 로그도 `DELEGATE_JOB_LOGS_DIR` > 행의 `delegate_job_logs_dir` 순 | 행 cwd 로 uuid 캡처·purge. 워크트리 해체는 `worktree_remove.sh` 가 판정(잠금·세션·잡·청결·서브모듈) 뒤에만 정지·제거, 서브모듈 워크트리는 검증 후 `--force` 1회 | — |
| status | `MAM_JOBS_DIR`·행 `mam_jobs_dir`·감사 로그(`DELEGATE_JOB_LOGS_DIR`·행 `delegate_job_logs_dir`)·기존 후보 순 조회 | 행 cwd 로 대화 키 | — | | status | `MAM_JOBS_DIR`·행 `mam_jobs_dir`·감사 로그(`DELEGATE_JOB_LOGS_DIR`·행 `delegate_job_logs_dir`)·레지스트리 옆 감사 로그(`MAM_JOBS_DIR`·행 `mam_jobs_dir` 의 형제 `delegate_job_logs`, #57)·기존 후보 순 조회 | 행 cwd 로 대화 키 | — |
| monitor/reconcile | `MAM_JOBS_DIR` 우선(정적 가드로만 검증) | 기존 행은 행 cwd 로 검증 | 루트 밖 cwd 의 미등록 세션은 자동 등록 안 됨 → create/prepare 로만 등록 (남은 제한) | | monitor/reconcile | `MAM_JOBS_DIR` 우선(정적 가드로만 검증) | 기존 행은 행 cwd 로 검증 | 루트 밖 cwd 의 미등록 세션은 자동 등록 안 됨 → create/prepare 로만 등록 (남은 제한) |
| delegate-job | `--registry-dir` > `MAM_JOBS_DIR` > `.mam/jobs`(`--workdir` 기준), 모두 절대화. 발행 명령이 감사 로그 루트(`DELEGATE_JOB_LOGS_DIR`)를 고정 | 문구·명령·리포트 경로 절대 | 작은따옴표가 든 경로는 지원 범위 밖 | | delegate-job | `--registry-dir` > `MAM_JOBS_DIR` > `.mam/jobs`(`--workdir` 기준), 모두 절대화. 발행 명령이 감사 로그 루트(`DELEGATE_JOB_LOGS_DIR`)를 고정 | 문구·명령·리포트 경로 절대 | 작은따옴표가 든 경로는 지원 범위 밖 |
| loop | `MAM_JOBS_DIR` 존중, 래퍼에 `--registry-dir` 명시. `--worktree` 로 소스 루트 분리 | diff·base·head 만 소스 루트. **구현이 루프 밖이면 `--base <구현 전 sha> --review-only` 필수**. `--worktree`/`--base` 가 있으면 빈 변경분 거부 | 루프 하나(8). 원격 리뷰어는 identity 모드에서 거부(기본) 또는 diff-only(`--remote-review diff-only`); 원격 리뷰어가 트리를 검증하게 하는 스냅샷 공유(공유 원격의 `refs/mam/review/<loop_id>/<n>`)는 범위 밖. 레거시 셸 루프(`MAM_LOOP_PY=0`)는 새 옵션 거부 | | loop | `MAM_JOBS_DIR` 존중, 래퍼에 `--registry-dir` 명시. `--worktree` 로 소스 루트 분리 | diff·base·head 만 소스 루트. **구현이 루프 밖이면 `--base <구현 전 sha> --review-only` 필수**. `--worktree`/`--base` 가 있으면 빈 변경분 거부 | 루프 하나(8). 원격 리뷰어는 identity 모드에서 거부(기본) 또는 diff-only(`--remote-review diff-only`); 원격 리뷰어가 트리를 검증하게 하는 스냅샷 공유(공유 원격의 `refs/mam/review/<loop_id>/<n>`)는 범위 밖. 레거시 셸 루프(`MAM_LOOP_PY=0`)는 새 옵션 거부 |
@@ -599,3 +599,35 @@ def test_disk_fallback_rejects_a_terminal_that_belongs_to_a_later_dispatch(moved
pending, terminal = {jid}, {} pending, terminal = {jid}, {}
job_subscriber._check_disk_fallback(pending, str(reg), terminal, {jid: tok_a}, {jid: 1}) job_subscriber._check_disk_fallback(pending, str(reg), terminal, {jid: tok_a}, {jid: 1})
assert pending == {jid} and terminal == {} assert pending == {jid} and terminal == {}
# ---------------------------------------------------------------------------
# Mac review R2: a registry directory that is itself a symlink has one audit log
# ---------------------------------------------------------------------------
def test_a_symlinked_registry_dir_resolves_to_one_audit_log(tmp_path, monkeypatch):
"""submit resolves --registry-dir with pwd -P, so its audit log lands next
to the link's target; the resolver must pair the same place for logs."""
from lib_py.delegation import logroot
for k in DROP:
monkeypatch.delenv(k, raising=False)
ws = tmp_path / "ws"
(ws / ".mam").mkdir(parents=True)
monkeypatch.setenv("WORKSPACE_ROOT", str(ws))
actual = tmp_path / "actual" / "jobs"
actual.mkdir(parents=True)
alias = tmp_path / "alias"
alias.mkdir()
(alias / "jobs").symlink_to(actual)
real_actual = Path(os.path.realpath(tmp_path / "actual"))
assert logroot.sibling_logs_dir(str(alias / "jobs")) == str(real_actual / "delegate_job_logs")
res = _wrapper(ws, "submit", "--dry-run", "--agent", "human", "--registry-dir", str(alias / "jobs"),
"--workdir", str(ws), "--prompt", "t")
assert res.returncode == 0, res.stdout[-800:] + res.stderr[-800:]
jid = _only_job_id(actual)
assert res.stdout.strip().splitlines()[-1] == str(real_actual / "delegate_job_logs" / jid)
assert (real_actual / "delegate_job_logs" / jid / "meta.json").is_file()
assert not (tmp_path / "alias" / "delegate_job_logs").exists()
res = _wrapper(ws, "logs", "--registry-dir", str(alias / "jobs"), jid)
assert res.returncode == 0 and "registered" in res.stdout, res.stdout + res.stderr
assert not (ws / ".mam" / "delegate_job_logs").exists()