범위는 #72 의 결정 1–10(댓글 921), 댓글 926·930, 코드 확인 뒤 더한 U1–U4(댓글 933)다.
update 스킬 (multi-agent-mux-update)
bash .agents/skills/multi-agent-mux-update/scripts/update.sh --yes. 설치된 작업 공간(.mam_deploy/remove.sh 있음)에서만 돈다. 터미널이 없으면 --yes 가 필요하고, 강제 옵션은 없다.
1단계 (설치된 옛 코드, update.sh)
배타 입장 잠금 아래에서 .mam/update-in-progress 표식을 건다.
아무것도 바꾸기 전에 거부하는 경우(lib_py/update.py precheck): running 행, 어느 서버든 cwd 가 이 작업 공간 안인 살아 있는 herdr 에이전트, 열린 잡(읽을 수 없는 JSON·잡 디렉터리 포함, dry_run 제외), live 루프 표식, 스킬이 띄우지 않은 모니터가 쥔 .mam/monitor.lock(U3), 다른 업데이트. 호출한 에이전트(서버+pane, 그 행, 실행 중인 잡 하나)는 세지 않는다. pane 이름이 없는 오케스트레이터는 pane id 로만 뺀다. 거부할 때는 정리 방법을 함께 출력한다.
모니터를 멈춘다(TERM, 끝나지 않으면 KILL; 대기는 MAM_UPDATE_MONITOR_STOP_S).
남은 백업이 있으면 상태에 따라 처리한다(U4, 아래).
.agents/, .mam_deploy/, 루트 파일, .venv, 설치기가 관리하는 .mam 파일, 레지스트리(레지스트리 잠금 아래 SQLite backup API)를 .mam/update-backup/ 에 백업한다.
install_phase.sh 의 복사본을 트리 밖에서 exec 한다(결정 5).
설치 (복사본, lib.sh/lib_py 를 부르지 않음, bash 3.2): MAM_INSTALLER_URL(기본 Gitea main)의 설치기를 제자리로 실행한다. 실패하면 백업에서 되돌리고, 되돌린 트리로 모니터를 다시 띄운다.
2단계 (새로 설치된 코드, post_update.sh): lib_py.registry_format 으로 레지스트리를 변환하고, 원래 떠 있던 모니터만 다시 띄운다(MAM_NO_SUBSCRIBER_DAEMON=1 이면 띄우지 않음). 변환이 실패하면 설치는 두고 0 이 아닌 코드로 끝난다.
끝나면 백업과 표식을 지운다.
남은 백업 (U4): 백업 폴더의 상태 파일 backing-up → installing → installed. 다음 업데이트는 사전 확인과 모니터 정지 뒤에 처리한다. backing-up/상태 없음/installed 는 지우고 진행, installing 은 레지스트리만 빼고 되돌린 뒤 진행, 되돌리기가 실패하면 백업을 두고 0 이 아닌 코드로 끝난다.
종료 코드: 0 업데이트됨(프롬프트 취소 포함), 1 거부·실패, 2 잘못된 인자. 설치 뒤 레지스트리 변환이 실패하면 변환기의 코드.
입장 잠금 (결정 8, U2)
create, resume, run_loop.sh, delegate submit, stop, orc-onboard 는 .mam/update-admission.lock 을 공유로 잡고(lib.sh mam_update_admit), 표식이 live/unknown 이면 거부한다. 기록을 쓸 때까지 잠금을 쥔다. stale 표식은 막지 않는다. .mam 이 없는 트리(개발 체크아웃)에서는 잠금을 만들지 않는다.
delegate submit 은 대상 세션이 속한 설치 작업 공간들의 잠금도 잡는다. 행의 pane.cwd 위의 설치본 전부와 행의 control_plane.workspace_root, 행이 없으면 전달에 쓰는 서버의 살아 있는 에이전트 cwd 로 찾는다. herdr 가 답하지 못하면 등록 전에 거부한다.
lib_py.pane_lock acquire 에 --shared, --wait-env(MAM_UPDATE_ADMISSION_WAIT_S, 기본 120)를 더했다. 오래 사는 자식은 그 fd 를 물려받지 않는다.
제자리 설치 (deploy/install.sh, 결정 3·4·9)
install_state 는 키 단위로 덮어쓰고 키마다 한 줄이다.
업데이터를 설치하지 않는다. 남아 있는 .mam_deploy/update.sh(와 설치 목록에 있는 루트 update.sh)는 지우고 목록에서 뺀다.
새 버전에 없는 프레임워크 파일은 사용자가 고치지 않았으면 지우고, 고쳤으면 남기고 목록을 출력한다. base/, 설치 목록, asset_hashes.txt 에서도 뺀다.
deploy/update.sh 삭제. install_mam.sh, remove.sh(옛 .mam_deploy/update.sh 를 지우는 줄은 유지), CI(deploy/update.sh 대신 update 스킬 스크립트를 shellcheck), 문서는 update 스킬을 가리킨다.
resume 이 모니터를 띄운다 (결정 2): 행을 쓴 뒤 이미 떠 있던 경로와 새로 띄운 경로 모두 제어 루트를 workdir 로 start_watchdog 를 부른다(worktree 세션 포함). MAM_NO_SUBSCRIBER_DAEMON=1 이면 띄우지 않고, 실패는 경고만 한다.
attach 안내 (926, 930, U1): create 의 마지막 안내, delegate-job 의 전달 안내, stop 의 TUI 멈춤 안내는 행의 attach_command 를, 행이 없으면 lib_py.agents.sanitize.attach_command 결과를 출력한다. deploy/INSTALL.md, install_mam.sh 는 herdr --session <server> agent attach <herdr 이름> 형식을 보여 준다.
문서는 이 변경으로 틀리게 된 줄만 고쳤고, tests/data/skill_code_blocks.json 의 핀을 따라 고쳤다.
버전 번호와 VERSIONS.md 는 바꾸지 않았다.
테스트
Linux, 253a0aa: 전체 스위트 2291 passed, 20 skipped, 0 failed (68분). 남은 프로세스 없음.
그 뒤의 454b772(아래 권고 반영): test_issue72_update.py, test_issue72_installer.py, test_issue8_update_state.py, test_deploy_layout.py, test_issue42_skill_blocks.py 93 passed, 1 skipped. 더한 테스트는 고치기 전 코드에서 모니터를 다시 띄우지 않아 실패한다.
새 테스트: test_issue72_{update,admission,attach_hints,installer,resume_monitor}.py. base 0e1e2e8 에서는 78건 중 77건 실패, 1건 건너뜀(첫 전환 테스트, base 아카이브에 git 기록이 없어서).
테스트는 임시 작업 공간에 설치하고, mock herdr 를 쓰며, 실제 레지스트리·모니터·herdr 서버를 건드리지 않는다.
macOS(bash 3.2, Python 3.9)에서는 실행하지 않았다. 리뷰어가 Python 3.9 문법과 bash -n 만 확인했다.
리뷰
리뷰어 4명(claude, agy, grok, codex) 3회차 전원 PASS. codex 의 1·2회차 지적(delegate submit 의 대상 작업 공간 입장, 읽을 수 없는 잡 디렉터리, 행 없는 에이전트 안내의 서버, 행 없는 대상, 우연한 .mam)은 e339465, ba75718, 253a0aa 에서 고쳤다.
비차단 권고 중 둘은 454b772 에 반영했다(리뷰 이후 커밋이라 리뷰어 재검토는 없음):
백업 상태를 installing 으로 쓰지 못하면(설치 전, 아무것도 바뀌지 않음) 1단계가 멈춘 모니터를 다시 띄우지 않고 백업을 남긴 채 끝났다. 이제 백업을 지우고 모니터를 다시 띄운 뒤 exit 1.
update SKILL.md 의 종료 코드 표: 설치 뒤 레지스트리 변환 실패는 변환기의 0 이 아닌 코드를 그대로 낸다는 것, 확인 프롬프트 취소는 0 이라는 것을 적었다.
남긴 권고(후속 후보): stale 표식 NOTE 출력 순서(다른 업데이트가 끼어들면 메시지가 둘), MAM_STATE_JSON 이 깨졌을 때 delegate submit 이 대상 해석을 건너뜀(등록은 뒤의 레지스트리 접근에서 실패), 행 없는 대상에 submit 할 때마다 herdr 를 한 번 더 조회함.
릴리스 노트에 적을 것
.mam_deploy/update.sh 가 없어지고 update 스킬로 바뀐다.
업데이트 중에는 create/resume/run_loop/delegate submit/stop/orc-onboard 가 거부한다.
resume 이 모니터 데몬을 띄운다.
첫 전환 (옛 update.sh 로 한 번):
루프가 끝나기를 기다리고, 열린 잡을 끝내거나 닫는다.
세션을 모두 stop 한다.
모니터(reconcile.sh --subscribe)를 멈춘다.
옛 .mam_deploy/update.sh 를 실행한다.
바로 update 스킬을 한 번 더 실행한다(레지스트리 변환).
4번의 옛 update.sh 는 .mam 을 지운 뒤 고정 목록만 되돌리므로 다음을 잃는다: herdr_workspace_id, diagnostics/, 모니터 로그, shim/(lib.sh 가 다시 만듦), 있다면 briefs/·resumelogs/·stoplogs/. .mam 밖의 .cache/multi-agent-mux-monitor 는 지금 코드가 쓰지 않는 폴더라 잃는 것이 없다.
Refs #72
## 변경
범위는 #72 의 결정 1–10(댓글 921), 댓글 926·930, 코드 확인 뒤 더한 U1–U4(댓글 933)다.
- **update 스킬 (`multi-agent-mux-update`)**
- `bash .agents/skills/multi-agent-mux-update/scripts/update.sh --yes`. 설치된 작업 공간(`.mam_deploy/remove.sh` 있음)에서만 돈다. 터미널이 없으면 `--yes` 가 필요하고, 강제 옵션은 없다.
- **1단계 (설치된 옛 코드, `update.sh`)**
- 배타 입장 잠금 아래에서 `.mam/update-in-progress` 표식을 건다.
- 아무것도 바꾸기 전에 거부하는 경우(`lib_py/update.py` precheck): running 행, 어느 서버든 cwd 가 이 작업 공간 안인 살아 있는 herdr 에이전트, 열린 잡(읽을 수 없는 JSON·잡 디렉터리 포함, `dry_run` 제외), live 루프 표식, 스킬이 띄우지 않은 모니터가 쥔 `.mam/monitor.lock`(U3), 다른 업데이트. 호출한 에이전트(서버+pane, 그 행, 실행 중인 잡 하나)는 세지 않는다. pane 이름이 없는 오케스트레이터는 pane id 로만 뺀다. 거부할 때는 정리 방법을 함께 출력한다.
- 모니터를 멈춘다(TERM, 끝나지 않으면 KILL; 대기는 `MAM_UPDATE_MONITOR_STOP_S`).
- 남은 백업이 있으면 상태에 따라 처리한다(U4, 아래).
- `.agents/`, `.mam_deploy/`, 루트 파일, `.venv`, 설치기가 관리하는 `.mam` 파일, 레지스트리(레지스트리 잠금 아래 SQLite backup API)를 `.mam/update-backup/` 에 백업한다.
- `install_phase.sh` 의 복사본을 트리 밖에서 exec 한다(결정 5).
- **설치 (복사본, lib.sh/lib_py 를 부르지 않음, bash 3.2)**: `MAM_INSTALLER_URL`(기본 Gitea `main`)의 설치기를 제자리로 실행한다. 실패하면 백업에서 되돌리고, 되돌린 트리로 모니터를 다시 띄운다.
- **2단계 (새로 설치된 코드, `post_update.sh`)**: `lib_py.registry_format` 으로 레지스트리를 변환하고, 원래 떠 있던 모니터만 다시 띄운다(`MAM_NO_SUBSCRIBER_DAEMON=1` 이면 띄우지 않음). 변환이 실패하면 설치는 두고 0 이 아닌 코드로 끝난다.
- 끝나면 백업과 표식을 지운다.
- **남은 백업 (U4)**: 백업 폴더의 상태 파일 `backing-up` → `installing` → `installed`. 다음 업데이트는 사전 확인과 모니터 정지 뒤에 처리한다. `backing-up`/상태 없음/`installed` 는 지우고 진행, `installing` 은 레지스트리만 빼고 되돌린 뒤 진행, 되돌리기가 실패하면 백업을 두고 0 이 아닌 코드로 끝난다.
- 종료 코드: 0 업데이트됨(프롬프트 취소 포함), 1 거부·실패, 2 잘못된 인자. 설치 뒤 레지스트리 변환이 실패하면 변환기의 코드.
- **입장 잠금 (결정 8, U2)**
- create, resume, `run_loop.sh`, delegate submit, stop, orc-onboard 는 `.mam/update-admission.lock` 을 공유로 잡고(lib.sh `mam_update_admit`), 표식이 `live`/`unknown` 이면 거부한다. 기록을 쓸 때까지 잠금을 쥔다. `stale` 표식은 막지 않는다. `.mam` 이 없는 트리(개발 체크아웃)에서는 잠금을 만들지 않는다.
- delegate submit 은 대상 세션이 속한 설치 작업 공간들의 잠금도 잡는다. 행의 `pane.cwd` 위의 설치본 전부와 행의 `control_plane.workspace_root`, 행이 없으면 전달에 쓰는 서버의 살아 있는 에이전트 cwd 로 찾는다. herdr 가 답하지 못하면 등록 전에 거부한다.
- `lib_py.pane_lock acquire` 에 `--shared`, `--wait-env`(`MAM_UPDATE_ADMISSION_WAIT_S`, 기본 120)를 더했다. 오래 사는 자식은 그 fd 를 물려받지 않는다.
- **제자리 설치 (`deploy/install.sh`, 결정 3·4·9)**
- `install_state` 는 키 단위로 덮어쓰고 키마다 한 줄이다.
- 업데이터를 설치하지 않는다. 남아 있는 `.mam_deploy/update.sh`(와 설치 목록에 있는 루트 `update.sh`)는 지우고 목록에서 뺀다.
- 새 버전에 없는 프레임워크 파일은 사용자가 고치지 않았으면 지우고, 고쳤으면 남기고 목록을 출력한다. `base/`, 설치 목록, `asset_hashes.txt` 에서도 뺀다.
- `deploy/update.sh` 삭제. `install_mam.sh`, `remove.sh`(옛 `.mam_deploy/update.sh` 를 지우는 줄은 유지), CI(`deploy/update.sh` 대신 update 스킬 스크립트를 shellcheck), 문서는 update 스킬을 가리킨다.
- **resume 이 모니터를 띄운다 (결정 2)**: 행을 쓴 뒤 이미 떠 있던 경로와 새로 띄운 경로 모두 제어 루트를 workdir 로 `start_watchdog` 를 부른다(worktree 세션 포함). `MAM_NO_SUBSCRIBER_DAEMON=1` 이면 띄우지 않고, 실패는 경고만 한다.
- **attach 안내 (926, 930, U1)**: create 의 마지막 안내, delegate-job 의 전달 안내, stop 의 TUI 멈춤 안내는 행의 `attach_command` 를, 행이 없으면 `lib_py.agents.sanitize.attach_command` 결과를 출력한다. `deploy/INSTALL.md`, `install_mam.sh` 는 `herdr --session <server> agent attach <herdr 이름>` 형식을 보여 준다.
- 문서는 이 변경으로 틀리게 된 줄만 고쳤고, `tests/data/skill_code_blocks.json` 의 핀을 따라 고쳤다.
- 버전 번호와 VERSIONS.md 는 바꾸지 않았다.
## 테스트
- Linux, `253a0aa`: 전체 스위트 **2291 passed, 20 skipped, 0 failed** (68분). 남은 프로세스 없음.
- 그 뒤의 `454b772`(아래 권고 반영): `test_issue72_update.py`, `test_issue72_installer.py`, `test_issue8_update_state.py`, `test_deploy_layout.py`, `test_issue42_skill_blocks.py` 93 passed, 1 skipped. 더한 테스트는 고치기 전 코드에서 모니터를 다시 띄우지 않아 실패한다.
- 새 테스트: `test_issue72_{update,admission,attach_hints,installer,resume_monitor}.py`. base `0e1e2e8` 에서는 78건 중 77건 실패, 1건 건너뜀(첫 전환 테스트, base 아카이브에 git 기록이 없어서).
- 테스트는 임시 작업 공간에 설치하고, mock herdr 를 쓰며, 실제 레지스트리·모니터·herdr 서버를 건드리지 않는다.
- macOS(bash 3.2, Python 3.9)에서는 실행하지 않았다. 리뷰어가 Python 3.9 문법과 `bash -n` 만 확인했다.
## 리뷰
- 리뷰어 4명(claude, agy, grok, codex) 3회차 전원 PASS. codex 의 1·2회차 지적(delegate submit 의 대상 작업 공간 입장, 읽을 수 없는 잡 디렉터리, 행 없는 에이전트 안내의 서버, 행 없는 대상, 우연한 `.mam`)은 `e339465`, `ba75718`, `253a0aa` 에서 고쳤다.
- 비차단 권고 중 둘은 `454b772` 에 반영했다(리뷰 이후 커밋이라 리뷰어 재검토는 없음):
- 백업 상태를 `installing` 으로 쓰지 못하면(설치 전, 아무것도 바뀌지 않음) 1단계가 멈춘 모니터를 다시 띄우지 않고 백업을 남긴 채 끝났다. 이제 백업을 지우고 모니터를 다시 띄운 뒤 exit 1.
- update SKILL.md 의 종료 코드 표: 설치 뒤 레지스트리 변환 실패는 변환기의 0 이 아닌 코드를 그대로 낸다는 것, 확인 프롬프트 취소는 0 이라는 것을 적었다.
- 남긴 권고(후속 후보): stale 표식 NOTE 출력 순서(다른 업데이트가 끼어들면 메시지가 둘), `MAM_STATE_JSON` 이 깨졌을 때 delegate submit 이 대상 해석을 건너뜀(등록은 뒤의 레지스트리 접근에서 실패), 행 없는 대상에 submit 할 때마다 herdr 를 한 번 더 조회함.
## 릴리스 노트에 적을 것
- `.mam_deploy/update.sh` 가 없어지고 update 스킬로 바뀐다.
- 업데이트 중에는 create/resume/run_loop/delegate submit/stop/orc-onboard 가 거부한다.
- resume 이 모니터 데몬을 띄운다.
- **첫 전환 (옛 update.sh 로 한 번)**:
1. 루프가 끝나기를 기다리고, 열린 잡을 끝내거나 닫는다.
2. 세션을 모두 stop 한다.
3. 모니터(`reconcile.sh --subscribe`)를 멈춘다.
4. 옛 `.mam_deploy/update.sh` 를 실행한다.
5. 바로 update 스킬을 한 번 더 실행한다(레지스트리 변환).
- 4번의 옛 update.sh 는 `.mam` 을 지운 뒤 고정 목록만 되돌리므로 다음을 잃는다: `herdr_workspace_id`, `diagnostics/`, 모니터 로그, `shim/`(lib.sh 가 다시 만듦), 있다면 `briefs/`·`resumelogs/`·`stoplogs/`. `.mam` 밖의 `.cache/multi-agent-mux-monitor` 는 지금 코드가 쓰지 않는 폴더라 잃는 것이 없다.
- 첫 전환 뒤에는 모니터가 없다. resume 하면 뜬다.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
create's closing hint, the delegate-job wrapper's dispatch hint and stop's
stuck-TUI message printed the pre-#71 forms (`herdr session attach <name>`,
`HERDR_SESSION_NAME=... herdr agent attach <name>`), which miss an agent whose
name is over 32 characters or has upper case. They now print the session row's
attach_command, or lib_py.agents.sanitize.attach_command(server, name) when
there is no row (lib.sh session_attach_command). deploy/INSTALL.md and
install_mam.sh show the `herdr --session <server> agent attach <herdr name>`
form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only create started the per-workspace monitor, and only on its job path, so
the stop -> update -> resume flow the update skill requires left no monitor.
resume now calls start_watchdog with the control root as workdir (as create
does since #21 A8, also for a worktree session) after the row is written, on
both the already-running and the spawn path. MAM_NO_SUBSCRIBER_DAEMON=1 starts
none; a failure warns and does not fail the resume.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The update skill publishes .mam/update-in-progress (pid + lstart, the
loop_lock.sh rules) while it holds .mam/update-admission.lock exclusively.
create, resume, run_loop.sh and delegate submit (decision 8) and stop and
orc-onboard (U2: a rollback would restore the registry without their write)
take that lock shared (lib.sh mam_update_admit, fd 7), refuse while the marker
is live or unknown, and hold the lock until their record is written. A stale
marker refuses nothing. lib_py.pane_lock acquire gains --shared and
--wait-env (MAM_UPDATE_ADMISSION_WAIT_S, default 120); create's wrapper path
closes fd 7 for the long-lived child. The exit-1 rows of create/resume/stop
name the refusal; skill_code_blocks.json pins follow the shifted lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The update is a skill (decision 4); deploy/update.sh and the installed
.mam_deploy/update.sh are gone.
- Phase 1 (installed code, update.sh): publish the update marker under the
exclusive admission lock; refuse before any change while a running row, a
live herdr agent in the workspace on any running server, an open job
(unreadable JSON included, dry_run not), a live loop marker or a
monitor.lock held by a monitor the skills did not start (U3) is there - the
calling agent (server + pane, its row and one running job) excepted
(lib_py/update.py precheck, decisions 1 and 7). Stop the monitor (TERM, then
KILL and restart it if it does not end; decision 2), handle a leftover
backup by its state (U4), back up .agents, .mam_deploy, root files, .venv,
the installer's .mam files and the registry (SQLite backup API under the
registry lock) into .mam/update-backup/, then exec a copy of
install_phase.sh outside the tree (decision 5).
- The copy (no lib.sh/lib_py, bash 3.2) runs the installer in place; on
failure it restores the backup and restarts the monitor from the restored
tree; on success it runs phase 2, post_update.sh of the new tree, which
converts the registry with lib_py.registry_format and restarts the monitor
if it ran. The backup and the marker are removed at the end.
- install.sh updates in place (decisions 3, 4, 9): install_state keys are set,
one line each; no updater is installed and an old one (.mam_deploy/update.sh,
or a root update.sh the manifest lists) is removed; framework files the new
version no longer ships are deleted when unmodified, kept and listed when
modified, and dropped from base/, the manifest and asset_hashes.txt; a local
source's .mam is not staged. remove.sh knows the new skill; CI shellchecks
its scripts; docs point at the skill.
- test_issue8_update_state and test_deploy_layout td6/td27 run the skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A v4.11.0 workspace's .mam_deploy/update.sh installs the new version - the
skill present, no updater left - and the skill run after it converts the
registry. An update-backup in an unknown state now restarts the monitor it
stopped: nothing was changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI now shellchecks the update skill's scripts: SC1091 is disabled at their
`source` lines as in the other skills, and the two findings this branch added
(lib.sh's loop_lock.sh source, install.sh's one-item legacy loop) are
disabled at their lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mam_update_admit created .mam/ to hold its lock, so a checkout running its
own run_loop.sh got a .mam (test_loop_marker_isolation L1, test_issue17 W1).
A tree without .mam is not an installed workspace, the only kind the update
runs in, so the gate admits there without a lock. The update's monitor-stop
wait is a normalised integer knob and is listed in INT_KNOBS
(test_issue26_ints B5).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decision 8: delegate submit uses the admission lock of the workspace the
target session belongs to. The wrapper resolved only its own
WORKSPACE_ROOT, so a job could be registered and delivered to an agent of
another installed workspace while that workspace was being updated.
lib_py.update target-workspace resolves a herdr:<name> row's workspace (the
.mam at or above its pane.cwd, else its control_plane.workspace_root);
mam_update_admit takes an fd, and submit holds the caller root on fd 7 and
the target on fd 8 until the record is written. Rowless and remote targets
keep the caller-root gate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
R2: glob() hid a listing error, so an unreadable .mam/jobs (or MAM_JOBS_DIR,
or another row's jobs dir) counted as empty and the update installed over an
open job. Records are now listed with os.listdir; a directory that cannot be
listed, or stat'ed, is a blocker with its path.
R3: a discovered agent with no row was told to run stop with --session only;
stop then looked on the workspace's server, not the one the agent was found
on. The hint now carries --herdr-session <server>, quoted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
R1: a target with no registry row skipped the target gate. Its cwd is now
read from the live agent on the server delivery uses (resolve_herdr_session),
through the real herdr only when that server is running, so no server is
started; a herdr that cannot answer refuses before registration.
R2: any .mam counted as a workspace and stopped the walk, so B/sub/.mam hid
an installed B and the row's control_plane root was dropped. Workspaces are
now update.sh's installed trees (.mam_deploy/remove.sh or remove.sh): every
installed ancestor of the cwd, plus the recorded control_plane root.
lib_py.update target-workspaces prints them one per line; the wrapper holds
each on fds 20.. until the record is written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install_phase.sh exited when it could not mark the backup `installing`,
before the installer ran, without starting the monitor phase 1 had stopped
(decision 2) and leaving the backup behind. Nothing was changed at that
point, so it now removes the backup, restarts the monitor if it ran,
releases the marker and exits 1.
SKILL.md's exit row said 1 for every failure; a failed registry conversion
after a good install passes the converter's nonzero code through, and a
cancelled prompt exits 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- daemon.find_running matches `<bash> <reconcile.sh> --subscribe[ ...]`
exactly instead of an unanchored, unescaped pgrep regex: another
workspace's daemon (ws.a vs wsXa) or a process naming the path in an
argument is no longer signalled by the update. update.sh re-checks which
PIDs are still the monitor before SIGKILL.
- create and resume (spawn path) take the update admission of every
installed workspace at or above the pane cwd, not only the control
root's (lib.sh mam_update_admit_at, lib_py.update target-workspaces --cwd);
the ~/.local/bin wrapper child drops all of them.
- tests: snapshot() skips the SQLite -wal/-shm and compares the DB by its
rows (macOS keeps the sidecars and a read rewrites -shm); regression tests
for the monitor match and for create/resume in another installed
workspace (marker and lock wait); evidence line refs shifted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With MAM_DELEGATION_PY=0 the legacy helper still found the monitor with an
unanchored pgrep -f, so a process that only named the monitor path was
reported as the restarted monitor and none was started. lib.sh _monitor_pids
is the shell twin of daemon.find_running; update.sh and post_update.sh check
the PID start_watchdog printed with mam_monitor_started before saying
"monitor: restarted".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With MAM_DELEGATION_PY=0 the monitor was left in the caller's process group
(decision 2), on every restart the update makes. The legacy body now spawns
it under job control (`set -m`, macOS has no setsid(1)) and restores the
caller's option. The update tests assert the restarted monitor's PGID in both
modes, and their teardown kills helper groups and this test's exact monitors
instead of `pkill -f <ws>`, then reports anything left that names the
workspace.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 #72
변경
범위는 #72 의 결정 1–10(댓글 921), 댓글 926·930, 코드 확인 뒤 더한 U1–U4(댓글 933)다.
multi-agent-mux-update)bash .agents/skills/multi-agent-mux-update/scripts/update.sh --yes. 설치된 작업 공간(.mam_deploy/remove.sh있음)에서만 돈다. 터미널이 없으면--yes가 필요하고, 강제 옵션은 없다.update.sh).mam/update-in-progress표식을 건다.lib_py/update.pyprecheck): running 행, 어느 서버든 cwd 가 이 작업 공간 안인 살아 있는 herdr 에이전트, 열린 잡(읽을 수 없는 JSON·잡 디렉터리 포함,dry_run제외), live 루프 표식, 스킬이 띄우지 않은 모니터가 쥔.mam/monitor.lock(U3), 다른 업데이트. 호출한 에이전트(서버+pane, 그 행, 실행 중인 잡 하나)는 세지 않는다. pane 이름이 없는 오케스트레이터는 pane id 로만 뺀다. 거부할 때는 정리 방법을 함께 출력한다.MAM_UPDATE_MONITOR_STOP_S)..agents/,.mam_deploy/, 루트 파일,.venv, 설치기가 관리하는.mam파일, 레지스트리(레지스트리 잠금 아래 SQLite backup API)를.mam/update-backup/에 백업한다.install_phase.sh의 복사본을 트리 밖에서 exec 한다(결정 5).MAM_INSTALLER_URL(기본 Giteamain)의 설치기를 제자리로 실행한다. 실패하면 백업에서 되돌리고, 되돌린 트리로 모니터를 다시 띄운다.post_update.sh):lib_py.registry_format으로 레지스트리를 변환하고, 원래 떠 있던 모니터만 다시 띄운다(MAM_NO_SUBSCRIBER_DAEMON=1이면 띄우지 않음). 변환이 실패하면 설치는 두고 0 이 아닌 코드로 끝난다.backing-up→installing→installed. 다음 업데이트는 사전 확인과 모니터 정지 뒤에 처리한다.backing-up/상태 없음/installed는 지우고 진행,installing은 레지스트리만 빼고 되돌린 뒤 진행, 되돌리기가 실패하면 백업을 두고 0 이 아닌 코드로 끝난다.run_loop.sh, delegate submit, stop, orc-onboard 는.mam/update-admission.lock을 공유로 잡고(lib.shmam_update_admit), 표식이live/unknown이면 거부한다. 기록을 쓸 때까지 잠금을 쥔다.stale표식은 막지 않는다..mam이 없는 트리(개발 체크아웃)에서는 잠금을 만들지 않는다.pane.cwd위의 설치본 전부와 행의control_plane.workspace_root, 행이 없으면 전달에 쓰는 서버의 살아 있는 에이전트 cwd 로 찾는다. herdr 가 답하지 못하면 등록 전에 거부한다.lib_py.pane_lock acquire에--shared,--wait-env(MAM_UPDATE_ADMISSION_WAIT_S, 기본 120)를 더했다. 오래 사는 자식은 그 fd 를 물려받지 않는다.deploy/install.sh, 결정 3·4·9)install_state는 키 단위로 덮어쓰고 키마다 한 줄이다..mam_deploy/update.sh(와 설치 목록에 있는 루트update.sh)는 지우고 목록에서 뺀다.base/, 설치 목록,asset_hashes.txt에서도 뺀다.deploy/update.sh삭제.install_mam.sh,remove.sh(옛.mam_deploy/update.sh를 지우는 줄은 유지), CI(deploy/update.sh대신 update 스킬 스크립트를 shellcheck), 문서는 update 스킬을 가리킨다.start_watchdog를 부른다(worktree 세션 포함).MAM_NO_SUBSCRIBER_DAEMON=1이면 띄우지 않고, 실패는 경고만 한다.attach_command를, 행이 없으면lib_py.agents.sanitize.attach_command결과를 출력한다.deploy/INSTALL.md,install_mam.sh는herdr --session <server> agent attach <herdr 이름>형식을 보여 준다.tests/data/skill_code_blocks.json의 핀을 따라 고쳤다.테스트
253a0aa: 전체 스위트 2291 passed, 20 skipped, 0 failed (68분). 남은 프로세스 없음.454b772(아래 권고 반영):test_issue72_update.py,test_issue72_installer.py,test_issue8_update_state.py,test_deploy_layout.py,test_issue42_skill_blocks.py93 passed, 1 skipped. 더한 테스트는 고치기 전 코드에서 모니터를 다시 띄우지 않아 실패한다.test_issue72_{update,admission,attach_hints,installer,resume_monitor}.py. base0e1e2e8에서는 78건 중 77건 실패, 1건 건너뜀(첫 전환 테스트, base 아카이브에 git 기록이 없어서).bash -n만 확인했다.리뷰
.mam)은e339465,ba75718,253a0aa에서 고쳤다.454b772에 반영했다(리뷰 이후 커밋이라 리뷰어 재검토는 없음):installing으로 쓰지 못하면(설치 전, 아무것도 바뀌지 않음) 1단계가 멈춘 모니터를 다시 띄우지 않고 백업을 남긴 채 끝났다. 이제 백업을 지우고 모니터를 다시 띄운 뒤 exit 1.MAM_STATE_JSON이 깨졌을 때 delegate submit 이 대상 해석을 건너뜀(등록은 뒤의 레지스트리 접근에서 실패), 행 없는 대상에 submit 할 때마다 herdr 를 한 번 더 조회함.릴리스 노트에 적을 것
.mam_deploy/update.sh가 없어지고 update 스킬로 바뀐다.reconcile.sh --subscribe)를 멈춘다..mam_deploy/update.sh를 실행한다..mam을 지운 뒤 고정 목록만 되돌리므로 다음을 잃는다:herdr_workspace_id,diagnostics/, 모니터 로그,shim/(lib.sh 가 다시 만듦), 있다면briefs/·resumelogs/·stoplogs/..mam밖의.cache/multi-agent-mux-monitor는 지금 코드가 쓰지 않는 폴더라 잃는 것이 없다.🤖 Generated with Claude Code