## 개요
Refs #21 — 워크트리 운영 모델 **1단계 선행 작업**(작업 항목 1·2·3·6). 항목 4·5·7·8은 이 PR 범위 밖이라 이슈는 열어 둔다.
이 브랜치는 `design/worktree-model`(`fe96db1`, `dcf2fab`: 설계 문서와 리뷰 4건) 위에서 시작했으므로, 두 설계 커밋이 함께 포함된다.
## 변경 사항
| 항목 | 커밋 | 내용 |
|---|---|---|
| 1 | `d3d3a76` | delegate-job 래퍼: 주입 문구의 brief 경로, 시작 명령의 `--registry-dir`, 감사 로그 루트, Output Report Path 를 절대경로로 |
| 2 | `d907359` | 루프 러너 `--worktree` / `--base` / `--review-only`, 변경 집합이 비면 fail-closed. 제어 평면(wrapper·레지스트리·체크포인트)은 본 체크아웃에 둔다 |
| 3 | `ee1d896` | create/resume/stop 이 제어 평면 바인딩을 행 필드로 영속화·전파(`lib_py/control_plane.py` 신설), status/reconcile 이 `MAM_JOBS_DIR` 을 존중 |
| 6 | `b61c8b1` | 설계 문서 정정: "현재 코드 기준"을 부분 지원으로, 1단계 리뷰 전용 흐름, 스킬별 호환성 표 |
| 리뷰 R1–R3 | `af8a54c` | 재사용 pane 에 env 를 에이전트 인자로 넘기지 않음, 상대 `MAM_JOBS_DIR` 에 jobs dir 하나, stop 이 감사 로그 루트 유지 |
| 리뷰 N1 | `13259ed` | h25 가 바뀐 env 전달 계약을 고정 |
| 리뷰 N2 | `4a81d15` | T2-8 을 `mam_sandbox` 에서 실행해 실제 `.mam/shim` 에 쓰지 않게 |
## 리뷰 (multi-agent-mux-loop)
- **루프 1** (`--plan --all-reviewer`, max-loop 3): 3회차에서 claude·agy·grok **PASS** @ `13259ed`, codex **NOT PASS**(N2 한 건)로 반복 한도 소진. codex 의 이전 지적 R1–R3·N1 은 모두 해결 확인.
- **루프 2** (N2 후속, 리뷰어 codex 단독): codex **PASS** @ `4a81d15`. `13259ed` 이후 변경은 테스트 파일 하나(+8/-4)뿐이다.
## 테스트
- 전체 스위트 @ `13259ed`: **908 passed, 33 failed**. 실패한 33건을 base `dcf2fab`(archive)에서 같은 조건으로 다시 돌린 결과 **33건 모두 동일하게 실패** → 이 브랜치가 만든 실패 0건.
- 실패는 환경 요인이다: nats-docker 서브모듈 미초기화(deploy 18), purge 12, Python 3.14 argparse help 캡처 1, `/bin/sh`=dash 의 `source` 1, 실제 브로커가 설정된 `.mam.env` 로 인한 `t1_4` 1.
- @ `4a81d15`: `tests/test_issue21_worktree_prereqs.py` + `tests/test_herdr_shim_contract.py` **52 passed**.
## 후속 (다음 작업에서 #21 항목 4·5·7 과 함께 처리)
리뷰어가 비차단으로 남긴 권고:
1. `create_session.sh`·`resume_session.sh` 의 `mam_control_plane_json … || true` 가 실패를 조용히 삼킨다 → stderr 경고, `--workspace ≠ WORKSPACE_ROOT` 이면 exit 2.
2. resume 의 "herdr 가 이미 살아 있음" 조기 종료 경로가 `CONTROL_PLANE_JSON` 을 넘기지 않는다.
3. T2-6 이 `os.environ` 을 직접 바꾼다.
4. `test_issue14_delivery_ack.py::_job_for_inprocess_publish` 가 ambient `WORKSPACE_ROOT` 때문에 실제 `.mam/delegate_job_logs` 에 감사 로그를 남긴다(#20 후속).
5. acceptance5 가 공용 브로커 지연에 흔들린다 → 원격 runner 가 `started --heartbeat` 로 무이벤트 창을 덮거나, 테스트의 `--heartbeat-misses` 를 늘린다.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01PfoijjCS2U6TQPFuxAE5Pn
Design proposal for review (no code change): why the checkout-path
workspace assumption blocks parallel issues and caused the #18 checkout
switch, three candidate models, the recommended hybrid (writers isolated
per worktree, readers shared, one .mam control plane via env overrides),
how it maps onto today's scripts, what has to change, and the questions
put to the reviewers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
claude, agy, grok: PASS with amendments; codex: NOT PASS (recipe overstates
current env-override support; review identity and lifecycle ownership
underspecified). Direction (creator per worktree, shared reviewer pool)
agreed by all four.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
The injected prompt, the start/success/error commands and the brief's Output
Report Path used cwd-relative `.mam/jobs/...`, and the wrapper ignored
MAM_JOBS_DIR. A creator whose cwd is a git worktree could not run them as
written, and its audit log fell back to <cwd>/.mam/delegate_job_logs.
- REGISTRY_DIR defaults to ${MAM_JOBS_DIR:-.mam/jobs} and is made absolute
right after `cd "$WORKDIR"` (a relative value still resolves against WORKDIR)
- the publish command is shell-quoted (printf %q) and prefixed with
DELEGATE_JOB_LOGS_DIR=<abs>, resolved by the same get_logs_dir() the
wrapper's subscriber uses; the heartbeat timer inherits it
- direct and loop jobs share one _instructions helper; brief and report paths
are $REGISTRY_DIR/$JOB_ID/... (absolute)
- create --onboard: absolute publish_event.py, --registry-dir and log root
Tests T1-1..T1-5 (a space in the control root, commands run from another cwd
with no workspace env).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
- --worktree <path> (CLI over MAM_LOOP_WORKTREE): the source root. Only the
diff, base, head and the empty-set check use it; the wrapper, registry,
checkpoint, loop guard, promotion and cleanup stay on the control root. The
collector is the control root's diff_collect.sh (never the worktree's
unreviewed copy), its arguments shlex-quoted. Validation: an existing
directory, the top of its working tree, the same git common dir as the
control root (".git" is resolved against the directory it came from).
- --base <ref> (CLI only, no env - a stale sha would silently widen the next
range): resolved to a full sha, must be an ancestor of the source HEAD.
- --review-only: requires --base, refuses --plan; Phase 2 is skipped and
logged; correction and comply jobs still go to the creator.
- With --worktree or --base every delegated prompt starts with a context
block (path, branch, base and its origin, head, base..head commit count,
uncommitted changes); without them prompts are byte-identical. All call
sites in run() go through one _dispatch helper.
- Fail-closed guard (only with --worktree/--base): before each review round,
self-review included, an empty change set or one git cannot determine
(exit codes, never output parsing) ends the loop with rc 1 instead of
asking for reviews. This closes the fail-open where a loop started after
an out-of-loop commit sent reviewers "(no changes since base commit)".
- The runner honours MAM_JOBS_DIR and passes --registry-dir to the wrapper.
- run_loop.sh accepts the three options; the legacy shell loop
(MAM_LOOP_PY=0) refuses them before the freeze and the loop lock.
Tests T2-1..T2-14.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
A session row now records the control plane it belongs to, and the pane
gets it explicitly instead of relying on env inheritance.
- lib_py/control_plane.py (+ thin lib.sh wrappers mam_control_plane_json /
mam_control_plane_prefix): the binding is WORKSPACE_ROOT,
AGENT_SESSIONS_YAML, MAM_JOBS_DIR, DELEGATE_JOB_LOGS_DIR as absolute paths.
- create: the binding is always persisted on the row (`control_plane`) and
forwarded to the pane only when it differs from what --workspace alone
implies, as leading K=V tokens that lib.sh's new-session already turns into
--env (fresh pane) or `env K=V` (reused idle pane). pane.cmd_full and
start_command stay unprefixed. A binding with whitespace that would have to
be forwarded is refused with exit 2 (lib.sh splits the flags unquoted); the
~/.local/bin wrapper path records the binding but warns it is not forwarded.
- resume: the row's binding wins over the caller's env (a difference is
reported on stderr) and is forwarded again; a legacy row without one uses
the caller's binding and update_yaml_resumed.sh records it.
- stop: registry order MAM_JOBS_DIR > the row's mam_jobs_dir >
$WORKSPACE_ROOT/.mam/jobs > .mam/jobs, exported for delegate_publish_event.
- status: both get_job_status copies look in MAM_JOBS_DIR, the row's
registry and the matching audit logs before the old candidates.
- reconcile: MAM_JOBS_DIR wins in the subscribe heredoc and is passed in.
Tests T3-0..T3-8 (mock herdr: forwarding is verified at the herdr-call level
only; real pane env propagation is left to item 7). T3-7 is a static guard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
Fixes the sentences the four design reviews flagged and records what phase 1
actually implements: status line, monitor-topic scope, option A's isolation
and capacity wording, creator/reviewer ownership (tests write; promotion is
the control root's), the control-plane binding persisted and forwarded, two
loop recipes ((a) the loop implements, (b) the base recorded before an
out-of-loop implementation, then --base --review-only; no base in --task),
the loop lock as a refusal, "phase 1 = parallel implementation, one review
loop", the eight Issue #21 items with their status, and a new section 7 with
root definitions and a per-skill compatibility table. The review files are
unchanged. The loop SKILL.md lock note names the lock branch instead of line
numbers that moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
Review 9a1adb46 (planner-reviewer-codex-01):
- R1 [P1] lib.sh new-session: a forwarded binding reached the agent as
arguments. With env flags the idle-pane branch prefixed `env K=V ...` to
the command that `herdr agent start ... --kind <agent> -- <args>` passes to
the agent executable. An idle pane is now reused only when no environment
has to be set; otherwise the pane is created by pane split / workspace
create, which take --env. The dead `env` prefix code is removed (its
send-text fallback would have typed the same line).
- R2 [P2] runner: a relative MAM_JOBS_DIR is resolved once against the
control root, the way the wrapper (cwd=workspace_root) reads it, and that
absolute dir is used for delegation, wait, report lookup and cleanup.
- R3 [P2] stop: the row's control_plane.delegate_job_logs_dir is restored
(exported unless the caller set DELEGATE_JOB_LOGS_DIR), so the
cancellation history lands in the row's audit-log root.
Tests: T3-1 and T3-3 now assert that the binding arrives as --env on the
pane-creation calls and that no env/K=V token follows `--` in agent start
(fresh and reused pane); T3-4c (audit root on stop) and T2-15 (relative
MAM_JOBS_DIR) are new. The design doc's compatibility table and a create
comment no longer describe the removed idle-pane env path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
Review N1 (planner-reviewer-claude-01 50ae9fb0, planner-reviewer-codex-01
8779f4b4): af8a54c (R1) stopped reusing an idle pane when env flags are
present, because the old path put `env K=V ...` after `herdr agent start ...
--`, where herdr passes it to the agent executable as arguments. h25 still
required that old behaviour (idle pane reused, env in the agent command) and
failed. It now asserts the new contract instead: a pane split / workspace
create carries MY_TEST_FLAG=active and HERDR_WORKSPACE_ID as --env, the agent
does not land on the idle pane, and nothing env-like follows `--` in agent
start. Finding A's intent (env must not be silently lost) is kept; reuse
without env flags stays pinned by h24. No product code changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
run_loop.sh sources lib.sh before it refuses --worktree/--base/--review-only
under MAM_LOOP_PY=0, and sourcing lib.sh runs _init_herdr_isolation, which
rewrites $WORKSPACE_ROOT/.mam/shim/herdr. T2-8 copied the caller's
environment as is, so that root was the caller's control root (normally the
real checkout). The test now requests mam_sandbox, pins WORKSPACE_ROOT to it
explicitly, runs from it, and asserts the shim was written there; the four
parametrized refusal cases and their three assertions are unchanged. No
product code changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
Issue #21 phase-1 prerequisites (PR #22) add loop runner options and the
control-plane module: MINOR. VERSIONS.md gains the v4.6.0 entry, the
archived design reviews lose their markdown trailing whitespace so
`git diff --check` is clean, and the three merge-side PASS reports
(claude, agy, codex @ 4a81d15) are archived under .agents/reports/.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
병합 워크스페이스 재검토 결과 (PR head 4a81d15를 detached 워크트리에 받아 검증, 본 체크아웃은 main 유지):
리뷰: claude·agy·codex 3인 [VERDICT: PASS] (grok 은 한도로 정지, 작성 측 루프의 PASS @13259ed 인정). 리포트는 .agents/reports/ 에 보관.
전체 스위트 @4a81d15 (이 환경, 서브모듈 초기화·py3.9 venv): 940 passed, 1 failed — test_wrapper_idle_grace_never_outlives_the_wall_clock 는 부하 시 경계값(+8 s)에 걸리는 기존 시간 판정으로 단독 재실행 통과. PR 회귀 없음.
병합 전 추가 커밋 d34b532: 버전 락스텝 4.5.0 → 4.6.0(MINOR: 러너 옵션 3개·control_plane.py)과 VERSIONS.md 항목, 보관된 리뷰 md 의 trailing whitespace 정리(git diff --check 클린), 재검토 리포트 3건 보관.
비차단 후속(#21·#20 으로 이관): mam_control_plane_json … || true 가 실패를 삼킴, resume 의 "herdr 이미 살아 있음" 경로 CONTROL_PLANE_JSON 미전달, T2-6 os.environ 직접 변경, issue14 _job_for_inprocess_publish 의 ambient WORKSPACE_ROOT, acceptance5 가 공용 브로커에서 동시 실행 시 같은 세션 이름의 러너끼리 dispatch 를 소비(세션 이름 uuid4 화), create 레시피에 MAM_NO_SUBSCRIBER_DAEMON=1.
병합 워크스페이스 재검토 결과 (PR head `4a81d15`를 detached 워크트리에 받아 검증, 본 체크아웃은 main 유지):
- 리뷰: claude·agy·codex 3인 `[VERDICT: PASS]` (grok 은 한도로 정지, 작성 측 루프의 PASS @13259ed 인정). 리포트는 `.agents/reports/` 에 보관.
- 전체 스위트 @4a81d15 (이 환경, 서브모듈 초기화·py3.9 venv): **940 passed, 1 failed** — `test_wrapper_idle_grace_never_outlives_the_wall_clock` 는 부하 시 경계값(+8 s)에 걸리는 기존 시간 판정으로 단독 재실행 통과. PR 회귀 없음.
- 병합 전 추가 커밋 `d34b532`: 버전 락스텝 4.5.0 → **4.6.0**(MINOR: 러너 옵션 3개·`control_plane.py`)과 VERSIONS.md 항목, 보관된 리뷰 md 의 trailing whitespace 정리(`git diff --check` 클린), 재검토 리포트 3건 보관.
- 비차단 후속(#21·#20 으로 이관): `mam_control_plane_json … || true` 가 실패를 삼킴, resume 의 "herdr 이미 살아 있음" 경로 `CONTROL_PLANE_JSON` 미전달, T2-6 `os.environ` 직접 변경, issue14 `_job_for_inprocess_publish` 의 ambient `WORKSPACE_ROOT`, acceptance5 가 공용 브로커에서 동시 실행 시 같은 세션 이름의 러너끼리 dispatch 를 소비(세션 이름 uuid4 화), create 레시피에 `MAM_NO_SUBSCRIBER_DAEMON=1`.
Godopu
merged commit 70e36350b9 into main2026-09-14 12:54:13 +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.
개요
Refs #21 — 워크트리 운영 모델 1단계 선행 작업(작업 항목 1·2·3·6). 항목 4·5·7·8은 이 PR 범위 밖이라 이슈는 열어 둔다.
이 브랜치는
design/worktree-model(fe96db1,dcf2fab: 설계 문서와 리뷰 4건) 위에서 시작했으므로, 두 설계 커밋이 함께 포함된다.변경 사항
d3d3a76--registry-dir, 감사 로그 루트, Output Report Path 를 절대경로로d907359--worktree/--base/--review-only, 변경 집합이 비면 fail-closed. 제어 평면(wrapper·레지스트리·체크포인트)은 본 체크아웃에 둔다ee1d896lib_py/control_plane.py신설), status/reconcile 이MAM_JOBS_DIR을 존중b61c8b1af8a54cMAM_JOBS_DIR에 jobs dir 하나, stop 이 감사 로그 루트 유지13259ed4a81d15mam_sandbox에서 실행해 실제.mam/shim에 쓰지 않게리뷰 (multi-agent-mux-loop)
--plan --all-reviewer, max-loop 3): 3회차에서 claude·agy·grok PASS @13259ed, codex NOT PASS(N2 한 건)로 반복 한도 소진. codex 의 이전 지적 R1–R3·N1 은 모두 해결 확인.4a81d15.13259ed이후 변경은 테스트 파일 하나(+8/-4)뿐이다.테스트
13259ed: 908 passed, 33 failed. 실패한 33건을 basedcf2fab(archive)에서 같은 조건으로 다시 돌린 결과 33건 모두 동일하게 실패 → 이 브랜치가 만든 실패 0건./bin/sh=dash 의source1, 실제 브로커가 설정된.mam.env로 인한t1_41.4a81d15:tests/test_issue21_worktree_prereqs.py+tests/test_herdr_shim_contract.py52 passed.후속 (다음 작업에서 #21 항목 4·5·7 과 함께 처리)
리뷰어가 비차단으로 남긴 권고:
create_session.sh·resume_session.sh의mam_control_plane_json … || true가 실패를 조용히 삼킨다 → stderr 경고,--workspace ≠ WORKSPACE_ROOT이면 exit 2.CONTROL_PLANE_JSON을 넘기지 않는다.os.environ을 직접 바꾼다.test_issue14_delivery_ack.py::_job_for_inprocess_publish가 ambientWORKSPACE_ROOT때문에 실제.mam/delegate_job_logs에 감사 로그를 남긴다(#20 후속).started --heartbeat로 무이벤트 창을 덮거나, 테스트의--heartbeat-misses를 늘린다.🤖 Generated with Claude Code
https://claude.ai/code/session_01PfoijjCS2U6TQPFuxAE5Pn
The injected prompt, the start/success/error commands and the brief's Output Report Path used cwd-relative `.mam/jobs/...`, and the wrapper ignored MAM_JOBS_DIR. A creator whose cwd is a git worktree could not run them as written, and its audit log fell back to <cwd>/.mam/delegate_job_logs. - REGISTRY_DIR defaults to ${MAM_JOBS_DIR:-.mam/jobs} and is made absolute right after `cd "$WORKDIR"` (a relative value still resolves against WORKDIR) - the publish command is shell-quoted (printf %q) and prefixed with DELEGATE_JOB_LOGS_DIR=<abs>, resolved by the same get_logs_dir() the wrapper's subscriber uses; the heartbeat timer inherits it - direct and loop jobs share one _instructions helper; brief and report paths are $REGISTRY_DIR/$JOB_ID/... (absolute) - create --onboard: absolute publish_event.py, --registry-dir and log root Tests T1-1..T1-5 (a space in the control root, commands run from another cwd with no workspace env). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G- --worktree <path> (CLI over MAM_LOOP_WORKTREE): the source root. Only the diff, base, head and the empty-set check use it; the wrapper, registry, checkpoint, loop guard, promotion and cleanup stay on the control root. The collector is the control root's diff_collect.sh (never the worktree's unreviewed copy), its arguments shlex-quoted. Validation: an existing directory, the top of its working tree, the same git common dir as the control root (".git" is resolved against the directory it came from). - --base <ref> (CLI only, no env - a stale sha would silently widen the next range): resolved to a full sha, must be an ancestor of the source HEAD. - --review-only: requires --base, refuses --plan; Phase 2 is skipped and logged; correction and comply jobs still go to the creator. - With --worktree or --base every delegated prompt starts with a context block (path, branch, base and its origin, head, base..head commit count, uncommitted changes); without them prompts are byte-identical. All call sites in run() go through one _dispatch helper. - Fail-closed guard (only with --worktree/--base): before each review round, self-review included, an empty change set or one git cannot determine (exit codes, never output parsing) ends the loop with rc 1 instead of asking for reviews. This closes the fail-open where a loop started after an out-of-loop commit sent reviewers "(no changes since base commit)". - The runner honours MAM_JOBS_DIR and passes --registry-dir to the wrapper. - run_loop.sh accepts the three options; the legacy shell loop (MAM_LOOP_PY=0) refuses them before the freeze and the loop lock. Tests T2-1..T2-14. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G병합 워크스페이스 재검토 결과 (PR head
4a81d15를 detached 워크트리에 받아 검증, 본 체크아웃은 main 유지):[VERDICT: PASS](grok 은 한도로 정지, 작성 측 루프의 PASS @13259ed 인정). 리포트는.agents/reports/에 보관.test_wrapper_idle_grace_never_outlives_the_wall_clock는 부하 시 경계값(+8 s)에 걸리는 기존 시간 판정으로 단독 재실행 통과. PR 회귀 없음.d34b532: 버전 락스텝 4.5.0 → 4.6.0(MINOR: 러너 옵션 3개·control_plane.py)과 VERSIONS.md 항목, 보관된 리뷰 md 의 trailing whitespace 정리(git diff --check클린), 재검토 리포트 3건 보관.mam_control_plane_json … || true가 실패를 삼킴, resume 의 "herdr 이미 살아 있음" 경로CONTROL_PLANE_JSON미전달, T2-6os.environ직접 변경, issue14_job_for_inprocess_publish의 ambientWORKSPACE_ROOT, acceptance5 가 공용 브로커에서 동시 실행 시 같은 세션 이름의 러너끼리 dispatch 를 소비(세션 이름 uuid4 화), create 레시피에MAM_NO_SUBSCRIBER_DAEMON=1.