diff --git a/.agents/skills/lib_py/loop/runner.py b/.agents/skills/lib_py/loop/runner.py index b864b56..406eabc 100644 --- a/.agents/skills/lib_py/loop/runner.py +++ b/.agents/skills/lib_py/loop/runner.py @@ -578,7 +578,7 @@ class LoopRunner: r = self._git(src, "rev-list", "--count", f"{base}..HEAD") if r.returncode == 0 and r.stdout.strip(): count = r.stdout.strip() - st = self._git(src, "status", "--porcelain") + st = self._git(src, "status", "--porcelain", "--untracked-files=all") dirty = "알 수 없음" if st.returncode != 0 else ("있음" if st.stdout.strip() else "없음") # a remote reviewer (diff-only) cannot reach this host's paths (B6) src_line = ("- 소스 경로: (오케스트레이터 호스트 전용 — 원격 리뷰어는 접근할 수 없음)\n" if remote diff --git a/.agents/skills/multi-agent-mux-create/SKILL.md b/.agents/skills/multi-agent-mux-create/SKILL.md index 08fa0fa..dcc06c9 100644 --- a/.agents/skills/multi-agent-mux-create/SKILL.md +++ b/.agents/skills/multi-agent-mux-create/SKILL.md @@ -243,7 +243,7 @@ bash .agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh \ [--creator-agent claude [--session ] [--role creator] [--herdr-session ] [--herdr-workspace ] [--onboard]] [--dry-run] ``` -- Creates the worktree next to the control root (a path **inside** it is refused), idempotently; creates the worktree's **own** `.venv` from the control root's `.venv/bin/python` and installs `requirements.txt` / `requirements-dev.txt` when present (never a link - a leftover link is refused with exit 1; no control-root interpreter is a warning and `VENV=skipped`); adds `/.venv` and `.pytest_cache/` to the shared `info/exclude` (a linked worktree's exclude file is `$GIT_COMMON_DIR/info/exclude`, so it applies to the main checkout too). +- Creates the worktree next to the control root (a path **inside** it is refused), idempotently; creates the worktree's **own** `.venv` from the control root's `.venv/bin/python` and installs the delegate-job skill's `requirements.txt` (paho-mqtt, PyYAML - what `deploy/install.sh` installs) plus `requirements.txt` / `requirements-dev.txt` when present (needs network access to the package index; a failed install removes the half-made venv and exits 1 so a re-run starts over; never a link - a leftover link is refused with exit 1; no control-root interpreter is a warning and `VENV=skipped`); adds `/.venv` and `.pytest_cache/` to the shared `info/exclude` (a linked worktree's exclude file is `$GIT_COMMON_DIR/info/exclude`, so it applies to the main checkout too). - **Submodules are opt-in** (`--submodules`). Initialisation writes `submodule..url` to the shared `.git/config`, and any attempt - even a failed one - leaves a `modules/` directory that makes a plain `git worktree remove` refuse. A failed `--submodules` undoes only what that attempt created - new submodule checkouts are deinitialised before their git dirs are deleted, so the worktree stays a usable checkout; submodules initialised earlier and pre-existing `submodule.*` values are kept - and exits 1; re-run without `--submodules` to finish. - `--creator-agent` creates the creator through this skill with the control root's binding and `MAM_NO_SUBSCRIBER_DAEMON=1`; the row's `pane.cwd` is the worktree. - Prints `BASE=`: record it **before** delegating the implementation, then review with `run_loop.sh --worktree --base "$BASE" --review-only`. diff --git a/.agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh b/.agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh index 99f426a..fd16030 100755 --- a/.agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh +++ b/.agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh @@ -79,7 +79,7 @@ fi if [ "$DRY_RUN" = "1" ]; then echo "[dry-run] control root: $MAIN" echo "[dry-run] worktree: $WT (branch $BRANCH${BASE_REF:+, base $BASE_REF})" - echo "[dry-run] own .venv (from $MAIN/.venv/bin/python + requirements*.txt) and shared info/exclude entries; submodules: $([ "$SUBMODULES" = 1 ] && echo init || echo skipped)" + echo "[dry-run] own .venv (from $MAIN/.venv/bin/python + the delegate-job requirements.txt and requirements*.txt) and shared info/exclude entries; submodules: $([ "$SUBMODULES" = 1 ] && echo init || echo skipped)" [ -n "$CREATOR_AGENT" ] && echo "[dry-run] creator: $CREATOR_AGENT as $SESSION (role $ROLE)" exit 0 fi @@ -122,12 +122,16 @@ elif [ -x "$WT/.venv/bin/python" ]; then elif [ -x "$MAIN/.venv/bin/python" ]; then "$MAIN/.venv/bin/python" -m venv "$WT/.venv" || { echo "ERROR: could not create '$WT/.venv'" >&2; exit 1; } req=() - for f in requirements.txt requirements-dev.txt; do + # the same files deploy/install.sh installs into the control root's venv + for f in .agents/skills/multi-agent-mux-delegate-job/requirements.txt requirements.txt requirements-dev.txt; do [ -f "$WT/$f" ] && req+=(-r "$WT/$f") done if [ "${#req[@]}" -gt 0 ]; then - "$WT/.venv/bin/python" -m pip install -q "${req[@]}" \ - || { echo "ERROR: pip install into '$WT/.venv' failed; fix it and re-run (the worktree is kept)" >&2; exit 1; } + if ! "$WT/.venv/bin/python" -m pip install -q "${req[@]}"; then + rm -rf "$WT/.venv" # review D1: a half-made venv would be reported as kept by the next run + echo "ERROR: pip install into '$WT/.venv' failed (network to the package index is needed); the venv was removed, the worktree is kept - fix it and re-run" >&2 + exit 1 + fi fi VENV_STATE="created" else diff --git a/docs/design/worktree-creator-model.md b/docs/design/worktree-creator-model.md index 313ed43..5d3e652 100644 --- a/docs/design/worktree-creator-model.md +++ b/docs/design/worktree-creator-model.md @@ -88,7 +88,7 @@ bash "$MAIN/.agents/skills/multi-agent-mux-stop/scripts/worktree_remove.sh" --pa - **1단계 운영 형태 = 구현 병렬, 리뷰 루프 하나**: creator 구현은 루프 밖(delegate-job direct)에서 워크트리마다 병렬로 한다. 리뷰는 `run_loop.sh --worktree --base <구현 전 sha> --review-only` 로 한 번에 하나씩 한다. base 를 구현 위임 **전에** 기록하지 않으면 리뷰를 시작할 수 없다(가드가 빈 변경분을 거부한다). busy 처리는 큐가 아니다(1회 재시도). 다중 루프는 항목 8(리뷰어 스케줄러) 이후다. - 준비·해체 헬퍼는 항목 4 로 구현했다(`worktree_prepare.sh`, `worktree_remove.sh`). - `.mam.env` 는 복사하지 않는다. creator 에는 제어 루트의 것을 `MAM_ENV_FILE` 로 준다. - - `.venv` 는 워크트리 자체의 것이다: 제어 루트의 `.venv/bin/python` 으로 만들고 `requirements*.txt` 를 설치한다. 심링크는 만들지 않으며 남아 있으면 prepare 가 거부한다(링크는 제어 루트 환경을 그대로 가리키고, 워크트리의 `git add -A` 가 링크 자체를 커밋한 적이 있다). 무시 규칙(`/.venv`, `.pytest_cache/`)은 linked worktree 가 공유하는 `$GIT_COMMON_DIR/info/exclude` 에 들어간다. 그래서 본 체크아웃에도 적용되며, 둘 다 본 체크아웃에서도 무시해야 하는 산출물이다. + - `.venv` 는 워크트리 자체의 것이다: 제어 루트의 `.venv/bin/python` 으로 만들고 delegate-job 스킬의 `requirements.txt`(paho-mqtt·PyYAML, `deploy/install.sh` 가 설치하는 것)와 `requirements*.txt` 를 설치한다. 설치가 실패하면 반쯤 만든 venv 를 지우고 exit 1 한다(다음 실행이 kept 로 오판하지 않도록). 심링크는 만들지 않으며 남아 있으면 prepare 가 거부한다(링크는 제어 루트 환경을 그대로 가리키고, 워크트리의 `git add -A` 가 링크 자체를 커밋한 적이 있다). 무시 규칙(`/.venv`, `.pytest_cache/`)은 linked worktree 가 공유하는 `$GIT_COMMON_DIR/info/exclude` 에 들어간다. 그래서 본 체크아웃에도 적용되며, 둘 다 본 체크아웃에서도 무시해야 하는 산출물이다. - 서브모듈은 `--submodules` 로만 초기화한다. 초기화는 성공·실패와 무관하게 워크트리 git 디렉터리에 `modules/` 를 남기고, 그러면 git 이 force 없는 `worktree remove` 를 거부한다. 초기화하지 않은 서브모듈 경로에 이미 파일이 있으면 아무것도 바꾸지 않고 exit 3 으로 거부한다. prepare 는 실패한 시도가 만든 것만 되돌리고 exit 1 한다. 새 체크아웃은 git 디렉터리보다 먼저 deinit 해서 워크트리가 쓸 수 있는 체크아웃으로 남게 하고, 새 `submodule.*` 공유 설정 키는 지운다. 이전 초기화 상태와 기존 설정 값은 보존한다. - 서브모듈을 초기화한 워크트리는 remove 가 수정(untracked 포함, `--untracked-files=all` 이라 `status.showUntrackedFiles` 설정으로 숨길 수 없다), HEAD 와 모든 로컬 ref(tag·notes 포함)에서 도달하는, 원격에 없는 커밋(서브모듈은 detached HEAD 다), stash 를 확인한 뒤 `--force` 를 한 번만 써서 제거한다. 이 확인은 초기화된 체크아웃뿐 아니라 워크트리 `modules/` 아래의 모든 저장소(deinit 된 서브모듈, 중첩 서브모듈 포함)에 직접 하며, 확인할 수 없는 저장소가 있으면 거부한다. 이 확인은 `--stop` 으로 세션을 정지한 뒤 제거 직전에 한 번 더 한다(정지 중에도 쓰기·커밋·stash 가 생길 수 있다). **사람이 `git worktree remove --force` 를 직접 쓰지 말 것.** 워크트리 전용 서브모듈 저장소와 미푸시 커밋이 함께 사라진다. diff --git a/tests/test_issue21_phase2_loop_a.py b/tests/test_issue21_phase2_loop_a.py index 05428f0..3dda3b1 100644 --- a/tests/test_issue21_phase2_loop_a.py +++ b/tests/test_issue21_phase2_loop_a.py @@ -290,6 +290,21 @@ def test_w1b_prepare_refuses_a_venv_link_and_skips_without_a_control_root_interp assert os.path.islink(wt / ".venv"), "nothing is changed by the refusal" +def test_w1c_failed_requirements_install_removes_the_half_made_venv(ctl): + """Review D1 (PR #29 delta): after a failed pip install the next run must + not report VENV=kept for an environment without the requirements.""" + main = ctl + shutil.rmtree(main / ".venv") + subprocess.run([sys.executable, "-m", "venv", "--without-pip", str(main / ".venv")], check=True) + (main / "requirements.txt").write_text("this-package-does-not-exist-mam-w1c==0.0.0\n") + _git(main, "add", "requirements.txt") + _git(main, "commit", "-q", "-m", "req") + wt = _wt(main) + res = _prepare(main, wt, env={**os.environ, "PIP_INDEX_URL": "http://127.0.0.1:9/simple", "PIP_RETRIES": "0", "PIP_TIMEOUT": "1"}) + assert res.returncode == 1 and "pip install" in res.stderr, res.stdout + res.stderr + assert not (wt / ".venv").exists() and os.path.realpath(wt) in _worktrees(main) + + def test_w2_prepare_refusals_and_dry_run(ctl): main = ctl res = _run(main, PREP, "--path", str(main / "inside"), "--branch", "x")