diff --git a/.agents/skills/multi-agent-mux-create/SKILL.md b/.agents/skills/multi-agent-mux-create/SKILL.md index fc5628c..37ffe5e 100644 --- a/.agents/skills/multi-agent-mux-create/SKILL.md +++ b/.agents/skills/multi-agent-mux-create/SKILL.md @@ -233,6 +233,23 @@ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \ The script handles the YAML append, pane capture, and the `last_visible_status` placeholder. +## Issue worktrees: `worktree_prepare.sh` (Issue #21 item 4) + +Run from the control root's skills tree (lib.sh's `WORKSPACE_ROOT` = the main checkout that owns `.mam`): + +```bash +bash .agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh \ + --path ../multi-agent-mux-wt/issue-42 --branch fix/issue-42 [--base ] [--submodules] \ + [--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; links `.venv` to the control root's; 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` removes those traces 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`. +- Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule. +- create itself: a control-plane binding that cannot be computed is refused (exit 2) for a `--workspace` other than the control root and warned about for the control root; the immediate reconcile and the watchdog run the control root's code. + ## Pitfalls - **Don't use `nohup`/`disown`/`setsid` for the agent itself** — those background the agent outside herdr. The whole point of this skill is *the herdr session is the supervisor*. `nohup` is OK only for *launching the wrapper* (which itself creates the herdr session via `herdr new-session -d`). diff --git a/.agents/skills/multi-agent-mux-resume/SKILL.md b/.agents/skills/multi-agent-mux-resume/SKILL.md index 4410cf6..a5a163b 100644 --- a/.agents/skills/multi-agent-mux-resume/SKILL.md +++ b/.agents/skills/multi-agent-mux-resume/SKILL.md @@ -123,6 +123,15 @@ bash .agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh \ herdr agent attach "$SESSION_NAME" ``` +## Original cwd only (Issue #21 phase 2) + +A session is resumed only in the directory it ran in (its row's `pane.cwd`): the conversation is keyed by that directory, and a class A agent (agy/hermes/opencode/codex) would otherwise start a new conversation in the wrong tree. + +- `--workspace` differing from `pane.cwd` → **exit 2** with the path to use (claude/grok used to fail with exit 1 "No saved session"). +- `pane.cwd` no longer exists (worktree removed) → **exit 2**; recreate the worktree at that path to resume. +- Rows without `pane.cwd` are not checked. The check runs before session-id resolution and also under `--dry-run`. +- The control-plane binding is computed before the herdr-alive check, so an already-running session's row gets it too; a binding that cannot be computed is refused (exit 2) for a worktree and warned about for the control root. + ## Pitfalls - **`claude -r` requires the SAME project directory** — if the workspace path differs from when the session was created, claude will create a new project dir key (`-home-...-different-name`) and put the resume in a different location. Always `-c` (cd to workspace) before running. diff --git a/.agents/skills/multi-agent-mux-stop/SKILL.md b/.agents/skills/multi-agent-mux-stop/SKILL.md index 43db5b3..e1a923c 100644 --- a/.agents/skills/multi-agent-mux-stop/SKILL.md +++ b/.agents/skills/multi-agent-mux-stop/SKILL.md @@ -109,6 +109,24 @@ The script: 6. Updates the YAML entry and SQLite database atomically 7. If `delegate_job_id` is set, publishes a `completed` event to the multi-agent-mux-delegate-job registry +## Removing an issue worktree: `worktree_remove.sh` (Issue #21 item 4) + +```bash +bash .agents/skills/multi-agent-mux-stop/scripts/worktree_remove.sh --path [--stop] [--dry-run] +``` + +Every check runs first and changes nothing; a refusal exits 3: + +1. a linked worktree of the control root, not locked (`git worktree unlock` first - git refuses even `--force` on a locked one); +2. running sessions under it only with `--stop` (stopped normally, conversations kept); +3. no open job of those sessions - never cancelled automatically, they may belong to a running loop (the delegate job of a session `--stop` stops is cancelled by stop itself); +4. a clean tree, submodules included (`status --porcelain --ignore-submodules=none`); +5. inside submodules: no modification and no commit that is on no remote (`HEAD` included - a submodule sits on a detached HEAD). + +Then it stops the sessions, re-checks the jobs and removes the worktree (the branch is kept): plain, or - when the worktree holds submodule state, which git refuses to remove otherwise - a **single `--force` justified by checks 4 and 5**. Do not run `git worktree remove --force` by hand on such a worktree: it deletes the worktree's own submodule repositories, unpushed commits included. + +Exit codes: 0 removed, 1 stop/git failure, 2 not a worktree of the control root, 3 refused (nothing changed). Sessions created in a removed worktree cannot be resumed until a worktree exists at that path again. + ## Pitfalls - **Don't delete on-disk artifacts by default** — the agent's `*.jsonl` / `conversations/*.db` is the data that `multi-agent-mux-resume` needs. `--purge-conversation` is for when the user is genuinely done with the conversation and wants zero recovery chance. diff --git a/docs/design/worktree-creator-model.md b/docs/design/worktree-creator-model.md index 4425536..7a7078b 100644 --- a/docs/design/worktree-creator-model.md +++ b/docs/design/worktree-creator-model.md @@ -37,27 +37,25 @@ MAM 은 **워크스페이스 = `.agents/skills` 가 들어 있는 체크아웃 - **오케스트레이터 커밋 = 워크트리.** 본 체크아웃은 리뷰어의 작업 공간이므로 오케스트레이터는 그곳에서 브랜치를 바꾸지 않는다. - **제어 평면 = 본 체크아웃의 `.mam` 하나.** create/resume 이 바인딩(`WORKSPACE_ROOT`·`AGENT_SESSIONS_YAML`·`MAM_JOBS_DIR`·`DELEGATE_JOB_LOGS_DIR`)을 행(`control_plane`)에 영속하고, `--workspace` 기본값과 다르면 pane 에 명시 전파한다(항목 3). 호출자 env 만으로는 pane 상속이 보장되지 않는다(lib.sh:667-680 — new-session 은 선행 `K=V` 토큰만 `--env` 로 바꾼다). 행의 `pane.cwd` 는 워크트리다. -## 3. C 안의 동작 방식 (부분 지원 + 1단계 수정 반영) +## 3. C 안의 동작 방식 (1단계 + 2단계 루프 A 반영) ```bash MAIN=/path/to/multi-agent-mux # 본 체크아웃: 리뷰어 풀 + .mam (제어 루트) WT=/path/to/multi-agent-mux-wt/issue-20 # 이슈 워크트리 (소스 루트) -git -C "$MAIN" worktree add -b fix/issue-20 "$WT" main -ln -s "$MAIN/.venv" "$WT/.venv"; git -C "$WT" submodule update --init # 워크트리 준비(.venv, nats-docker) — 헬퍼는 항목 4 - -# creator 를 워크트리 cwd 로, 그러나 본 체크아웃의 .mam 에 등록. -# 바인딩은 행의 control_plane 에 남고, --workspace 기본값과 다르므로 pane 에 --env 로 전파된다. -WORKSPACE_ROOT="$MAIN" AGENT_SESSIONS_YAML="$MAIN/.mam/agent-sessions.yaml" \ -MAM_JOBS_DIR="$MAIN/.mam/jobs" DELEGATE_JOB_LOGS_DIR="$MAIN/.mam/delegate_job_logs" \ - bash "$MAIN/.agents/skills/multi-agent-mux-create/scripts/create_session.sh" \ - --workspace "$WT" --agent claude --role Creator --session creator-claude-issue20 \ - --herdr-session multi-agent-mux --herdr-workspace mam-agent +# 워크트리 준비 + creator 생성(항목 4, worktree_prepare.sh): git worktree add, 제어 루트 .venv 링크, +# 공통 info/exclude 에 /.venv·.pytest_cache/, creator 는 제어 루트 바인딩과 MAM_NO_SUBSCRIBER_DAEMON=1 로 생성. +# 행의 pane.cwd 는 워크트리, control_plane 은 본 체크아웃. 서브모듈은 --submodules 를 줄 때만 초기화한다. +cd "$MAIN" && bash .agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh \ + --path "$WT" --branch fix/issue-20 --base main \ + --creator-agent claude --session creator-claude-issue20 \ + --herdr-session multi-agent-mux --herdr-workspace mam-agent +# 출력 끝의 BASE= 가 워크트리 HEAD 다. (b) 흐름이면 구현 위임 전에 기록해 둔다. # (a) 루프가 구현까지 한다: base 는 Phase 2 시작 시점의 워크트리 HEAD cd "$MAIN" && bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ --worktree "$WT" --creator creator-claude-issue20 --all-reviewer --task "Issue #20 ..." -# (b) 구현은 루프 밖(1단계 기본): base 를 구현 위임 **전에** 기록한다 +# (b) 구현은 루프 밖(1단계 기본): base 를 구현 위임 **전에** 기록한다(prepare 가 출력한 BASE 와 같다) BASE=$(git -C "$WT" rev-parse HEAD) bash "$MAIN/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job" submit --type direct \ --agent claude --agent-session herdr:creator-claude-issue20 --workdir "$MAIN" --prompt "Issue #20 ..." @@ -65,10 +63,15 @@ bash "$MAIN/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate cd "$MAIN" && bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ --worktree "$WT" --base "$BASE" --review-only --creator creator-claude-issue20 --all-reviewer --task "Issue #20 review" -# 정지·재개: --workspace 는 원래 워크트리 경로. 재개는 행에 영속된 바인딩을 다시 전파한다 +# 정지·재개: --workspace 는 원래 워크트리 경로여야 한다(다른 경로·삭제된 경로는 exit 2). +# 재개는 행에 영속된 바인딩을 다시 전파한다 bash "$MAIN/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh" --session creator-claude-issue20 bash "$MAIN/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh" \ --workspace "$WT" --agent claude --session creator-claude-issue20 + +# 정리(worktree_remove.sh): 잠금·세션·잡·청결·서브모듈 판정을 모두 통과해야 정지·제거한다. +# 하나라도 걸리면 exit 3 이고 아무것도 바꾸지 않는다. 브랜치는 남긴다. +bash "$MAIN/.agents/skills/multi-agent-mux-stop/scripts/worktree_remove.sh" --path "$WT" --stop ``` - `--task` 에 base 를 적지 말 것. 러너가 넣는 base 와 둘이 된다. 리뷰어가 받는 base 는 러너의 `[워크트리 컨텍스트]` 블록 하나뿐이어야 한다. @@ -83,18 +86,22 @@ bash "$MAIN/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh" \ - **리뷰 단계는 풀 크기만큼만 병렬**이다. 이슈 3개를 동시에 돌려도 리뷰어 하나는 잡을 순서대로 처리한다(래퍼는 busy 세션에 rc 5 → 러너가 `MAM_SESSION_BUSY_RETRY_S` 뒤 1회 재시도 → 그래도 busy 면 실패). 다중 루프 동시 기동에는 **리뷰어 큐잉 또는 리뷰어 인스턴스 증설**이 전제다. - 같은 `.mam` 의 두 번째 루프는 경고가 아니라 **거부**다: loop lock 이 exit 1 로 막는다(`run_loop.sh` 의 `mam_acquire_loop_lock` 분기). 체크포인트·loop guard 도 파일 하나다(`runner.py` 의 `checkpoint_file`, `MAM_LOOP_MARKER`). YAML/SQLite 는 flock·`BEGIN IMMEDIATE` 로 트랜잭션 안전(atomic_yaml.py)하므로, 문제는 DB 손상이 아니라 **공유 논리 상태와 리뷰어 소유권**이다. - **1단계 운영 형태 = 구현 병렬, 리뷰 루프 하나**: creator 구현은 루프 밖(delegate-job direct)에서 워크트리마다 병렬로 한다. 리뷰는 `run_loop.sh --worktree --base <구현 전 sha> --review-only` 로 한 번에 하나씩 한다. base 를 구현 위임 **전에** 기록하지 않으면 리뷰를 시작할 수 없다(가드가 빈 변경분을 거부한다). busy 처리는 큐가 아니다(1회 재시도). 다중 루프는 항목 8(리뷰어 스케줄러) 이후다. -- 준비 헬퍼는 항목 4 다. `.mam.env` 는 복사 대신 중앙 `MAM_ENV_FILE` 정책으로 하고, `.venv` 심링크는 gitignore 되지 않으므로 `.git/info/exclude` 로 처리한다. +- 준비·해체 헬퍼는 항목 4 로 구현했다(`worktree_prepare.sh`, `worktree_remove.sh`). + - `.mam.env` 는 복사하지 않는다. creator 에는 제어 루트의 것을 `MAM_ENV_FILE` 로 준다. + - `.venv` 는 제어 루트 것의 심링크다. 무시 규칙(`/.venv`, `.pytest_cache/`)은 linked worktree 가 공유하는 `$GIT_COMMON_DIR/info/exclude` 에 들어간다. 그래서 본 체크아웃에도 적용되며, 둘 다 본 체크아웃에서도 무시해야 하는 산출물이다. + - 서브모듈은 `--submodules` 로만 초기화한다. 초기화는 성공·실패와 무관하게 워크트리 git 디렉터리에 `modules/` 를 남기고, 그러면 git 이 force 없는 `worktree remove` 를 거부한다. prepare 는 실패한 초기화의 흔적(`modules/`, 새 `submodule.*` 공유 설정)을 되돌리고 exit 1 한다. + - 서브모듈을 초기화한 워크트리는 remove 가 수정과 원격에 없는 커밋(`HEAD` 포함 — 서브모듈은 detached HEAD 다)을 확인한 뒤 `--force` 를 한 번만 써서 제거한다. **사람이 `git worktree remove --force` 를 직접 쓰지 말 것.** 워크트리 전용 서브모듈 저장소와 미푸시 커밋이 함께 사라진다. ## 5. 필요한 변경 (Issue #21 작업 항목) | # | 항목 | 상태 | |---|---|---| -| 1 | 래퍼 주입 문구·시작 명령(`--registry-dir`, 감사 로그 루트)·Output Report Path 절대경로화, `MAM_JOBS_DIR` 존중 | 이 브랜치 | -| 2 | 루프 러너 `--worktree` / `--base` / `--review-only`, 빈 변경분 fail-closed 가드, 러너의 `MAM_JOBS_DIR` 존중 | 이 브랜치 | -| 3 | 제어 평면 바인딩 영속(`control_plane`)·명시 전파(create/resume), stop·status·reconcile 의 `MAM_JOBS_DIR`·행 바인딩 존중 | 이 브랜치 | -| 4 | 워크트리 준비·해체 헬퍼, resume 을 원래 cwd 로만 허용, create 의 reconcile/watchdog 기동 위치, `MAM_NO_SUBSCRIBER_DAEMON` 기본화, reconcile 의 루트 밖 cwd 재발견 거부(#17-4 셸 축소와 함께) | 대기 | +| 1 | 래퍼 주입 문구·시작 명령(`--registry-dir`, 감사 로그 루트)·Output Report Path 절대경로화, `MAM_JOBS_DIR` 존중 | 완료(v4.6.0) | +| 2 | 루프 러너 `--worktree` / `--base` / `--review-only`, 빈 변경분 fail-closed 가드, 러너의 `MAM_JOBS_DIR` 존중 | 완료(v4.6.0) | +| 3 | 제어 평면 바인딩 영속(`control_plane`)·명시 전파(create/resume), stop·status·reconcile 의 `MAM_JOBS_DIR`·행 바인딩 존중 | 완료(v4.6.0). 루프 A 에서 바인딩 계산 실패 처리·조기 종료 경로 영속 보강 | +| 4 | 워크트리 준비·해체 헬퍼, resume 을 원래 cwd 로만 허용, create 의 reconcile/watchdog 기동 위치, `MAM_NO_SUBSCRIBER_DAEMON`(prepare 가 설정). reconcile 의 루트 밖 cwd 재발견 거부는 제한으로 남김 | 루프 A (이 브랜치) | | 5 | 리뷰 identity: (loop_id, base_sha, head_sha) 판정 기록·무효화, 리뷰어 검증 격리(detached 워크트리) | 대기 | -| 6 | 이 설계 문서 정정 | 이 브랜치 | +| 6 | 이 설계 문서 정정 | 완료(v4.6.0), 루프 A 에서 갱신 | | 7 | 수용 사례 고정(실제 herdr 의 pane env 전파 포함) | 대기 | | 8 | 다중 루프 네임스페이스·리뷰어 스케줄러 | 보류 | @@ -116,11 +123,11 @@ bash "$MAIN/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh" \ | 스킬 | 제어 평면 바인딩 | 워크트리 cwd 행 | 1단계 이후 남은 제한 (항목) | |---|---|---|---| -| 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` | create 가 `$WORKSPACE` 사본으로 reconcile/watchdog 기동, `MAM_NO_SUBSCRIBER_DAEMON` 미기본 (4) | -| resume | 행 바인딩 우선(호출자 env 와 다르면 stderr 경고) 재전파. 바인딩 없는 기존 행은 호출자 바인딩으로 채움 | `--workspace` 는 원래 워크트리 경로여야 함 | 워크트리 삭제 시 재개 불가, 종료 헬퍼 (4) | -| stop | `MAM_JOBS_DIR` > 행의 `mam_jobs_dir` > `$WORKSPACE_ROOT/.mam/jobs` 순으로 잡 조회·발행. 감사 로그도 `DELEGATE_JOB_LOGS_DIR` > 행의 `delegate_job_logs_dir` 순 | 행 cwd 로 uuid 캡처·purge | 해체 순서 헬퍼 (4) | +| 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 로 대화 키 | — | -| monitor/reconcile | `MAM_JOBS_DIR` 우선(정적 가드로만 검증) | 기존 행은 행 cwd 로 검증 | 루트 밖 cwd 의 미등록 세션은 자동 등록 안 됨 → create 로만 등록 (4) | +| 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·검증 격리(5), 레거시 셸 루프(`MAM_LOOP_PY=0`)는 새 옵션 거부 |