From a7cb90e63abf454ccf22f6ff887cc25393755402 Mon Sep 17 00:00:00 2001 From: Godopu Date: Thu, 15 Oct 2026 00:06:30 +0000 Subject: [PATCH] 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 --- .agents/skills/lib_py/control_plane.py | 3 +- .agents/skills/lib_py/delegation/logroot.py | 13 +++++++-- docs/design/worktree-creator-model.md | 2 +- tests/test_issue57_logs_follow_registry.py | 32 +++++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/.agents/skills/lib_py/control_plane.py b/.agents/skills/lib_py/control_plane.py index 01b139e..29f5959 100644 --- a/.agents/skills/lib_py/control_plane.py +++ b/.agents/skills/lib_py/control_plane.py @@ -79,7 +79,8 @@ def forwarded_keys(desc: dict) -> list: when it is the sibling of the registry and so derivable there (Issue #57). An explicit, non-sibling root is forwarded as before.""" 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") return keys diff --git a/.agents/skills/lib_py/delegation/logroot.py b/.agents/skills/lib_py/delegation/logroot.py index b6d6492..10328c1 100644 --- a/.agents/skills/lib_py/delegation/logroot.py +++ b/.agents/skills/lib_py/delegation/logroot.py @@ -39,9 +39,16 @@ def default_registry_dir() -> str: def sibling_logs_dir(registry_dir: str) -> str: - """The audit-log root that pairs with ``registry_dir``: its sibling - ``delegate_job_logs``. Pure; reads no environment. Issue #57.""" - return os.path.join(os.path.dirname(os.path.abspath(registry_dir)), "delegate_job_logs") + """The audit-log root that pairs with ``registry_dir``: the + ``delegate_job_logs`` next to it. Pure; reads no environment. Issue #57. + + 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: diff --git a/docs/design/worktree-creator-model.md b/docs/design/worktree-creator-model.md index 9422c60..782ba64 100644 --- a/docs/design/worktree-creator-model.md +++ b/docs/design/worktree-creator-model.md @@ -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 -- ` 는 `--` 뒤를 에이전트 인자로 넘기므로 `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 | 워크트리 삭제 시 재개 불가(대화 보존, 같은 경로에 다시 만들면 재개) | | 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 로만 등록 (남은 제한) | | 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//`)는 범위 밖. 레거시 셸 루프(`MAM_LOOP_PY=0`)는 새 옵션 거부 | diff --git a/tests/test_issue57_logs_follow_registry.py b/tests/test_issue57_logs_follow_registry.py index 28c3673..53f595b 100644 --- a/tests/test_issue57_logs_follow_registry.py +++ b/tests/test_issue57_logs_follow_registry.py @@ -599,3 +599,35 @@ def test_disk_fallback_rejects_a_terminal_that_belongs_to_a_later_dispatch(moved pending, terminal = {jid}, {} job_subscriber._check_disk_fallback(pending, str(reg), terminal, {jid: tok_a}, {jid: 1}) 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()