feat(issue-21): 워크트리 운영 모델 1단계 선행 작업 — 경로 절대화, 루프 --worktree, 제어 평면 바인딩, 설계 문서 정정 #22

Merged
Godopu merged 10 commits from feat/issue-21-worktree-prereqs into main 2026-09-14 12:54:13 +00:00
Owner

개요

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.ai/code/session_01PfoijjCS2U6TQPFuxAE5Pn

## 개요 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
Godopu added 9 commits 2026-09-14 10:03:06 +00:00
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
Godopu added 1 commit 2026-09-14 12:53:54 +00:00
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
Author
Owner

병합 워크스페이스 재검토 결과 (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 main 2026-09-14 12:54:13 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tmpl/multi-agent-mux#22