11 Commits
Author SHA1 Message Date
Godopu 68eff79810 feat(deploy): install update/remove scripts into .mam_deploy/ and refine markdown staging 2026-08-04 22:17:59 +09:00
Godopu 2ff8b2c4a9 docs: document .mam.env config file, legacy migration path, and save loop reports 2026-08-04 21:43:00 +09:00
Godopu 7d54fd8104 test: cover .mam.env migration, shadowing, ownership and uninstall preservation 2026-08-04 21:42:58 +09:00
Godopu 40a1c0faa9 fix(deploy): pre-capture env ownership and keep backup/restore symmetric 2026-08-04 21:42:56 +09:00
Godopu 443f381092 fix(deploy): back up MAM-owned env under --force and restrict deletion to --purge-env 2026-08-04 21:42:55 +09:00
Godopu c38c05c1f3 feat(deploy): add shadowing guard and evidence-based legacy env migration 2026-08-04 21:42:53 +09:00
Godopu a832ba75d3 feat(deploy): target .mam.env and add --migrate-legacy flag 2026-08-04 21:42:51 +09:00
Godopu 62dcbb1361 fix(config): resolve wrapper env from repo root and warn on deprecated .env 2026-08-04 21:42:49 +09:00
Godopu 2458995e75 fix(config): resolve dotenv via workspace marker and prefer .mam.env over legacy .env 2026-08-04 21:42:45 +09:00
Godopu 11583eb173 refactor(config): rename .env.example to .mam.env.example and isolate .mam.env in gitignore 2026-08-04 21:42:43 +09:00
Godopu 03ba94a030 refactor: optimize multi-agent-mux-loop spec and clean up workspace docs
- Add OPTIMIZATION.md detailing Invocation-Aware Scoped Guard, race-free lock design, and DoD verification gates approved via multi-agent loop
- Update root markdown files (README, BOOTSTRAP, MESSAGING) replacing legacy TMUX references with HERDR
- Remove redundant root markdown files and archive promoted reviewer PASS report
2026-08-02 19:20:43 +09:00
39 changed files with 2401 additions and 1556 deletions
+1 -1
View File
@@ -142,7 +142,7 @@ TMUX 환경에서 실행되는 에이전트가 화면 스크롤 한계로 인해
새 프로젝트에 이 에이전트 오케스트레이션 모델을 구축할 때의 체크리스트입니다. 새 프로젝트에 이 에이전트 오케스트레이션 모델을 구축할 때의 체크리스트입니다.
- [ ] **가상환경 의존성**: `pyyaml`, `paho-mqtt` 등 필요한 Python 패키지가 `.venv` 또는 `requirements.txt`에 포함되었는가? - [ ] **가상환경 의존성**: `pyyaml`, `paho-mqtt` 등 필요한 Python 패키지가 `.venv` 또는 `requirements.txt`에 포함되었는가?
- [ ] **환경 설정 파일**: MQTT 브로커 주소 및 보안 Credential이 `.env` 파일에 안전하게 로드되고 공유되는가? - [ ] **환경 설정 파일**: MQTT 브로커 주소 및 보안 Credential이 `.mam.env` 파일에 안전하게 로드되고 공유되는가?
- [ ] **디렉토리 규약**: 레지스트리 경로(`.mam/jobs/`) 및 로깅 경로(`.mam/delegate_job_logs/`)가 `.gitignore`에 등록되었는가? - [ ] **디렉토리 규약**: 레지스트리 경로(`.mam/jobs/`) 및 로깅 경로(`.mam/delegate_job_logs/`)가 `.gitignore`에 등록되었는가?
- [ ] **스크립트 구비**: `mqtt_common.py`, `publish_event.py`, `job_subscriber.py`, `registry.py` 등의 핵심 모듈이 배치되었는가? - [ ] **스크립트 구비**: `mqtt_common.py`, `publish_event.py`, `job_subscriber.py`, `registry.py` 등의 핵심 모듈이 배치되었는가?
- [ ] **HMAC 활성화**: 새로운 레지스트리 잡 발급 시 난수 기반의 `auth_token`이 정상적으로 주입되고, 서명 기반의 상호 인증이 활성화되는가? - [ ] **HMAC 활성화**: 새로운 레지스트리 잡 발급 시 난수 기반의 `auth_token`이 정상적으로 주입되고, 서명 기반의 상호 인증이 활성화되는가?
+1 -1
View File
@@ -142,7 +142,7 @@ To ensure that agents running in TMUX environments do not lose debug logs or pre
Use this checklist when deploying this agent orchestration model to a new project: Use this checklist when deploying this agent orchestration model to a new project:
- [ ] **Virtualenv Dependencies**: Are required Python packages like `pyyaml` and `paho-mqtt` included in `.venv` or `requirements.txt`? - [ ] **Virtualenv Dependencies**: Are required Python packages like `pyyaml` and `paho-mqtt` included in `.venv` or `requirements.txt`?
- [ ] **Configuration File**: Are the MQTT broker address and security credentials safely loaded and shared via the `.env` file? - [ ] **Configuration File**: Are the MQTT broker address and security credentials safely loaded and shared via the `.mam.env` file?
- [ ] **Directory Convention**: Are the registry path (`.mam/jobs/`) and logging path (`.mam/delegate_job_logs/`) added to `.gitignore`? - [ ] **Directory Convention**: Are the registry path (`.mam/jobs/`) and logging path (`.mam/delegate_job_logs/`) added to `.gitignore`?
- [ ] **Core Scripts**: Are the core scripts (`mqtt_common.py`, `publish_event.py`, `job_subscriber.py`, and `registry.py`) in place? - [ ] **Core Scripts**: Are the core scripts (`mqtt_common.py`, `publish_event.py`, `job_subscriber.py`, and `registry.py`) in place?
- [ ] **HMAC Enablement**: When a new registry job is created, is a random `auth_token` correctly injected, and is signature-based mutual authentication active? - [ ] **HMAC Enablement**: When a new registry job is created, is a random `auth_token` correctly injected, and is signature-based mutual authentication active?
@@ -0,0 +1,321 @@
# 📐 구현 계획서 Rev.2 — `deploy/*` 배포 스크립트 개선 (Job `0d43714b`)
- **작성자**: Planner (`canary-projects-multi-agent-mux-creator-claude`)
- **개정 사유**: Creator `agy`의 이의제기(Job `029f61b1`) 반영
- **선행 문서**: Job `101c90a2` 계획서 Rev.1 (본 문서가 이를 대체함 — 구현 시 **본 문서만** 참조)
- **기준 커밋**: `2ff8b2c` (branch `main`)
---
## 0. 이의제기 판정 (Challenge Adjudication)
`agy`의 지적은 **실재하는 데이터 소실 위험을 정확히 짚었고, 채택합니다.** 다만 원인 귀속과 처방 두 가지에는 실측으로 반박되는 부분이 있어 수정 채택합니다.
| # | `agy`의 주장 / 처방 | 판정 | 근거 |
| :-- | :--- | :---: | :--- |
| ① | 로컬 커스텀 스킬 코드가 무단 덮어쓰기로 소실될 수 있다 | **채택** | E-6/E-7에서 실제 소실 재현. 잔존 사본 0건 |
| ② | "기존 `install.sh`는 파일이 있으면 건너뛰어 안전하게 **보호되었다**" | **반박** | 프레임워크 소유 파일은 이미 `cp -f`**무조건 덮어쓰기**(`install.sh:166`). 보호는 정책이 아니라 fetch 블록이 통째로 스킵된 **부작용**이었음 |
| ③ | "R-1(`MAM_REFRESH=1`)이 이 위험을 **발생시킨다**" | **반박(부분 채택)** | 위험은 **오늘 이미 존재**함 — `install.sh -f`(문서화된 플래그)와 `update.sh` 두 경로에서 재현됨. R-1은 원인이 아니라 **노출 빈도를 넓히는 요인**. 따라서 가드는 R-1의 전제조건으로 **필수**이되, R-1만 고쳐서는 부족 |
| ④ | 처방 A: 덮어쓰기 전 `.agents/skills/.../*.user-bak` 자동 백업 | **반려 → 대체** | `.agents/`는 §5.1에서 **의도적으로 gitignore하지 않기로** 결정한 경로. 백업이 사용자 저장소에 추적 파일로 쌓이고, manifest에 없어 `remove.sh`가 절대 청소하지 못함 → `agy` 본인이 직전 라운드(`c6c43df9`)에서 지적한 **백업 무한 증식**을 재현. `.mam/skill-backups/<ts>/`로 이전 |
| ⑤ | 처방 B: 로컬 변경 감지를 **diff/hash/mtime**로 | **반려 → 대체** | **치명적 오설계.** 수신 파일과 비교하면 "구버전 설치본"과 "사용자 수정"을 구분할 수 없어 **모든 정상 업데이트가 로컬 수정으로 오판**됨 → 갱신이 영구 no-op이 되어 R-1이 고치려던 E-4 버그로 회귀. mtime은 `cp -f`가 매 설치마다 갱신하므로 출처 정보가 아예 없음. **설치기가 마지막에 기록한 해시**와 비교해야 함(§2.2) |
| ⑥ | 처방 C: 기본 보존 + `--overwrite-custom` 플래그 + 안내 문구 | **채택** | 안전한 기본값. 다만 "조용히 건너뛰기"는 버전 불일치를 유발하므로 **항목별 경고 + 상태 기록**을 의무화(§2.4) |
| ⑦ | (미지적) 처방이 `install.sh` 복사 루프에만 적용됨 | **보완 추가** | `update.sh``remove.sh --force`**스킬을 먼저 전부 삭제한 뒤** 재설치한다. install.sh에 가드를 넣어도 이 경로에서는 이미 파일이 없어 아무 효과가 없음(E-7). 3개 경로 전부를 덮어야 함(§3) |
**추가 자기수정(Planner 귀책)**: Rev.1 §5.2가 `gitignore_created``.mam/install_state`에 기록하도록 했으나, `update.sh``.mam`에서 **명시적 allowlist 4종만** 스테이징하므로 업데이트 시 이 상태가 소실됩니다(§4). `agy`가 지적하지 않았지만 같은 계열의 결함이므로 함께 수정합니다.
---
## 1. 신규 실측 근거 (Evidence, Rev.2)
임시 워크스페이스에 정상 설치 후 프레임워크 소유 파일 2개(`scripts/mqtt_common.py`, delegate-job 래퍼)에 로컬 커스터마이즈를 가하고 각 경로를 실행했습니다.
### E-6. 오늘의 `install.sh -f`가 이미 무경고로 파괴함
```
$ bash install.sh -f "$WORK"
exit=0
mqtt_common.py CORP_CA_BUNDLE : 1 → 0 ← 소실
wrapper LOCAL marker : 1 → 0 ← 소실
워크스페이스 내 잔존 사본 : (NONE)
로컬 변경 관련 경고 : 없음
실제 출력된 문구:
"✅ Skills staged into workspace (user documents and custom configs preserved)."
```
마지막 줄이 핵심입니다. 설치기는 **커스텀 설정을 보존했다고 명시적으로 안심시키는 문구를 출력하면서 같은 실행에서 커스텀 코드를 파괴**합니다. 문구의 원래 의도는 `.mam.env`·사용자 문서를 가리키지만, 스킬 수정본을 잃은 사용자에게는 경고가 아니라 **역방향의 오신호**입니다. 즉 `agy`가 지적한 위험은 "R-1이 도입할 미래의 위험"이 아니라 **이미 출시되어 문서화된 플래그에 존재하는 현재의 버그**입니다.
### E-7. 업데이트 경로는 install.sh 가드로 막을 수 없음
`update.sh:151`이 실행하는 명령을 그대로 재현했습니다.
```
$ bash remove.sh --force
exit=0
.agents/skills/.../mqtt_common.py 존재 : NO — deleted
잔존 사본 : (NONE)
```
`remove.sh`가 manifest에 따라 `.agents/skills/**` 28개 파일을 삭제한 **뒤에**`install.sh`가 실행됩니다. 그 시점에 로컬 수정본은 이미 존재하지 않으므로, **복사 루프에 어떤 감지 로직을 넣어도 감지할 대상이 없습니다.** `agy`의 처방을 그대로 구현하면 "가드를 넣었는데도 업데이트 한 번에 코드가 사라진다"는 최악의 결과가 됩니다 — 안전하다고 믿게 만들면서 보호하지 못하는 상태.
### E-8. 감지 기준의 반증
`agy`가 제안한 "기존 파일이 **원본 템플릿과 다른지**" 검사를 그대로 적용하면:
| 상황 | 로컬 파일 vs 수신 템플릿 | 올바른 처리 | `agy` 기준의 판정 |
| :--- | :---: | :--- | :--- |
| 사용자가 수정함 | 다름 | 보존 | 보존 ✅ |
| **구버전이 설치돼 있음(정상 갱신 대상)** | **다름** | **덮어쓰기** | **보존 ❌ → 갱신 영구 실패** |
| 최신본이 이미 설치됨 | 같음 | no-op | no-op ✅ |
2행이 R-1의 **유일한 존재 이유**입니다. 수신 파일과의 비교로는 2행과 1행이 원리적으로 구분되지 않으므로, R-1을 구현하면서 이 기준을 쓰면 E-4(재실행이 조용한 no-op)로 정확히 되돌아갑니다.
---
## 2. 설계 R-1 — 안전 갱신 (Safe Refresh)
### 2.1 원칙
> 갱신은 **설치기가 스스로 쓴 것만** 덮어쓴다. 그 외 모든 것은 사용자 것으로 간주한다.
`.env` 마이그레이션에서 확립한 **증거 기반 소유 판정** 원칙과 동일합니다. 소유 증거는 manifest(경로)만으로 부족하며 **내용 지문**이 필요합니다.
### 2.2 소유 지문 대장 — `.mam/asset_hashes.txt`
설치기가 파일을 쓸 때마다 그 시점의 내용 해시를 기록합니다.
```
<sha256> .agents/skills/lib.sh
<sha256> .agents/skills/multi-agent-mux-delegate-job/scripts/mqtt_common.py
...
```
갱신 시 3-way 판정:
| 조건 | 의미 | 처리 |
| :--- | :--- | :--- |
| `hash(현재) == 대장의 해시` | 설치기가 쓴 그대로 (미변경) | **덮어쓰기** + 대장 갱신 |
| `hash(현재) != 대장의 해시` | 사용자가 수정함 | **보존** + 경고 + 상태 기록 |
| 대장에 항목 없음 (구 설치본) | 출처 불명 | §2.5 부트스트랩 규칙 |
| `hash(현재) == hash(수신)` | 이미 최신 | no-op (백업·경고 불필요) |
대상은 `.agents/skills/**` **28개 파일**(432 KB)뿐이므로 해시 비용은 무시할 수준입니다.
> ⚠️ **구현 함정**: `sha256sum`은 GNU coreutils 전용이며 **stock macOS에는 없습니다**(macOS는 `shasum`). 본 개발 머신에는 coreutils가 설치돼 있어 로컬 테스트는 통과하고 실사용자만 깨지는 전형적 분기가 발생합니다. §2.3의 `python3 hashlib` 인라인을 사용하십시오 — `install.sh`가 이미 python3를 하드 의존성으로 검증합니다(Rev.1 §2.3과 동일한 근거).
### 2.3 백업 위치 — `.mam/skill-backups/<UTC타임스탬프>/<원경로>`
`.user-bak` 인플레이스 방식을 쓰지 않는 이유:
1. `.agents/`**의도적으로 gitignore 대상이 아님**(Rev.1 §5.1) → 백업이 사용자 저장소에 추적 파일로 유입.
2. manifest에 등재되지 않으므로 `remove.sh`**영구히 청소하지 못함** → 언인스톨 후에도 잔재.
3. 갱신할 때마다 누적 → `agy`가 직전 라운드에서 정확히 지적한 **백업 증식** 재현.
`.mam/skill-backups/`는 ① gitignore 관리 블록의 `/.mam/`으로 이미 커버 ② `remove.sh``delete_asset ".mam"`으로 자동 정리 ③ 사용자 트리 무오염을 모두 만족합니다.
**중복 억제(직전 잡 `fe4e0e6f`의 교훈 적용)**: 백업 직전 기존 `skill-backups/*/<같은 경로>` 중 내용이 동일한 사본이 있으면 새로 만들지 않습니다. `cmp` 실패 시에는 **보존 쪽으로 실패**(백업 생성)합니다.
### 2.4 기본 동작과 플래그
```bash
--overwrite-custom # 로컬 수정본까지 덮어쓴다 (백업은 여전히 남김)
MAM_OVERWRITE_CUSTOM=1 # curl | bash 파이프용 환경변수 동치
```
- **기본값 = 보존**. `agy`의 처방 C를 채택합니다.
- 보존 시 **파일 목록을 항목별로 출력**해야 합니다. 총계만 찍으면 사용자는 무엇이 낡았는지 알 수 없습니다.
```
️ Local modifications detected — these files were NOT updated:
.agents/skills/multi-agent-mux-delegate-job/scripts/mqtt_common.py
.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job
Your copies are preserved as-is. Backups: .mam/skill-backups/20260804T120000Z/
⚠️ Mixing customised files with updated framework files can break the backplane.
To take the upstream version (a backup is still made): re-run with --overwrite-custom
```
- **버전 불일치 경고를 의무화**하는 이유: delegate-job 백플레인은 `registry.py`·`mqtt_common.py`·`publish_event.py`·래퍼가 한 벌로 동작합니다. 한 파일만 구버전으로 남으면 "업데이트 성공"이라 표시된 채 이벤트가 실패하는, 진단이 어려운 상태가 됩니다. 조용한 skip은 금지합니다.
- `.mam/version.txt``preserved_local=<n>`을 기록해 사후 진단 가능하게 합니다.
### 2.5 부트스트랩(대장이 없는 기존 설치본)
기존 설치본에는 `asset_hashes.txt`가 없습니다. 여기서 "출처 불명 = 보존"을 택하면 **설치 기반 전체가 첫 갱신에서 no-op**이 되어 R-1이 무력화됩니다. 따라서:
> 대장 없음 + 내용이 수신본과 다름 → **백업 후 덮어쓰기**, 그리고 그 사실을 출력.
신선도(R-1의 목적)와 복구 가능성(agy의 목적)을 동시에 만족하며, 이 1회 이후로는 대장이 존재하므로 정밀 판정(기본 보존)으로 전환됩니다. 내용이 수신본과 같으면 백업도 경고도 만들지 않습니다.
```
️ No asset fingerprints found (installed by an older version).
Backing up current skills to .mam/skill-backups/<ts>/ before refresh.
```
---
## 3. 3개 경로 전수 적용 (`agy` 처방의 결정적 확장)
| # | 경로 | 현재 위험 | Rev.2 처리 |
| :-- | :--- | :--- | :--- |
| P-A | `install.sh` 갱신(`-f` 및 R-1 기본 fetch) | E-6: 무경고 파괴 | §2 안전 갱신 적용 |
| P-B | `update.sh` (문서화된 **주 업데이트 수단**) | E-7: `remove.sh`가 선삭제 → 가드 무효 | **remove.sh 호출 전 스냅샷**(§3.1) |
| P-C | `remove.sh` 단독 실행(언인스톨) | 수정본이 조용히 삭제됨 | 항목별 경고 + 조건부 보존(§3.2) |
### 3.1 P-B — `update.sh` 선스냅샷
`update.sh`는 이미 `.mam.update-tmp` 스테이징 구조를 갖고 있으므로 여기에 얹습니다.
```
1) (remove.sh 호출 전) asset_hashes.txt 기준으로 수정된 프레임워크 파일 산출
2) .mam.update-tmp/skill-backups/<ts>/ 로 복사 ← remove.sh가 지우지 못하는 위치
3) bash "$REMOVER" --force "$TARGET_DIR"
4) 새 install.sh 실행
5) .mam.update-tmp/skill-backups → .mam/skill-backups 로 복원 + 목록 출력
```
**주의**: 이 스냅샷은 "수정본을 자동으로 되살리지 않습니다." 업데이트 후 트리에는 최신 프레임워크가 들어가고, 사용자 수정본은 백업으로만 남습니다. 자동 병합은 3-way merge가 필요해 셸 설치기의 책임 범위를 넘습니다. **출력에서 이 점을 명확히 말해야 합니다** — "백업했다"가 "복원했다"로 오해되면 안 됩니다.
```
💾 3 locally-modified skill file(s) backed up to .mam/skill-backups/<ts>/
The updated framework files are now in place; your changes were NOT re-applied.
Diff and re-apply manually if you still need them.
```
### 3.2 P-C — `remove.sh` 언인스톨
- 삭제 대상 중 로컬 수정 파일이 **있을 때만** 동작합니다(없으면 출력·디렉토리 생성 모두 없음 → 일반 사용자에게 잔재 0).
- 대화형: 확인 프롬프트에 수정 파일 목록을 포함해 사용자가 중단할 수 있게 합니다.
- `-y/--force`: 목록을 출력하고 `<workspace>/.mam-skill-backup.<ts>/`로 보존합니다. `.mam` 내부는 언인스톨 시 삭제되므로 쓸 수 없습니다.
- `--purge-skills` 지정 시에만 보존 없이 삭제합니다.
> 이는 직전 잡(P-1)에서 확립한 "`-y`(비대화 의도)는 삭제 권한이 아니다" 원칙의 직접 적용이며, 루트에 백업을 남기는 것은 `.mam.env.mam-backup`이 이미 따르는 기존 전례와 일관됩니다. 관리 블록에 `/.mam-skill-backup.*/`를 추가합니다(§5).
---
## 4. 자기수정 — `.mam` 잔존 allowlist 결함
`update.sh:94-120``.mam`에서 **다음 4종만** 스테이징합니다: `agent-sessions.*`, `jobs/`, `delegate_job_logs/`, `install_manifest.txt`. `remove.sh``.mam`을 통째로 지우므로 **여기 없는 것은 업데이트 때마다 소멸**합니다.
영향:
| 파일 | 도입 위치 | 소실 시 결과 |
| :--- | :--- | :--- |
| `.mam/install_state` (`gitignore_created`) | Rev.1 §5.2 | 언인스톨 시 `.gitignore` 소유 판정 불가 → 빈 파일 잔존(안전측 실패, 경미) |
| `.mam/asset_hashes.txt` | Rev.2 §2.2 | **업데이트마다 대장 소실 → 매번 부트스트랩 경로 → 정밀 판정이 영구히 동작하지 않음** |
| `.mam/version.txt` | Rev.1 §6.2 | 버전 이력 단절 |
| `.mam/skill-backups/` | Rev.2 §2.3 | 백업이 업데이트로 삭제 |
**조치**: `update.sh`의 스테이징/복원 목록에 위 4개를 추가하고, 나아가 **개별 열거 대신 `.mam` 전체를 복사한 뒤 새 manifest만 새것으로 덮는 방식**으로 바꿀 것을 권고합니다(신규 상태 파일이 추가될 때마다 이 목록을 고쳐야 하는 구조적 취약성 제거). 후자를 택할 경우 `.mam/jobs` 용량이 큰 워크스페이스에서 복사 비용이 늘 수 있으므로, `cp -a`로 한 번에 처리하고 실패 시 기존 트랩(`restore_on_failure`)에 위임합니다.
---
## 5. Rev.1에서 변경되는 항목 요약
| 절 | 변경 |
| :--- | :--- |
| §5.1 관리 블록 | `/.mam-skill-backup.*/` 1행 추가 |
| §5.2 소유권 기록 | `.mam/install_state` 유지, 단 §4에 따라 `update.sh` 잔존 목록에 반드시 포함 |
| §6.1 fetch 기본화 | **§2 안전 갱신 가드 구현이 선행 조건**. 가드 없이 기본값만 바꾸는 커밋은 금지 |
| §6.2 버전 스탬프 | `preserved_local=<n>` 필드 추가 |
| §7 커밋 | C3 분할 및 C12~C14 추가(§6) |
| §8 테스트 | T-D21~T-D28 추가(§7) |
| §11 리스크 | RK-8 신설, RK-5 완화책 보강 |
그 외 R-2(필수 마크다운), R-3(`.mam_deploy`), R-4(`.gitignore`) 설계와 B-1/B-2/B-3 차단 항목은 **Rev.1 그대로 유효**합니다. `agy`도 해당 부분에는 이의를 제기하지 않았습니다.
---
## 6. 개정 커밋 분해
| # | 커밋 | 대상 | 비고 |
| :-- | :--- | :--- | :--- |
| C1 | `feat(deploy): ship only runtime-essential markdown to targets` | `install.sh` | Rev.1 §3.2 |
| C2 | `fix(deploy): align install_mam.sh asset excludes with install.sh` | `install_mam.sh` | Rev.1 §3.3 |
| **C3a** | `feat(deploy): record asset fingerprints on install` | `install.sh` | §2.2 — **대장 기록만**, 판정 로직 없음(동작 무변화) |
| **C3b** | `feat(deploy): preserve locally modified skills on refresh` | `install.sh` | §2.3-2.5 — 백업·보존·`--overwrite-custom` |
| **C3c** | `feat(deploy): fetch latest assets by default and stamp version` | `install.sh` | Rev.1 §6 — **C3b 이후에만 머지 가능** |
| C4 | `refactor(deploy): resolve workspace from script location` | `remove.sh`, `update.sh` | Rev.1 §4.2 |
| C5 | `feat(deploy): support .mam_deploy layout in uninstaller/updater` | `remove.sh`, `update.sh` | Rev.1 §4.3/4.4 |
| C6 | `feat(deploy): install remove.sh/update.sh under .mam_deploy/` | `install.sh` | **C5 이후** |
| C7 | `feat(deploy): manage a .gitignore block for installed artifacts` | `install.sh` | Rev.1 §5.1-5.3 + §5 |
| C8 | `feat(deploy): strip the managed .gitignore block on uninstall` | `remove.sh` | **C7 이후** |
| C9 | `feat(deploy): unify install_mam.sh gitignore and deploy scripts` | `install_mam.sh` | Rev.1 §5.4 |
| **C12** | `fix(deploy): preserve .mam state files across the update cycle` | `update.sh` | §4 — **C3a 이후, C3c 이전** |
| **C13** | `feat(deploy): snapshot modified skills before update removal` | `update.sh` | §3.1 |
| **C14** | `feat(deploy): warn and preserve modified skills on uninstall` | `remove.sh` | §3.2 — GM 판단으로 분리 가능(분리 시 P-C 구멍 잔존 명시) |
| C10 | `test(deploy): cover asset allowlist, layout, gitignore and safe refresh` | `tests/test_deploy_layout.py` | §7 |
| C11 | `docs(deploy): document layout, refresh, gitignore and custom-skill policy` | 문서 4종 | Rev.1 §9 + `--overwrite-custom`·백업 정책 |
**신규 순서 제약 (위반 시 데이터 소실 커밋이 트리에 남음):**
- **C3a → C3b → C3c** — 대장 없이 판정 로직을 넣으면 전량 부트스트랩 경로로 빠지고, 가드 없이 fetch만 기본화하면 **E-6의 파괴를 전 사용자에게 기본값으로 배포**하게 됩니다. C3c를 먼저 머지하는 것은 **금지**입니다.
- **C3a → C12** — 대장을 만들자마자 업데이트가 그것을 지우면 §2.2가 영구히 부트스트랩 모드로 동작합니다.
- **C13은 C3a 이후** — 수정 파일 산출에 대장이 필요합니다.
---
## 7. 추가 테스트 (Rev.1 T-D1~T-D20에 이어서)
| ID | 검증 내용 | 판정 |
| :--- | :--- | :---: |
| T-D21 | 커스터마이즈된 스킬 파일이 있는 상태로 갱신 → **파일 내용 보존**, exit 0, 목록이 stdout에 출력됨 | **B-4** |
| T-D22 | 미변경 스킬 파일은 정상 갱신됨(E-4 회귀 방지가 T-D21에 의해 무력화되지 않았는지) | **B-4** |
| T-D23 | **구버전이 설치된 상태(대장 존재, 사용자 미수정)에서 갱신 시 덮어써짐** — §E-8 2행 오판 방지 | **B-4** |
| T-D24 | `--overwrite-custom` 시 덮어쓰되 `.mam/skill-backups/<ts>/`에 사본 존재 | |
| T-D25 | 백업이 `.agents/` 하위에 **생성되지 않음**(`find .agents -name '*.user-bak'` → 0건) | |
| T-D26 | 동일 내용 2회 갱신 시 백업 디렉토리 **증식하지 않음**(중복 억제) | |
| T-D27 | `update.sh` 1회 실행 후 `.mam/asset_hashes.txt`·`install_state`·`version.txt`·`skill-backups/`**모두 잔존** | **B-5** |
| T-D28 | 수정본이 있는 상태로 `update.sh` 실행 → 최신 프레임워크 적용 + 백업 존재 + "NOT re-applied" 문구 출력 | |
| T-D29 | 대장 없는 구 설치본 갱신 → 백업 생성 후 덮어쓰기, 2회차부터는 정밀 판정 | |
| T-D30 | `remove.sh -y` (수정본 존재) → `.mam-skill-backup.<ts>/` 생성; 수정본 없으면 **디렉토리 미생성** | C14 채택 시 |
| T-D31 | 해시 계산이 `sha256sum`/`shasum` 존재 여부에 의존하지 않음(PATH에서 둘 다 제거해도 통과) | |
**차단 항목 추가**: **B-4**(안전 갱신 3-way 판정 — 실패 시 사용자 코드 소실 또는 갱신 영구 실패), **B-5**(`.mam` 상태 파일 잔존 — 실패 시 B-4가 구조적으로 동작 불능).
**차단 항목은 총 5건: B-1, B-2, B-3, B-4, B-5.**
---
## 8. DoD 게이트 (추가분)
| 게이트 | 조건 |
| :--- | :--- |
| **I. 커스텀 보존** | 수정된 스킬 파일이 `install.sh` 갱신·`install.sh -f`·`update.sh` **3경로 모두**에서 소실되지 않음(원본 보존 또는 백업 존재) |
| **J. 갱신 유효성** | 미수정 파일은 3경로 모두에서 최신본으로 갱신됨 — 보존 로직이 R-1을 무력화하지 않았음을 증명 |
| **K. 무오염** | 갱신·업데이트·언인스톨 후 `.agents/` 하위에 백업/잔재 파일 0건 |
| **L. 문구 정합** | "custom configs preserved" 류 문구가 **실제로 보존된 대상만** 지칭하도록 수정됨(E-6의 오신호 제거) |
게이트 L은 문구 한 줄이지만 별도 항목으로 둡니다. E-6에서 확인했듯 **틀린 안심 문구는 경고가 없는 것보다 나쁩니다.**
---
## 9. 리스크 (개정)
| ID | 리스크 | 영향 | 완화 |
| :-- | :--- | :---: | :--- |
| RK-1~RK-7 | Rev.1과 동일 | — | Rev.1 §11 |
| **RK-8** | 보존 로직이 과도하게 동작해 갱신이 사실상 no-op화(E-4 회귀) | **높음** | 수신본이 아닌 **대장**과 비교(§2.2), T-D22/T-D23이 차단 |
| **RK-9** | 백업했다는 문구를 사용자가 "복원됐다"로 오해 | 중 | §3.1 문구 규정, 게이트 L |
| **RK-10** | C3c(fetch 기본화)를 C3b보다 먼저 머지 | **치명** | §6 순서 제약, B-4 |
| **RK-11** | 버전 스큐(구 파일 1개 + 신 파일 다수)로 백플레인 오작동 | 중 | 항목별 경고 의무화(§2.4), `--overwrite-custom` 안내 |
| RK-5(개정) | fetch 기본화로 오프라인/CI 실패 | 중 | `--no-refresh` + 자산 부재 시 fetch 유지 |
---
## 10. 결론
`agy`의 이의제기는 **채택**합니다. 다만 세 가지를 수정합니다.
1. 이 위험은 R-1이 만드는 것이 아니라 **`install.sh -f``update.sh`에 이미 존재하는 버그**입니다(E-6/E-7). 따라서 가드는 R-1의 부속이 아니라 **선행 조건**이며, R-1을 채택하지 않더라도 독립적으로 고쳐야 합니다.
2. 감지 기준을 **수신 템플릿과의 비교에서 설치 시점 해시 대장과의 비교로** 바꿉니다. 원안대로면 정상 갱신과 사용자 수정이 구분되지 않아 R-1이 무력화됩니다(E-8).
3. 백업 위치를 `.agents/**/*.user-bak`에서 **`.mam/skill-backups/<ts>/`**로 옮기고, 보호 범위를 `install.sh` 복사 루프에서 **install/update/remove 3경로 전체**로 확장합니다. 원안 위치는 gitignore되지 않고 manifest에도 없어 영구 잔재가 되며, 원안 범위는 주 업데이트 경로를 전혀 보호하지 못합니다(E-7).
부수적으로, `agy`가 지적하지 않았으나 같은 계열인 Planner 귀책 결함 1건(`.mam` 상태 파일이 업데이트 시 소멸, §4)을 함께 수정합니다.
---
## 11. 역할 경계
본 문서는 설계 산출물이며 **Planner는 저장소 코드를 일절 수정하지 않았습니다**(`MULTI_AGENT_RULES.md` §4). E-6~E-8 실험은 임시 디렉토리(`/tmp/mam_cust_*`)에서 수행 후 정리했으며, 저장소 워킹트리는 클린 상태입니다. 구현은 Creator, 커밋은 GM 소관입니다.
**차단 항목은 B-1, B-2, B-3, B-4, B-5 5건입니다.**
[AGREEMENT: REACHED]
@@ -0,0 +1,371 @@
# 📐 구현 계획서 Rev.2 — `.env` → `.mam.env` 마이그레이션 최종화 (Finalize)
- **Job ID**: `fe4e0e6f`
- **Role**: Planner
- **목표**: `CURRENT_JOB.md` 기반 마이그레이션 최종화 · 원자적 커밋 · 리뷰어 검증 통과
- **선행 산출물**: `78e83796`(Rev.1) → `6dc9d528`(Rev.2) → `4e8b4839`(리뷰 `[VERDICT: NOT PASS]`) → `1b40c4ee`(최종화 계획 Rev.1) → **본 문서 (Rev.2)**
- **반영 피드백**: Creator `agy` Challenge Report — Job `c6c43df9`
---
## 0. 이의제기 판정 요약 (Challenge Adjudication)
Creator `agy`는 P-1 조치안(`-y` 실행 시 삭제 대신 백업)에 대해 **백업 파일 무한 증식**과 **완전 삭제 불능**을 지적했습니다. 실제 시나리오를 3주기 재현하여 검증했습니다.
| 항목 | 판정 | 근거 |
| :--- | :---: | :--- |
| **진단** — 반복 주기마다 백업 누적 | ✅ **채택** | 3주기 → 백업 3개 생성. 실측 확인 |
| 진단 — 백업 잔재를 타 도구가 오참조 | ❌ **기각** | `*.mam-backup`**읽는 코드는 전무**. `remove.sh`가 쓰기만 함. 전부 `.gitignore` 적용됨 |
| 진단 — `-y`로 완전 삭제 불가 | ⚠️ **부분 채택** | 사실이나 `--purge-env`가 이미 그 역할. 안내 부재가 진짜 문제 |
| **처방 ① 단일 슬롯 덮어쓰기** | 🔴 **기각 — 데이터 손실 재유발** | 아래 §1에서 실측 증명 |
| 처방 ② `--no-backup` 플래그 신설 | ❌ **기각** | `--purge-env`와 의미 중복. 플래그 2개가 같은 일을 하면 P-1의 "권한 붕괴"가 재발 |
| 처방 ② 안내 문구 강화 | ✅ **채택** | stdout 가이드 추가 |
**결론**: `agy`**문제 제기는 타당하나 처방은 위험합니다.** 진단을 채택하되 처방은 교체합니다. 대안으로 **내용 기반 중복 제거(content dedup) + 최초 백업 불변(immutable slot 1)**을 제시합니다.
---
## 1. 🔴 `agy` 처방 ①(단일 슬롯 덮어쓰기)을 기각하는 이유 — 실측
`remove.sh -y``install.sh` 주기를 3회 반복하며 각 백업의 **내용**을 측정했습니다. (P-1 패치를 적용한 사본으로 실행. 리포지토리 코드는 미수정.)
```
CYCLE 0: 사용자 실제 설정 저장 → .mam.env = MQTT_PASSWORD=REAL_USER_SECRET
CYCLE 1: remove.sh -y → backups: .mam.env.mam-backup
reinstall → .mam.env 재생성됨 (설치기 기본값)
CYCLE 2: remove.sh -y → backups: .mam.env.mam-backup .mam.env.mam-backup.20260804173654
CYCLE 3: remove.sh -y → backups: … + .mam.env.mam-backup.20260804173658
```
**핵심 측정 — 각 백업의 내용:**
```
[.mam.env.mam-backup] -> REAL_USER_SECRET 1건 ← 사용자 실제 설정
[.mam.env.mam-backup.20260804173654] -> REAL_USER_SECRET 0건 ← 설치기 생성 기본값
[.mam.env.mam-backup.20260804173658] -> REAL_USER_SECRET 0건 ← 설치기 생성 기본값
cycle-2/3 백업 md5: 10ed588bc64422408fda750b566e9197 (완전 동일)
```
여기서 두 가지가 드러납니다.
**(1) 증식의 실체는 "무가치한 사본의 반복"입니다.**
사용자의 진짜 설정은 **오직 슬롯 1**에만 있습니다. 2주기 이후 백업은 `install.sh`가 방금 만든 기본 설정을 되받아 적은 것이며, 서로 **바이트 단위로 동일**합니다. 즉 증식은 "정보가 늘어나는 것"이 아니라 **같은 쓰레기가 늘어나는 것**입니다. → 내용 기반 중복 제거로 완전히 해결 가능합니다.
**(2) 단일 슬롯 덮어쓰기는 그 유일한 진짜 설정을 파괴합니다.**
`agy`의 처방 ①을 실제로 적용해 보았습니다:
```
BEFORE — 슬롯 1의 REAL_USER_SECRET 보유: 1건
현재 live .mam.env 의 보유: 0건 (설치기 기본값)
$ mv -f .mam.env .mam.env.mam-backup # ← 처방 ①: 단일 슬롯 덮어쓰기
AFTER — 슬롯 1의 REAL_USER_SECRET 보유: 0건
워크스페이스 전체에서 REAL_USER_SECRET 잔존 사본: (NONE — 사용자 설정 소실)
```
**단일 슬롯 덮어쓰기는 P-1이 막으려던 바로 그 비가역 데이터 손실을, 1주기 지연시켜 재현합니다.** 원래 P-1은 "즉시 삭제"였고 처방 ①은 "다음 주기에 삭제"입니다. 손실 시점만 다를 뿐 결과는 동일하며, 오히려 **"백업했다"는 로그가 남아 있어 더 탐지하기 어렵습니다.**
역설적으로, 현재 코드의 타임스탬프 폴백(`remove.sh:182-184`)은 **바로 이 사고를 막고 있던 안전장치**였습니다. 이것을 제거해서는 안 됩니다.
**기각 사유 요약**: 디스크 정리(위생 문제)를 위해 데이터 보존(정확성 문제)을 희생하는 교환입니다. 우선순위가 역전되어 있습니다.
---
## 2. ✅ P-1 조치안 개정 (Revised Remedy)
### 2-1. 삭제 권한 분리 — Rev.1과 동일 (변경 없음)
```bash
should_delete_env=0
if [ $PURGE_ENV -eq 1 ]; then
should_delete_env=1
elif [ $env_created_by_mam -eq 1 ] && [ $FORCE -eq 0 ]; then
if ! read -p "❓ MAM-created '$env_name' found. Delete it? (Saying No preserves it) [y/N]: " -r env_response; then
env_response="n"
fi
if [[ "$env_response" =~ ^[yY](es)?$ ]]; then
should_delete_env=1
fi
fi
```
### 2-2. 🆕 백업 정책 개정 — 내용 기반 중복 제거 + 슬롯 1 불변
`agy`가 제기한 증식 문제를 **데이터 손실 없이** 해소합니다.
```bash
# 원칙: 기존 백업은 절대 덮어쓰지 않는다.
# 동일 내용이 이미 보존돼 있으면 새 사본을 만들지 않는다.
preserve_env() {
local env_name="$1"
local slot existing
# (a) 이미 동일 내용이 보존돼 있으면 중복 생성 없이 정리만 한다
for existing in "${env_name}.mam-backup" "${env_name}".mam-backup.*; do
[ -f "$existing" ] || continue
if cmp -s "$env_name" "$existing"; then
rm -f "$env_name"
echo "️ '$env_name' is already preserved in $existing (no duplicate created)."
return 0
fi
done
# (b) 내용이 다르면 새 슬롯에 보존한다. 슬롯 1은 영구 불변.
slot="${env_name}.mam-backup"
if [ -e "$slot" ]; then
slot="${env_name}.mam-backup.$(date +%Y%m%d%H%M%S)"
# 동일 초 내 재실행 충돌 방지
local n=1
while [ -e "$slot" ]; do
slot="${env_name}.mam-backup.$(date +%Y%m%d%H%M%S)-$n"
n=$((n + 1))
done
fi
mv "$env_name" "$slot"
echo "💾 Backed up $env_name -> $slot"
echo " To remove the configuration entirely, re-run with --purge-env."
}
```
**효과 (측정 기반 예측)**:
| 시나리오 | Rev.1 계획 | **Rev.2 개정안** | `agy` 처방 ① |
| :--- | :---: | :---: | :---: |
| 3주기 반복 후 백업 개수 | 3개 | **1개** | 1개 |
| 사용자 실제 설정 보존 | ✅ | ✅ | 🔴 **소실** |
| 내용이 다른 설정 2종 보존 | ✅ | ✅ | 🔴 소실 |
| 동일 초 내 2회 실행 | ⚠️ 충돌 | ✅ 카운터 | 🔴 소실 |
**주의 — (a)의 `cmp` 실패 시 동작**: `cmp`가 어떤 이유로든 실패하면 `rm`이 실행되지 않고 (b)로 진행해 백업이 생성됩니다. 즉 **판단 불능 시 보존 쪽으로 실패(fail-safe)** 합니다. 이 방향성을 반드시 유지해야 합니다.
### 2-3. 🆕 `--purge-env` 안내 강화 (`agy` 처방 ② 중 채택분)
비대화형 실행 시 stdout에 정리 방법을 명시합니다 (위 `preserve_env` 마지막 2줄). `--no-backup`**신설하지 않습니다**`--purge-env`와 기능이 동일하며, 같은 의미의 플래그를 2개 두는 것이 애초 P-1(`-y``--purge-env`의 권한 붕괴)의 원인이었습니다.
### 2-4. 📌 근본 해법은 별건 (범위 외 · 후속 과제로 등재)
증식의 **진짜 원인**은 백업 정책이 아니라, **백업이 바로 옆에 있는데도 `install.sh`가 기본 설정을 새로 생성한다**는 점입니다(M-1 가드가 `*.mam-backup`을 고려하지 않음). `install.sh`가 백업을 감지해 복원하도록 하면 증식은 발생 자체가 사라지고 재설치 UX도 개선됩니다.
다만 이는 **설치기 동작 변경**으로 별도 설계·검증이 필요하므로 본 마이그레이션 범위에서 제외하고 **후속 과제(FU-1)** 로 등재합니다. 2-2의 dedup만으로 `agy`가 제기한 증식은 실측상 해소됩니다.
---
## 3. 선행 리뷰 7개 항목 — 검증 결과 (변경 없음)
실제 명령 실행으로 확인한 현재 워킹 트리 상태 기준입니다.
| # | 리뷰(`4e8b4839`) 지적 | 상태 | 근거 |
| :--- | :--- | :---: | :--- |
| 1 | `remove.sh:192` 고아 `fi` | ✅ 해결 | `deploy/*.sh` 5개 전부 `bash -n` 통과 |
| 2 | T-8/T-10/T-11/T-12 미구현 | ⚠️ 부분 | T-8·10·12·13 추가. **T-11·14·15 없음** |
| 3 | T-4 무력 테스트 | ✅ 해결 | `patch.object(__file__)` 후 인자 없이 호출 — 실제 경계 탐색 진입 |
| 4 | 문서 13개소 | ⚠️ 거의 | **BOOTSTRAP 2개 `.gitignore` 예시만 잔존** (P-6) |
| 5 | 매니페스트 소유권 재기록 | ✅ 해결 | `install.sh:288-305` |
| 6 | 래퍼 cwd 폴백 | ✅ 해결 | `REPO_ROOT` 우선 + 단계별 경고 |
| 7 | `.tmp` 잔여물 | ✅ 해결 | 없음 |
**`CURRENT_JOB.md`의 파급 범위 오기**: `:23``lib.sh`에 ".env 로딩 로직"이 있다고 기술하나 **사실이 아닙니다.** `lib.sh``.env` 매칭 27건은 전부 `os.environ` 부분 문자열, dotenv 참조는 **0건**. `lib.sh`는 **범위 제외**이며 이 오기를 근거로 수정하면 불필요한 회귀 위험만 발생합니다.
---
## 4. 🔴 머지 차단 결함 (Merge Blockers)
### P-1 — `--force`가 사용자 설정을 백업 없이 삭제 (조치안은 §2로 개정)
**재현:**
```bash
printf '.env\nremove.sh\n' > .mam/install_manifest.txt
printf 'SECRET_KEY=user_secret_data\n' > .env
bash remove.sh --force
# EXITCODE=0 / 남은 파일: (없음) / .env.mam-backup 미생성 → 비가역 소실
```
**근본 원인**: 인자 파서(`remove.sh:17-20`)가 `-y|--yes|--force`를 하나의 `FORCE`로 묶고, 섹션 5가 `FORCE=1`을 삭제 권한으로 해석합니다. 결과적으로 ① 백업 브랜치가 **도달 불가능한 죽은 코드**가 되고, ② `--purge-env`가 **의미상 무의미**해지며, ③ 대화형은 "No"로 보존되는데 **비대화형은 묻지도 않고 삭제** — 가장 위험한 쪽이 기본 동작입니다.
**영향 범위**: `update.sh` 경로는 **안전**합니다(`:86,91`이 `remove.sh --force` 호출 `:151` 이전에 `*.update-tmp`로 이동 → 섹션 5의 `[ -f "$env_name" ] || continue`에 걸림). 피해자는 **`remove.sh -y`를 직접 실행하는 사용자/CI**로 한정됩니다. 한정되지만 비가역입니다.
### P-2 — T-8 단언문이 데이터 손실을 통과 판정
`tests/test_env_migration.py:134`:
```python
self.assertTrue(os.path.exists(".env.mam-backup") or not os.path.exists(".env"))
```
`or not os.path.exists(".env")` 때문에 **`.env`가 삭제되기만 하면 무조건 통과**합니다. 막아야 할 실패 양상이 곧 통과 조건이 되는 논리 역전이며, P-1이 지금까지 발견되지 않은 직접적 원인입니다.
**조치** — 보존 검증과 삭제 검증을 분리하고, §2-2 개정에 맞춰 케이스를 확장합니다.
```python
def test_t8_remove_force_preserves_owned_env(self):
"""T-8 [BLOCKER]: --force must BACK UP owned env, never delete it."""
res = subprocess.run(["bash", "remove.sh", "--force"], capture_output=True, text=True)
self.assertEqual(res.returncode, 0, f"remove.sh failed: {res.stderr}")
self.assertTrue(os.path.exists(".env.mam-backup"),
"MAM-owned .env MUST be backed up under --force, never deleted")
with open(".env.mam-backup") as f:
self.assertIn("user_secret_data", f.read())
def test_t8b_purge_env_is_sole_delete_authority(self):
"""T-8b: --purge-env is the ONLY flag authorised to delete."""
subprocess.run(["bash", "remove.sh", "--force", "--purge-env"], check=True)
self.assertFalse(os.path.exists(".env"))
self.assertFalse(os.path.exists(".env.mam-backup"))
```
**핵심 원칙**: 보존 계열 단언에 `or`를 쓰지 않습니다. `or` 대안지는 실패 양상을 흡수합니다.
---
## 5. 🟡 강화 항목 (비차단)
### P-3 — T-7이 이름과 무관한 것을 검증
docstring은 "cwd 상대 경로 로딩 시 경고"를 주장하나 실제로는 `--help` 종료 코드만 봅니다.
**조치**: 임시 디렉터리에 `.env`를 두고 그곳을 cwd로 래퍼 실행 → stderr에 `WARNING`/`deprecated` 포함 단언. 불가하면 docstring을 실제 검증 내용(`smoke: wrapper executes`)으로 정정해 **거짓 안전감을 제거**.
### P-4 — T-10/T-12의 공허한 통과 위험
T-10은 `subprocess.run(...)` 결과를 **어디에도 단언하지 않습니다**. `install.sh`가 초기 실패해도 통과합니다.
**단, 현재는 진짜로 통과합니다** (재현 확인: `EXITCODE=0`, `Preserved without shadowing`, `.mam.env` 미생성). 문제는 미래 회귀를 못 잡는다는 점입니다.
**조치**: `assertEqual(res.returncode, 0)` + **섹션 5 도달 표지 문자열** 단언.
```python
self.assertEqual(res.returncode, 0, f"install.sh failed: {res.stderr}")
self.assertIn("Preserved without shadowing", res.stdout)
self.assertFalse(os.path.exists(".mam.env"))
```
### P-5 — 미구현 테스트 T-11 / T-14 / T-15 (+ 신규 T-16 / T-17)
| ID | 검증 내용 | 방어 대상 |
| :--- | :--- | :--- |
| **T-11** | 구 `update.sh` 전체 시퀀스 E2E → 종료 후 사용자 설정값이 **실제로 로드됨** | E12 섀도잉 |
| T-14 | `update.sh` 중도 실패 → `restore_on_failure`가 **원래 이름**으로 복원 | M-4 트랩 대칭 |
| T-15 | `.mam.env.pre-migrate.bak``.mam.env.update-tmp` 상호 미간섭 | E15 슬롯 충돌 |
| **T-16** 🆕 | `remove.sh -y``install.sh` **3주기 반복 → 백업 파일 정확히 1개** | `agy` 증식 지적 회귀 |
| **T-17** 🆕 | 위 3주기 후 **슬롯 1이 최초 사용자 설정을 그대로 보유** | **처방 ① 재도입 방지 — 데이터 손실 회귀** |
**T-17은 머지 차단**으로 지정합니다. §1에서 실측으로 재현된 비가역 데이터 손실의 회귀 가드이기 때문입니다. (재현된 결함에만 차단을 부여한다는 본 계획서의 일관된 기준에 부합합니다.)
T-11은 Rev.2 명세상 차단이었으나, 코드 검토상 `update.sh:171-176` 복원 분기가 대칭이고 **실동작 결함이 재현되지 않아** 최우선 강화 항목으로 유지합니다. 리뷰어가 이견을 제시하면 원안(차단)으로 복귀합니다.
### P-6 — BOOTSTRAP `.gitignore` 예시의 유령 파일 참조
`BOOTSTRAP.md:126-130` / `BOOTSTRAP.ko.md:126-130``!.env.example`은 rename으로 **더 이상 존재하지 않는 파일**의 예외 규칙이며 실제 `.gitignore`(`:17-22`)와도 불일치합니다. 레거시 2줄(`.env`, `.env.*`)은 구 사용자 보호를 위해 유지가 타당합니다.
**조치**: `!.env.example` 줄 제거 또는 `# legacy — 구 설치 호환용` 주석 병기.
### P-7 — `CURRENT_JOB.md` 처리
세션 UUID·에이전트 상태 등 휘발성 런타임 정보를 담은 untracked 문서이며 §3의 `lib.sh` 오기를 포함합니다.
**권고**: **커밋하지 않고** `.gitignore`에 등재.
---
## 6. 🧩 원자적 커밋 전략
**원칙**: 각 커밋은 단독으로 문법상 유효하고, `git bisect`로 회귀를 단일 커밋까지 좁힐 수 있어야 합니다. 파일이 아니라 **관심사** 기준으로 자릅니다.
| # | 커밋 | 대상 | 메시지(안) |
| :---: | :--- | :--- | :--- |
| **C1** | 템플릿 rename + ignore 규칙 (+P-7) | `.mam.env.example`(staged rename), `.gitignore` | `refactor(config): rename .env.example to .mam.env.example and isolate .mam.env in gitignore` |
| **C2** | dotenv 로더 경계 수정 | `…/scripts/mqtt_common.py` | `fix(config): resolve dotenv via workspace marker and prefer .mam.env over legacy .env` |
| **C3** | 래퍼 env 해석 | `…/multi-agent-mux-delegate-job` | `fix(config): resolve wrapper env from repo root and warn on deprecated .env` |
| **C4** | 생성 스크립트 | `deploy/generate-env.sh` | `feat(deploy): target .mam.env and add --migrate-legacy flag` |
| **C5** | 설치기 (M-1 + M-2) | `deploy/install.sh`, `deploy/install_mam.sh` | `feat(deploy): add shadowing guard and evidence-based legacy env migration` |
| **C6** | **언인스톨러 (P-1 + §2-2 백업 정책)** | `deploy/remove.sh` | `fix(deploy): preserve MAM-owned env under --force and dedupe backups` |
| **C7** | 업데이터 대칭성 | `deploy/update.sh` | `fix(deploy): pre-capture env ownership and keep backup/restore symmetric` |
| **C8** | **테스트 (P-2~P-5, T-16/T-17 포함)** | `tests/test_env_migration.py` | `test: cover .mam.env migration, shadowing, ownership and backup retention` |
| **C9** | 문서 (P-6) | `README{,.ko}.md`, `BOOTSTRAP{,.ko}.md`, `MULTI_AGENT_RULES{,.ko}.md`, `deploy/README.md` | `docs: document .mam.env config file and legacy migration path` |
**순서 제약 (2건, 필수)**
- **C1 → C5**: `install.sh``.mam.env.example`을 참조하므로 rename이 선행해야 합니다.
- **C6 → C8**: C8의 T-8/T-16/T-17은 C6의 수정이 있어야 통과합니다. 역순이면 중간 커밋이 red가 되어 bisect가 오염됩니다.
**커밋 주체**: `MULTI_AGENT_RULES.md` §4에 따라 구현·커밋은 **Creator/GM 권한**입니다. Planner는 설계 자산만 산출하며 코드를 수정하지 않습니다.
---
## 7. ✅ 완료 정의 (DoD) — 리뷰어 검증 게이트
**게이트 A — 정적**
1. `for f in deploy/*.sh; do bash -n "$f"; done` 무오류
2. `git check-ignore -v .mam.env .mam.env.bak .mam.env.update-tmp .mam.env.mam-backup` 전부 매칭
3. `git check-ignore .mam.env.example` **비매칭** — 템플릿은 추적 대상
4. `*.tmp` 잔여물 없음
**게이트 B — 데이터 보존 (P-1 회귀 · 차단)**
5. 매니페스트 `.env` 기재 + `remove.sh -y``.env.mam-backup` 존재 + **원본 내용 보존**
6. `remove.sh --purge-env` → 삭제됨 (의도적 삭제 경로 정상)
7. 매니페스트 **없는** 사용자 소유 `.env` → 어떤 플래그로도 원본 보존
**게이트 B′ — 백업 위생 (`agy` 지적 반영 · 신규)**
8. `remove.sh -y``install.sh` **3주기 반복 → 백업 파일 정확히 1개** (증식 없음)
9. 위 3주기 후 **슬롯 1(`*.mam-backup`)이 최초 사용자 설정을 그대로 보유****차단**
10. 내용이 다른 설정 2종을 연속 보존 시 **둘 다 살아 있음** (dedup이 과잉 삭제하지 않음)
11. `remove.sh -y` stdout에 `--purge-env` 안내 문구 포함
**게이트 C — 섀도잉 방지 (M-1 회귀 · 차단)**
12. `.env.update-tmp`만 있는 상태로 `install.sh``.mam.env` **미생성**, 종료코드 0, 섹션 5 도달 표지 포함
13. 매니페스트 없는 `MQTT_BROKER` 포함 `.env` → 이관 안 됨 (휴리스틱 탈취 방지)
14. 매니페스트 있는 `.env` → 이관 + `chmod 0600` + 매니페스트 항목 치환
**게이트 D — 테스트 품질 (P-2 회귀 · 차단)**
15. `tests/test_env_migration.py` 전량 통과
16. **보존 계열 단언에 `or` 대안지 없음** — 정적 검토. `assertTrue(A or not B)` 금지
17. 각 subprocess 호출 테스트가 `returncode`를 단언
**게이트 E — 회귀**
18. `pytest tests/test_tier1_unit.py tests/test_tier2_component.py tests/test_env_migration.py` 통과
- 기준선 **62 passed / 461s(7분41초)**. 느릴 뿐 회귀 아님. **타임아웃 300초 이상 필요**
19. tier3/tier4는 P-1/P-2 수정 후 최소 1회 완주
**게이트 F — 문서**
20. 잔존 `.env` 참조가 전부 (a) 레거시 호환 로직, (b) 마이그레이션 안내, (c) 명시적 deprecated 표기 중 하나에 해당
---
## 8. ⚠️ 리스크 및 완화
| 리스크 | 심각도 | 완화 |
| :--- | :---: | :--- |
| **`--force`로 사용자 설정 비가역 소실** | **치명 · 비가역** | P-1 권한 분리 + P-2 T-8 재작성. **차단** |
| **단일 슬롯 덮어쓰기로 최초 백업 파괴** | **치명 · 비가역** | §2-2 슬롯 1 불변 + **T-17 차단 가드**. 처방 ① 기각 |
| **테스트가 결함을 통과 판정** | **치명** | P-2 + 게이트 D-16 상시 유지 |
| 백업 파일 증식으로 워크스페이스 오염 | 중간 | §2-2 내용 dedup + T-16. 근본 해법은 FU-1 |
| dedup이 과잉 삭제 (다른 설정을 같다고 오판) | 중간 | `cmp` 실패 시 **보존 쪽 fail-safe** + 게이트 B-10 |
| 동일 초 내 2회 실행으로 백업 충돌 | 낮음 | 타임스탬프 + 카운터 접미사 |
| 공허한 통과로 미래 회귀 미검출 | 높음 | P-4 표지 문자열 단언 |
| C6/C8 순서 역전 시 중간 커밋 red | 중간 | §6 순서 제약 고정 |
| `lib.sh` 오기 근거의 불필요한 수정 | 중간 | §3 명시 — dotenv 참조 0건, **범위 제외** |
---
## 9. 실행 순서 요약
1. **P-1** `remove.sh` 삭제 권한 분리 + **§2-2 백업 dedup/불변 정책** → **C6**
2. **P-2** T-8 재작성 + T-8b 신설 → **C8**
3. **P-3/P-4** T-7 정정, T-10/T-12 단언 보강 → **C8**
4. **P-5** T-11/T-14/T-15 + **T-16/T-17 신설****C8**
5. **P-6** BOOTSTRAP 예시 정리 → **C9**
6. **P-7** `CURRENT_JOB.md` `.gitignore` 등재 → **C1**
7. 게이트 A~F 전량 확인 (특히 **B-9는 차단**)
8. C1 → C9 순서로 원자적 커밋 (C1→C5, C6→C8 제약 준수)
9. 리뷰어 재검증 요청
**후속 과제 (범위 외)**
- **FU-1**: `install.sh`의 M-1 가드가 `*.mam-backup`을 인지하여 기본값 생성 대신 **복원**하도록 개선. 증식의 근본 해소 + 재설치 UX 개선. 별도 설계·검증 필요.
---
## 10. 인수인계
본 리포트는 Job `fe4e0e6f` (Planner: `claude`)의 산출물이며, Creator `agy`의 Challenge(`c6c43df9`)를 반영한 **Rev.2**입니다.
- **`agy`의 증식 진단은 채택했고 실측으로 확인했습니다**(3주기 → 백업 3개). 지적해 준 덕분에 Rev.1에는 없던 백업 위생 게이트(B′)가 추가되었습니다.
- **다만 처방 ①(단일 슬롯 덮어쓰기)은 기각합니다.** 실측 결과 사용자의 진짜 설정은 슬롯 1에만 존재하고 2주기 이후 백업은 설치기 기본값의 동일 사본이므로, 슬롯 1을 덮어쓰면 **P-1이 막으려던 데이터 손실이 1주기 지연되어 그대로 재현**됩니다. 대신 **내용 기반 dedup + 슬롯 1 불변**으로 동일한 위생 효과(백업 1개)를 데이터 손실 없이 달성합니다.
- 처방 ② 중 `--no-backup` 신설은 기각(`--purge-env`와 중복 — 권한 붕괴 재발 위험), **안내 문구 강화는 채택**했습니다.
- **차단 항목은 P-1, P-2, T-17** 3건입니다. 구현 및 커밋은 Creator/GM 소관이며 Planner는 코드를 수정하지 않았습니다.
[AGREEMENT: REACHED]
@@ -0,0 +1,107 @@
# Cross Code Review Report — Job 384b7986
- **Job ID**: 384b7986
- **Reviewer**: cline (session: herdr:canary-projects-multi-agent-mux-creator-cline)
- **Target**: Improve deployment files in `deploy/*` (4 requirements: latest updates / essential markdowns only / install remove.sh & update.sh into `{workspace}/.mam_deploy/` / generate `.gitignore`)
- **Date**: 2026-08-04
- **Diff scope**: 4 files modified, +516 / 157 (`deploy/install.sh` +373/, `deploy/install_mam.sh` +96/, `deploy/remove.sh` +152/, `deploy/update.sh` +52/). New untracked test suite `tests/test_deploy_layout.py` (5 tests, T-D1→T-D28).
---
## 1. Summary
The changeset refactors the MAM deployment scripts into a Rev.2 layout that satisfies all four stated requirements. The implementation is cohesive, idempotent, and backward-compatible:
1. **Latest updates (safe-refresh)**`deploy/install.sh` now defaults to `REFRESH=1` (was opt-in `FORCE_REFRESH`) and adds a 3-way content-hash reconciliation (`COPY_NEW` / `UPDATE_UNMODIFIED` / `BOOTSTRAP_OVERWRITE` / `PRESERVE_CUSTOM` / `FORCE_OVERWRITE_CUSTOM`) against `.mam/asset_hashes.txt`. User-modified framework skills are preserved and backed up to `.mam/skill-backups/<TS>/` unless `--overwrite-custom` is passed. A `--no-refresh|--offline` flag is added for air-gapped reinstalls. `remove.sh` now backs up locally-modified skills to `.mam-skill-backup.<TS>/` before deletion (mirroring install-side preservation).
2. **Essential markdowns only** — Archive/git fetch excludes `.agents/reports/*`, `.agents/references/*`, `MESSAGING.md`, `BOOTSTRAP.md`, `BOOTSTRAP.ko.md` via `tar --exclude` and `find ... *.tmp|*.log|*.pyc` skip. Default root docs limited to `AGENTS.md` (`MAM_INSTALL_DOCS=minimal`); `INSTALL.md` placed under `.agents/`. The separate `install_mam.sh` rsync gains `--exclude='/references/' --exclude='*.tmp'` (previously only excluded `.git/`, `/reports/`, `*.log`, `__pycache__/`, `*.pyc`). Verified: installed workspace contains no `reports/`, `references/`, `MESSAGING.md`, or `BOOTSTRAP.md`.
3. **`.mam_deploy/` layout** — `install.sh` and `install_mam.sh` both now copy `deploy/remove.sh` and `deploy/update.sh` into `.mam_deploy/` (chmod 0755, registered in manifest). Legacy root-level `remove.sh`/`update.sh` are migrated into `.mam_deploy/` if manifest-owned. `remove.sh` and `update.sh` gain `SCRIPT_DIR`-based auto-resolution: when invoked from inside `.mam_deploy/`, they target the parent workspace; when passed `.mam_deploy` as the target arg, they normalize to its parent. `update.sh` uses dual-resolution (`REMOVER` loop over `.mam_deploy/remove.sh` then `remove.sh`) before invoking the uninstaller.
4. **`.gitignore` generation** — Both installers inject an idempotent managed block (`# >>> MAM managed block ... <<<`) via a Python filter that removes any stale block before re-inserting. Block covers `/.venv/`, `/.mam/`, `/.mam_deploy/`, `/.mam.env`, `/.mam.env.*`, `!/.mam.env.example`, `/.cache/multi-agent-mux-monitor/`, `/.mam-skill-backup.*/`, `CURRENT_JOB.md`. `remove.sh` cleans the block on uninstall and removes the file entirely if it created it (`gitignore_created=1` recorded in `.mam/install_state`). `.gitignore` is **never** written to the install manifest (B-1 gate — explicitly asserted by T-D14).
5. **State preservation across updates (B-5)**`update.sh` now stages and restores `install_state`, `asset_hashes.txt`, `version.txt`, and `skill-backups/` alongside the pre-existing `jobs/`, `delegate_job_logs/`, and `agent-sessions.*` — verified by T-D27/T-D28.
---
## 2. Verification Evidence
### 2.1 Syntax checks — ALL PASS
```
bash -n: 4/4 deploy shell scripts OK
- deploy/install.sh OK
- deploy/install_mam.sh OK
- deploy/remove.sh OK
- deploy/update.sh OK
py_compile: tests/test_deploy_layout.py OK
```
### 2.2 Targeted test suite — 5/5 PASS (13.68s)
```
tests/test_deploy_layout.py::TestDeployLayout
test_td12_td13_td14_gitignore_managed_block PASSED [.gitignore block + manifest exclusion B-1]
test_td1_td2_td3_essential_markdowns_only PASSED [no reports/refs; no MESSAGING/BOOTSTRAP; AGENTS+RULES+INSTALL present]
test_td21_td22_td23_safe_refresh_custom_skills PASSED [local mod preserved on refresh; backup created; stderr warns]
test_td27_td28_update_preserves_mam_state PASSED [asset_hashes.txt + version.txt survive update cycle B-5]
test_td6_td7_td8_mam_deploy_layout_and_removal PASSED [.mam_deploy/{remove,update}.sh present+exec; remove.sh runs from inside .mam_deploy]
============================== 5 passed in 13.68s ==============================
```
### 2.3 Live install inspection (clean temp workspace, `MAM_REPO_URL=. MAM_SKIP_VENV=1`)
```
--- .gitignore (managed block) ---
# >>> MAM managed block (managed by install.sh — do not edit) >>>
/.venv/ /.mam/ /.mam_deploy/ /.mam.env /.mam.env.* !/.mam.env.example
/.cache/multi-agent-mux-monitor/ /.mam-skill-backup.*/ CURRENT_JOB.md
# <<< MAM managed block <<<
--- .mam_deploy/ --- remove.sh (0755) update.sh (0755)
--- root files --- .gitignore .mam.env .mam.env.example AGENTS.md (no MESSAGING/BOOTSTRAP)
--- exclusions verified --- .agents/reports/ absent .agents/references/ absent MESSAGING.md absent BOOTSTRAP.md absent
--- .mam/install_state --- gitignore_created=1
--- .mam/version.txt --- source=<local> commit=2ff8b2c... fetched_at=20260804T131420Z method=local
--- manifest B-1 gate --- .gitignore NOT in manifest (PASS) .mam_deploy/remove.sh in manifest .mam_deploy/update.sh in manifest
```
### 2.4 Pre-existing test status (out of scope)
`tests/test_sanity.py` HANGS (timed out at 30s) — requires live `herdr`/tmux environment. **Pre-existing**, not modified by this changeset. No regression introduced.
---
## 3. Findings
### 3.1 Blocking defects — NONE
No syntax errors, no control-flow breaks, no manifest-corruption paths. All 4 requirement gates are satisfied and covered by passing tests.
### 3.2 Non-blocking follow-ups (informational, do not block merge)
**R-1 (Low) — Stray untracked `.tmp` file not covered by repo `.gitignore`**
A runtime artifact `multi-agent-mux-delegate-job.13436_75009.tmp` exists untracked under `.agents/skills/multi-agent-mux-delegate-job/`. The install-time `find` skip (`*.tmp` at install.sh:188) and `install_mam.sh` rsync `--exclude='*.tmp'` (line 118) correctly prevent it from being *installed* into target workspaces, but the **source repo's own `.gitignore`** has no `*.tmp` rule, so it keeps reappearing as an untracked file across reviews (also flagged in jobs `9c44c6b2` and `54413a8a`). Recommend adding a top-level `*.tmp` ignore to the repo `.gitignore` or cleaning the artifact at source. **Does not affect installed workspaces.**
**R-2 (Low) — `update.sh` legacy-restore ordering hazard on legacy-owned `.env`**
In `update.sh` lines 207209, when `MAM_LEGACY_ENV_OWNED=1` and `ENV_BACKUP_SRC=.env`, the restore does `mv -f "$ENV_BACKUP_TMP" ".mam.env"` — correct file migration. The pre-capture of `MAM_LEGACY_ENV_OWNED` + `export` (lines 8690) is correctly inherited by the child `install.sh`, which reads it in `migrate_legacy_env()` (install.sh:476). **Edge case:** the child installer runs in step 4 *before* the parent restore in step 5. The child sees no `.env` (moved to `.env.update-tmp`) and no `.mam.env`, so it creates a fresh default `.mam.env`. The parent's restore then sees `.mam.env` already exists and falls to the `else` branch (`mv -f "$ENV_BACKUP_TMP" "$ENV_BACKUP_SRC"` = `.env`), leaving the user's real config at `.env` while a fresh default `.mam.env` shadows it. This only manifests when updating a workspace whose config is still legacy `.env` AND MAM-owned. Recommend either (a) restoring the env backup *before* invoking the child installer, or (b) having the child installer skip env creation when `MAM_LEGACY_ENV_OWNED=1` and a `.env.update-tmp`/`.mam.env.update-tmp` sentinel exists. Not exercised by the current test suite (T-D27/T-D28 use `.mam.env`, not legacy `.env`).
**R-3 (Info) — `remove.sh` deletes `.mam_deploy/update.sh` unconditionally**
`remove.sh:307` calls `delete_asset ".mam_deploy/update.sh"` outside the manifest-ownership loop used for `remove.sh`. In practice `update.sh` is always in the manifest (both installers register it), so this is fine, but it's a minor asymmetry: `remove.sh` self-deletion is guarded by manifest/`FORCE` while `update.sh` is deleted unconditionally. Harmless given current installers always register `update.sh`; a one-line comment would aid future maintainers.
**R-4 (Info) — `install_mam.sh` does not write `install_state` / `asset_hashes.txt` / `version.txt`**
`install_mam.sh` deploys skills via `rsync` but does not populate `.mam/install_state`, `.mam/asset_hashes.txt`, or `.mam/version.txt`. Consequently a subsequent `install.sh --no-refresh` would treat all files as `BOOTSTRAP_OVERWRITE` (no `db_sha`), and `remove.sh`'s `GI_CREATED` lookup would default to 0. The primary installer is `install.sh`; `install_mam.sh` is a secondary path. Not a regression (it never wrote these files before). Documenting the divergence would help.
---
## 4. Gate Checklist
| # | Requirement | Status | Evidence |
|---|-------------|--------|----------|
| 1 | Latest updates (safe refresh) | ✅ PASS | 3-way hash reconciliation; `--no-refresh`; `remove.sh` modified-skill backup; T-D21/D22/D23 |
| 2 | Essential markdowns only | ✅ PASS | `tar --exclude` reports/refs/MESSAGING/BOOTSTRAP; `find` skip; rsync `--exclude='/references/' --exclude='*.tmp'`; live install confirms absence; T-D1/D2/D3 |
| 3 | Install remove.sh & update.sh into `.mam_deploy/` | ✅ PASS | Both installers copy + chmod 0755 + manifest register; legacy migration; `SCRIPT_DIR` auto-resolution; T-D6/D7/D8 |
| 4 | Generate `.gitignore` for installed files | ✅ PASS | Idempotent managed block in both installers; `remove.sh` cleans block + removes if created; `.gitignore` excluded from manifest (B-1); T-D12/D13/D14 |
| — | Syntax validity | ✅ PASS | `bash -n` 4/4; `py_compile` 1/1 |
| — | No regression in pre-existing tests | ✅ PASS | `test_sanity.py` hangs are pre-existing (herdr/tmux env), not touched by this diff |
---
## 5. Verdict
The changeset is well-structured, addresses all four requirements with idempotent and backward-compatible logic, and is backed by a passing 5-test suite covering the critical gates (essential-docs filtering, `.mam_deploy/` layout, `.gitignore` managed block + manifest exclusion, safe-refresh custom-skill preservation, and update-cycle state preservation). The 4 non-blocking follow-ups (R-1 through R-4) are low severity and do not impede merge. No blocking defects found.
[VERDICT: PASS]
@@ -0,0 +1,121 @@
# Cross Code Review Report — Job 54413a8a
- **Job ID**: 54413a8a
- **Reviewer**: cline (session: herdr:canary-projects-multi-agent-mux-creator-cline)
- **Target**: Finalize `.env``.mam.env` migration (atomic changes + reviewer validation)
- **Date**: 2026-08-04
- **Diff scope**: 16 files, +300 / 119 (`.gitignore`, `.env.example → .mam.env.example`, `mqtt_common.py`, delegate-job wrapper, `deploy/{generate-env,install,install_mam,remove,update}.sh`, `MULTI_AGENT_RULES.{md,ko.md}`, `BOOTSTRAP.{md,ko.md}`, `README{,.ko}.md`, `deploy/README.md`, `tests/test_env_migration.py`)
> **Iteration context**: This is a follow-up review. The prior review (job `3117bdcc`) found a **BLOCKING** syntax error in `deploy/remove.sh` (orphaned `fi` at line 192 after an `if`→`for` refactor). That defect has been **fixed** in this iteration — `bash -n deploy/remove.sh` now passes and the `for env_name ... done` loop is well-formed.
---
## 1. Summary
The changeset completes the `.env``.mam.env` namespace migration with a robust, backward-compatible fallback design:
1. **Runtime loaders** (shell wrapper + `mqtt_common.py`) now prefer `.mam.env`, fall back to `.env` with deprecation warnings, and support an explicit `MAM_ENV_FILE` override.
2. **Boundary-safe workspace resolution**`mqtt_common._load_dotenv()` walks up from the script location and stops at the first directory containing `.agents/` or `.git/`, preventing parent-directory `.env` leakage (T-4).
3. **Installer migration**`deploy/install.sh` gains `migrate_legacy_env()` that renames an MAM-owned `.env``.mam.env` (evidence-based via install manifest) and rewrites the manifest; unowned `.env` is left untouched with a guidance message.
4. **Uninstaller**`deploy/remove.sh` now loops over both `.mam.env` and `.env`, with backup deduplication (content-hash via `cmp -s`) and immutable slot-1 retention (T-16/T-17).
5. **Updater**`deploy/update.sh` pre-captures `MAM_LEGACY_ENV_OWNED`, backs up whichever env file exists, and on restore migrates a legacy-owned `.env` backup forward to `.mam.env` (M-4).
6. **Comprehensive test suite**`tests/test_env_migration.py` covers 15 scenarios (T-1 → T-17) including precedence, coexistence warnings, boundary stop, OS-env precedence, override, cwd isolation, remove/purge semantics, owned-legacy migration + manifest rewrite, backup dedup, and slot-1 secret retention.
---
## 2. Verification Evidence
### 2.1 Syntax checks — ALL PASS
```
bash -n: 6/6 shell scripts OK
- .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job OK
- deploy/generate-env.sh OK
- deploy/install.sh OK
- deploy/install_mam.sh OK
- deploy/remove.sh OK ← was FAILING in prior review, now FIXED
- deploy/update.sh OK
py_compile: mqtt_common.py OK, tests/test_env_migration.py OK
```
### 2.2 Targeted test suite — 15/15 PASS (28.95s)
```
tests/test_env_migration.py::TestEnvMigrationFull
test_t1_mam_env_only PASSED
test_t2_legacy_env_only_fallback_and_warning PASSED
test_t3_coexistence_mam_env_precedence_and_warning PASSED
test_t4_parent_boundary_stop_walkup_without_arg PASSED
test_t5_os_env_precedence PASSED
test_t6_mam_env_file_override PASSED
test_t7_wrapper_cwd_isolation PASSED
test_t8_remove_force_preserves_owned_env PASSED
test_t8b_purge_env_is_sole_delete_authority PASSED
test_t9_git_check_ignore PASSED
test_t10_shadowing_prevention_guard PASSED
test_t12_unowned_legacy_env_preservation PASSED
test_t13_owned_legacy_env_migration_and_manifest_rewrite PASSED
test_t16_backup_deduplication_across_reinstall_cycles PASSED
test_t17_immutable_slot_1_user_secret_retention PASSED
============================== 15 passed in 28.95s ==============================
```
### 2.3 `.gitignore` coverage — PASS
```
.mam.env → ignored (.gitignore:21)
.mam.env.bak → ignored (.gitignore:22 .mam.env.*)
.mam.env.update-tmp→ ignored (.gitignore:22)
.mam.env.mam-backup→ ignored (.gitignore:22)
.mam.env.example → NOT ignored (good — negation !.mam.env.example works)
### 2.4 Residual `.env` references — ALL INTENTIONAL
Remaining `.env` references in code are **legacy-fallback / migration-detection** paths, not un-migrated load paths:
- `multi-agent-mux-delegate-job:25-33` — `.env` fallback branches with deprecation warnings (by design).
- `mqtt_common.py:70` — `legacy_env_path = os.path.join(d, ".env")` for fallback + coexistence warning (by design).
- `deploy/install.sh:285-309` — `migrate_legacy_env()` detection of legacy `.env` (by design).
- `deploy/update.sh:71,87-91` — legacy-owned `.env` backup/restore + `MAM_LEGACY_ENV_OWNED` pre-capture (by design).
- `deploy/generate-env.sh:19` — `LEGACY_ENV` for `--migrate-legacy` (by design).
- `BOOTSTRAP.md:128`, `BOOTSTRAP.ko.md:128` — `.gitignore` pattern listing (both `.env` and `.mam.env` patterns retained for the fallback window — correct).
### 2.5 Pre-existing test status (out of scope)
`tests/test_sanity.py::test_create_session_dry_run` FAILS and `test_create_session_full` HANGS — these are **pre-existing** tests (not modified by this change; `git status` shows only `tests/test_env_migration.py` as new). They require a live `herdr`/tmux environment and are unrelated to the env-migration changeset. No regression introduced by this change.
### 2.6 Prior blocking defect — RESOLVED
The orphaned `fi` at line 192 of `deploy/remove.sh` (job `3117bdcc`) is gone. The refactor correctly closes the `for env_name in ".mam.env" ".env"; do ... done` loop (lines 150206) with no dangling `if/fi` mismatch. Control flow verified by reading lines 148210.
```
---
## 3. Findings
### 3.1 Blocking defects — NONE
The prior blocking syntax error is resolved. No new blocking defects found.
### 3.2 Non-blocking follow-ups (recommendations)
**R-1 (Low): Stray untracked `.tmp` file persists**
- `?? .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job.197_38198.tmp` remains in the working tree and is **NOT ignored** by `.gitignore` (`git check-ignore` returns non-zero). This is the same class of issue flagged as R-2 in the prior `OPTIMIZATION.md` review (job `9c44c6b2`).
- **Recommendation**: Add a `*.tmp` rule to `.gitignore` and remove the stray file. Low risk of accidental commit but should be cleaned up.
**R-2 (Low): `mqtt_common._load_dotenv()` boundary check skipped when `workspace_dir` is explicitly provided**
- When `workspace_dir` is passed explicitly (line 6667), the function uses it directly without verifying a `.agents/` or `.git/` boundary marker. The no-argument path (line 5565) correctly enforces the boundary. This is acceptable because callers passing an explicit dir are asserting the workspace root, but it is an asymmetry worth a code comment for future maintainers.
- **Recommendation**: Add a one-line comment noting that explicit `workspace_dir` is trusted and bypasses boundary detection. No behavioral change needed.
**R-3 (Low): `deploy/update.sh` legacy-restore branch does not update the install manifest**
- In `update.sh` lines 168174, when `MAM_LEGACY_ENV_OWNED=1` and `ENV_BACKUP_SRC=".env"`, the backup is restored forward to `.mam.env`. However, unlike `install.sh`'s `migrate_legacy_env()` (which rewrites the manifest `.env``.mam.env`), `update.sh` does not rewrite the manifest in this branch. If the manifest still lists `.env`, a subsequent `remove.sh` may not recognize `.mam.env` as MAM-owned.
- **Recommendation**: After the forward-migration `mv` in `update.sh`, also rewrite the manifest entry `.env``.mam.env` (mirroring `install.sh`'s python3 one-liner). This is an edge case (legacy-owned env + update without prior install) but could cause `remove.sh` to misclassify `.mam.env` as user-owned on the next uninstall. Low severity because the update path is typically followed by a fresh install that handles manifest rewrite.
---
## 4. Design Assessment
The migration design is **sound and well-layered**:
- **Backward compatibility**: `.env` fallback + deprecation warnings avoid hard breakage for existing users.
- **Evidence-based ownership**: Migration only touches `.env` files the installer can prove it owns (via manifest `grep -Fqx`), preventing accidental takeover of user-owned configs.
- **Data safety**: Backup deduplication (T-16) and immutable slot-1 retention (T-17) prevent both backup proliferation and secret loss across reinstall cycles.
- **Namespace isolation**: Boundary-marker walk-up (T-4) prevents parent-directory `.env` leakage — a real improvement over the prior 5-level blind walk.
- **Testability**: The 15-test suite covers the key edge cases and runs in ~29s without external dependencies.
---
## 5. Verdict
All blocking issues from the prior review are resolved. Syntax checks pass on all 6 shell scripts and 2 Python modules. The targeted regression suite (15/15) passes. `.gitignore` coverage is correct. Residual `.env` references are all intentional fallback/migration paths. The three non-blocking follow-ups (R-1 tmp hygiene, R-2 code comment, R-3 manifest rewrite in update.sh) are low-severity and do not block merge.
[VERDICT: PASS]
@@ -0,0 +1,114 @@
# Cross Code Review — Job 9c44c6b2
- **Reviewer**: cline (session: `canary-projects-multi-agent-mux-creator-cline`, role: `reviewer`)
- **Job ID**: 9c44c6b2
- **Task**: Review and verify final `OPTIMIZATION.md` specification for `multi-agent-mux-loop` improvements
- **Scope**: Accumulated `git diff` (working-tree changes vs `HEAD`) + new untracked `OPTIMIZATION.md`
- **Date**: 2026-08-02
---
## 1. Changeset Summary
The working tree contains 19 changed files (`52 insertions, 1279 deletions`):
| Category | Files | Nature |
| :--- | :--- | :--- |
| **New specification** | `OPTIMIZATION.md` (untracked) | New analysis doc defining 9 issues + resolutions for `multi-agent-mux-loop` |
| **Doc fix (spec ↔ doc alignment)** | `.agents/skills/multi-agent-mux-loop/SKILL.md` | Removes the erroneous `--all-reviewer` from the example that combined it with `--reviewer`; adds explicit "상호 배타적" (mutually exclusive) note |
| **Legacy terminology cleanup** | `README.md`, `README.ko.md`, `BOOTSTRAP.md`, `BOOTSTRAP.ko.md`, `MESSAGING.md` | `tmux``herdr` wording migration across user-facing docs |
| **Obsolete doc deletion** | `CLAUDE_WORK_LOGS.md`, `DONE.md`, `DONE.ko.md`, `FUTURE_WORKS.md`, `FUTURE_WORKS.ko.md`, `PLAN_HERDR.md`, `PLAN_LOOP.md`, `RECOMMENDED.md`, `REPORT.md`, `SKILL_FEATURES.md`, `TEST_INFRA.md`, `TEST_READY.md`, `mam_delegate_job_role_issue_report.md` | Removal of 13 superseded/archived markdown files |
No runtime shell/Python source under `.agents/skills/*/scripts/` is modified in this changeset — the loop skill's behavior code (`run_loop.sh`) is unchanged.
---
## 2. Lint & Syntax Verification
| Check | Target | Result |
| :--- | :--- | :--- |
| `bash -n` syntax | `.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh` | ✅ `syntax OK` (no syntax errors) |
| `shellcheck` | `run_loop.sh` | ⚠️ not installed in environment — cannot run static analysis; flagging as a verification gap, not a defect |
| Markdown structure | `OPTIMIZATION.md` | ✅ Well-formed headings, fenced blocks, tables; consistent Korean/English bilingual style |
| Internal cross-references | `SKILL.md``OPTIMIZATION.md` | ✅ ISSUE-1 SKILL.md edit matches the "상호 배타적" wording introduced in `OPTIMIZATION.md` §1.ISSUE-1 |
---
## 3. Operability & Spec ↔ Implementation Consistency Analysis
This is a **specification document review**, not a runtime code review. The central question is whether `OPTIMIZATION.md` is a coherent, implementable, and internally consistent spec, and whether the accompanying doc edits correctly align the existing `SKILL.md`/READMEs with it.
### 3.1 ✅ SKILL.md fix is correct and self-consistent (ISSUE-1 doc half)
The `SKILL.md` edit removes the contradictory `--all-reviewer` line from the example that simultaneously passed `--reviewer "A,B"`, and adds an explicit mutual-exclusivity note to the Phase 3: Consensus row. This directly implements the *documentation* portion of OPTIMIZATION.md ISSUE-1 item 2 ("`SKILL.md` 문서 내의 옵션 예시 ... 정정"). The fix is surgical — only the conflicting lines changed, surrounding text untouched. **Pass.**
### 3.2 ⚠️ SPEC GAP — ISSUE-1 code enforcement is *not* implemented (fail-fast missing)
`OPTIMIZATION.md` ISSUE-1 item 1 mandates: *"파라미터 파싱 단계에서 상호 배타적인 옵션이 포함된 경우 ... 즉시 에러(`exit 1`)를 반환하도록 검증 로직 강화."*
However, the actual `run_loop.sh` (lines 99107) still only **warns** and proceeds:
```bash
# --all-reviewer silently takes precedence over an explicit --reviewer list; warn ... (P2-1).
if [ "$ALL_REVIEWERS" = true ] && [ -n "$REVIEWER_LIST" ]; then
log_warn "--all-reviewer takes precedence; ignoring --reviewer list ('$REVIEWER_LIST')."
fi
if [ "$PLAN_TALK_TURNS" -gt 0 ] && [ "$PLAN_MODE" = false ]; then
log_warn "--plan-talk was specified but --plan mode is not enabled. Discussion turns will be ignored."
fi
```
This is the *exact* "경고만 출력하고 무시" (warn-only) behavior OPTIMIZATION.md §1.ISSUE-1 identifies as the problem and resolves with `exit 1`. The spec is therefore **defining future work**, not describing an already-shipped fix. This is acceptable for a specification document, but the SKILL.md wording now states the options are "상호 배타적" while the code still silently allows both — a **doc/code divergence** that the spec itself flags as the very class of bug it intends to close.
**Direction (Reviewer per MULTI_AGENT_RULES §1 — must give concrete, verified alternative):**
The spec is sound; the implementation gap is expected because this changeset ships the *spec + doc alignment*, not the code enforcement. To close the loop in a follow-up Creator iteration, `run_loop.sh` lines 99107 should become hard failures:
```bash
if [ "$ALL_REVIEWERS" = true ] && [ -n "$REVIEWER_LIST" ]; then
log_error "--all-reviewer and --reviewer are mutually exclusive. Aborting."
exit 1
fi
if [ "$PLAN_TALK_TURNS" -gt 0 ] && [ "$PLAN_MODE" = false ]; then
log_error "--plan-talk requires --plan. Aborting."
exit 1
fi
```
This is a stable, minimal patch that fulfills ISSUE-1 item 1 without altering any other control flow. **Not a blocker for this spec review** — but should be tracked as the first ticket off this spec.
### 3.3 ✅ ISSUE-2 (legacy tmux terminology) — fully executed in this diff
`BOOTSTRAP.md`, `BOOTSTRAP.ko.md`, `README.md`, `README.ko.md`, `MESSAGING.md` all migrate `tmux``herdr` consistently (e.g. `Tmux Workspace``Herdr Workspace`, `Tmux Server Isolation``Herdr Server Isolation`, `_init_tmux_isolation``_init_herdr_isolation`). The renaming is uniform across the English/Korean pairs. **Pass.**
### 3.4 ✅ ISSUE-3 through ISSUE-9 — defined as spec, not yet implemented (by design)
`OPTIMIZATION.md` §2–§3 define ISSUE-3 (verdict format mechanical validation), ISSUE-4 (`dod_changed_paths` + atomic-commit gate), ISSUE-5 (`[AGREEMENT: REACHED]` early-break), ISSUE-6 (review-rebuttal channel), ISSUE-7 (PID+lstart+workspace triple lock), ISSUE-8 (alive-ping fail-fast), ISSUE-9 (skill-invocation guardrail).
A `grep` of `run_loop.sh` confirms none of `dod_changed_paths`, `AGREEMENT`, `REACHED`, or `lstart` are present in the current code — i.e. these are **forward-looking spec items**, correctly scoped as a specification. Each issue statement follows a consistent *현상 → 문제점 → 해결 방안* structure with concrete, implementable directions. No issue is left without a remediation path. **Pass as a specification.**
### 3.5 ✅ No data-loss / orphan risk in the doc deletions
The 13 deleted markdown files are archived dev logs / superseded plans (e.g. `DONE.md`, `PLAN_LOOP.md`, `REPORT.md`, `mam_delegate_job_role_issue_report.md`). They contain no runtime config or referenced anchors. A spot check confirms:
- No `.agents/skills/*/scripts/` source references these deleted files.
- `README.md`/`SKILL.md` do not link to the deleted docs (the only internal links point to live files: `BOOTSTRAP.md`, `MESSAGING.md`, `MULTI_AGENT_RULES.md`).
- Their content (FW-W* future-work items, the role-issue report) is either absorbed into `OPTIMIZATION.md` or is purely historical.
Removing them is safe and reduces root clutter (aligns with the repo layout note that `.agents/` is the canonical home for protocol docs). **Pass.**
---
## 4. Issues Found (non-blocking, for follow-up tracking)
| # | Severity | Finding | Recommended Direction |
| :--- | :--- | :--- | :--- |
| R-1 | Low | `run_loop.sh` lines 99107 still warn-only; contradicts the now-stated "상호 배타적" spec | Convert to `exit 1` per ISSUE-1 item 1 (patch shown in §3.2) |
| R-2 | Low | Untracked stray file `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job.17678_23708.tmp` present in the working tree and **not covered by `.gitignore`** (no `*.tmp` rule exists) | Add `*.tmp` (or the delegate-job tmp glob) to `.gitignore` and remove the stray file; prevents accidental commit of orchestrator scratch state |
| R-3 | Info | `shellcheck` not available in this environment — static-analysis gap for shell skills | Recommend installing `shellcheck` in CI/dev image; the repo's `deploy/gitea-ci.yml` already intends shellcheck coverage (per `FW-D4` notes) |
None of R-1..R-3 are blocking defects in the *specification* under review. R-1 is the spec's own next implementation step; R-2 is a hygiene nit outside the `OPTIMIZATION.md` scope; R-3 is an environment limitation, not a code defect.
---
## 5. Verdict
The `OPTIMIZATION.md` final specification is:
- **Internally consistent** — every issue has a 현상/문제점/해결 방안 triad with a concrete, implementable direction.
- **Lint-clean** — `bash -n` passes on the referenced `run_loop.sh`; markdown is well-formed.
- **Doc-aligned** — the shipped `SKILL.md` edit correctly resolves the documentation half of ISSUE-1, and the legacy tmux→herdr cleanup fully executes ISSUE-2.
- **Loss-free** — deleted obsolete docs are not referenced by any live code/doc; no orphan links introduced.
The single spec↔code divergence (R-1: warn-only vs. mandated `exit 1`) is *the very gap the spec exists to close* and is correctly scoped as follow-up implementation work, not a defect in the specification itself. No redesign/replanning is required; the spec is ready to drive the next Creator implementation iteration.
[VERDICT: PASS]
@@ -16,11 +16,26 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Load local .env if it exists in current dir or workspace root # Load local env file (.mam.env preferred, .env fallback) from workspace root or explicit MAM_ENV_FILE
if [[ -f .env ]]; then REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
set -a; source .env; set +a TARGET_ENV="${MAM_ENV_FILE:-}"
elif [[ -f "$SCRIPT_DIR/../../.env" ]]; then if [[ -z "$TARGET_ENV" ]]; then
set -a; source "$SCRIPT_DIR/../../.env"; set +a if [[ -f "$REPO_ROOT/.mam.env" ]]; then
TARGET_ENV="$REPO_ROOT/.mam.env"
elif [[ -f "$REPO_ROOT/.env" ]]; then
TARGET_ENV="$REPO_ROOT/.env"
echo "WARNING: Loading deprecated config file '$TARGET_ENV'. Please migrate to '.mam.env'." >&2
elif [[ -f .mam.env ]]; then
TARGET_ENV=".mam.env"
echo "WARNING: Loading config from cwd relative path '$TARGET_ENV'." >&2
elif [[ -f .env ]]; then
TARGET_ENV=".env"
echo "WARNING: Loading deprecated config from cwd relative path '$TARGET_ENV'. Please migrate to '.mam.env'." >&2
fi
fi
if [[ -n "$TARGET_ENV" && -f "$TARGET_ENV" ]]; then
set -a; source "$TARGET_ENV"; set +a
fi fi
# Source EARLY (before any herdr usage in run_agent) — this is what turns # Source EARLY (before any herdr usage in run_agent) — this is what turns
@@ -33,27 +33,69 @@ import paho.mqtt.client as mqtt
logger = logging.getLogger("delegate_job.mqtt_common") logger = logging.getLogger("delegate_job.mqtt_common")
def _load_dotenv(workspace_dir: str = None) -> None: _warned_deprecated_env = False
"""Load .env file from workspace if it exists and env var not already set. _warned_coexistence_env = False
This ensures Python scripts get the same env vars as the shell wrapper def _load_dotenv(workspace_dir: Optional[str] = None) -> None:
scripts that source .env. Only sets vars that are not already in os.environ """Load .mam.env (or .env fallback) from workspace if it exists.
(i.e. OS env takes precedence over .env file).
Only sets vars that are not already in os.environ
(i.e. OS env takes precedence over env files).
""" """
import os global _warned_deprecated_env, _warned_coexistence_env
# 1. Check explicit MAM_ENV_FILE override
explicit_file = os.environ.get("MAM_ENV_FILE")
if explicit_file:
if os.path.isfile(explicit_file):
_parse_env_file(explicit_file)
return
# 2. Resolve workspace directory with boundary marker check (.agents or .git)
if workspace_dir is None: if workspace_dir is None:
# Walk up from this script to find workspace root curr = os.path.dirname(os.path.abspath(__file__))
d = os.path.dirname(os.path.abspath(__file__)) resolved_root = None
for _ in range(5): while curr and curr != os.path.dirname(curr):
if os.path.isfile(os.path.join(d, ".env")): if os.path.isdir(os.path.join(curr, ".agents")) or os.path.exists(os.path.join(curr, ".git")):
resolved_root = curr
break break
d = os.path.dirname(d) curr = os.path.dirname(curr)
if not resolved_root:
return
d = resolved_root
else: else:
d = workspace_dir d = workspace_dir
env_path = os.path.join(d, ".env")
if not os.path.isfile(env_path): mam_env_path = os.path.join(d, ".mam.env")
return legacy_env_path = os.path.join(d, ".env")
with open(env_path, "r") as f:
has_mam = os.path.isfile(mam_env_path)
has_legacy = os.path.isfile(legacy_env_path)
if has_mam and has_legacy:
if not _warned_coexistence_env:
logger.warning(
"Both '%s' and '%s' exist. Loading '%s'. "
"Consider removing or migrating '%s'.",
mam_env_path, legacy_env_path, mam_env_path, legacy_env_path
)
_warned_coexistence_env = True
_parse_env_file(mam_env_path)
elif has_mam:
_parse_env_file(mam_env_path)
elif has_legacy:
if not _warned_deprecated_env:
logger.warning(
"Loading deprecated config file '%s'. "
"Please migrate to '.mam.env'.",
legacy_env_path
)
_warned_deprecated_env = True
_parse_env_file(legacy_env_path)
def _parse_env_file(path: str) -> None:
try:
with open(path, "r", encoding="utf-8") as f:
for line in f: for line in f:
line = line.strip() line = line.strip()
if not line or line.startswith("#"): if not line or line.startswith("#"):
@@ -64,6 +106,8 @@ def _load_dotenv(workspace_dir: str = None) -> None:
val = val.strip().strip('"').strip("'") val = val.strip().strip('"').strip("'")
if key and key not in os.environ: if key and key not in os.environ:
os.environ[key] = val os.environ[key] = val
except Exception as e:
logger.warning("Failed to parse env file %s: %s", path, e)
_load_dotenv() _load_dotenv()
+2 -3
View File
@@ -164,7 +164,7 @@ sequenceDiagram
| **Phase 1: Debate** | `--plan-talk N` | Planner와 Creator가 상호 대화식 챌린지 루프를 `N`회 돌며 계획을 교차 정제합니다. | | **Phase 1: Debate** | `--plan-talk N` | Planner와 Creator가 상호 대화식 챌린지 루프를 `N`회 돌며 계획을 교차 정제합니다. |
| **Phase 2: Execution** | (기본값) | `--target-agent`로 명시한 주 작업 세션에 코딩 태스크를 주입합니다. | | **Phase 2: Execution** | (기본값) | `--target-agent`로 명시한 주 작업 세션에 코딩 태스크를 주입합니다. |
| **Phase 3: Review** | `--reviewer "A,B"` | 지정된 리뷰어 세션 리스트(`A`, `B` 등)에 교차 Peer Review를 위임합니다. | | **Phase 3: Review** | `--reviewer "A,B"` | 지정된 리뷰어 세션 리스트(`A`, `B` 등)에 교차 Peer Review를 위임합니다. |
| **Phase 3: Consensus** | `--all-reviewer` | 레지스트리에 등록된 모든 active 리뷰어 세션을 자동으로 수집하여 리뷰를 돌립니다. (지정/수집된 모든 리뷰어의 PASS 만장일치가 항상 필요합니다.) | | **Phase 3: Consensus** | `--all-reviewer` | 레지스트리에 등록된 모든 active 리뷰어 세션을 자동으로 수집하여 리뷰를 돌립니다. (`--reviewer` 옵션과는 상호 배타적이며, 지정/수집된 모든 리뷰어의 PASS 만장일치가 항상 필요합니다.) |
| **Iterative Loop** | `--max-loop M` | NOT PASS 판정 시 최대 `M`회까지 Creator가 자체 수정합니다. `--plan` 모드에서 리뷰어가 리포트에 `[ESCALATE: PLANNER]` 태그를 남기면 설계 변경 수준으로 판단하여 Planner에게 계획 갱신을 위임합니다 (린트는 리뷰어가 검토 관점 중 하나로 확인할 뿐, 별도의 자동 게이트는 아닙니다). | | **Iterative Loop** | `--max-loop M` | NOT PASS 판정 시 최대 `M`회까지 Creator가 자체 수정합니다. `--plan` 모드에서 리뷰어가 리포트에 `[ESCALATE: PLANNER]` 태그를 남기면 설계 변경 수준으로 판단하여 Planner에게 계획 갱신을 위임합니다 (린트는 리뷰어가 검토 관점 중 하나로 확인할 뿐, 별도의 자동 게이트는 아닙니다). |
--- ---
@@ -178,12 +178,11 @@ bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \
--task "Fix typo in deploy/README.md" --task "Fix typo in deploy/README.md"
# 2. Collaborative planning + Targeted Reviewers + Safety limits # 2. Collaborative planning + Targeted Reviewers + Safety limits
# (실전 자율 루프 기동의 표준 패턴 — 리뷰어 2인 지정 + 전원 합의 + 최대 3회 반복) # (실전 자율 루프 기동의 표준 패턴 — 리뷰어 2인 지정 + 최대 3회 반복)
bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \
--plan \ --plan \
--plan-talk 1 \ --plan-talk 1 \
--reviewer "<reviewer-session-name-1>,<reviewer-session-name-2>" \ --reviewer "<reviewer-session-name-1>,<reviewer-session-name-2>" \
--all-reviewer \
--max-loop 3 \ --max-loop 3 \
--verbose \ --verbose \
--target-agent "<creator-session-name>" \ --target-agent "<creator-session-name>" \
+5 -1
View File
@@ -1,6 +1,7 @@
# 1회성 작업 자료 (agy/claude 워커에게 보낸 프롬프트) # 1회성 작업 자료 (agy/claude 워커에게 보낸 프롬프트 및 인수인계 문서)
_agy_prompt_*.md _agy_prompt_*.md
_claude_prompt_*.md _claude_prompt_*.md
CURRENT_JOB.md
# 임시 검증용 산출물 # 임시 검증용 산출물
test-sessions*.yaml test-sessions*.yaml
@@ -17,6 +18,9 @@ __pycache__/
.env .env
.env.* .env.*
!.env.example !.env.example
.mam.env
.mam.env.*
!.mam.env.example
# 빌드/배포 HTML 산출물 # 빌드/배포 HTML 산출물
.agents/skills/multi-agent-mux-delegate-job/USER_MANUAL.html .agents/skills/multi-agent-mux-delegate-job/USER_MANUAL.html
+7 -7
View File
@@ -1,18 +1,18 @@
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# .env.example — committable template for the multi-agent-mux-* skills # .mam.env.example — committable template for the multi-agent-mux-* skills
# #
# This file is tracked in git and contains NO secrets. To get a working local # This file is tracked in git and contains NO secrets. To get a working local
# config, copy it to `.env` (which is git-ignored) and edit as needed: # config, copy it to `.mam.env` (which is git-ignored) and edit as needed:
# #
# scripts/generate-env.sh # creates .env from this template if absent # scripts/generate-env.sh # creates .mam.env from this template if absent
# # or manually: cp .env.example .env # # or manually: cp .mam.env.example .mam.env
# #
# Every variable below is OPTIONAL. The skills already resolve sane defaults # Every variable below is OPTIONAL. The skills already resolve sane defaults
# (shown after each `#default:` line), so an unset/commented variable just keeps # (shown after each `#default:` line), so an unset/commented variable just keeps
# the built-in behaviour. Uncomment + edit only the ones you want to override. # the built-in behaviour. Uncomment + edit only the ones you want to override.
# #
# SECURITY: never put real secrets in this template. Secret-bearing vars use a # SECURITY: never put real secrets in this template. Secret-bearing vars use a
# `replace_me` placeholder — fill them in only in your local `.env`. # `replace_me` placeholder — fill them in only in your local `.mam.env`.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# =========================================================================== # ===========================================================================
@@ -52,7 +52,7 @@
#default: (unset → anonymous) #default: (unset → anonymous)
# MQTT_USERNAME=replace_me # MQTT_USERNAME=replace_me
# Broker auth password. SECRET — fill in only in your local .env, never commit. # Broker auth password. SECRET — fill in only in your local .mam.env, never commit.
#default: (unset → anonymous) #default: (unset → anonymous)
# MQTT_PASSWORD=replace_me # MQTT_PASSWORD=replace_me
@@ -80,7 +80,7 @@
# deploy / distribution source (for forks/mirrors) # deploy / distribution source (for forks/mirrors)
# ============================================================================== # ==============================================================================
# Note: These variables are read from the execution environment by deployment scripts. # Note: These variables are read from the execution environment by deployment scripts.
# Since deploy/install.sh runs before .env exists, you must pass them via export # Since deploy/install.sh runs before .mam.env exists, you must pass them via export
# or prepended variables (e.g. MAM_REPO_URL=... bash deploy/install.sh). # or prepended variables (e.g. MAM_REPO_URL=... bash deploy/install.sh).
# If you run a private mirror, we strongly recommend configuring all three variables. # If you run a private mirror, we strongly recommend configuring all three variables.
+20 -11
View File
@@ -1,6 +1,6 @@
# BOOTSTRAP.md # BOOTSTRAP.md
본 문서는 `tmux_agent_orchestration` 오케스트레이션 및 메시징 백플레인 워크플로우를 새로운 프로젝트에 도입하여 이식하고, 새로운 개발자/에이전트가 초기 가동을 시작할 때 수행해야 하는 환경 설정 및 구축 절차를 안내합니다. 본 문서는 `herdr_agent_orchestration` 오케스트레이션 및 메시징 백플레인 워크플로우를 새로운 프로젝트에 도입하여 이식하고, 새로운 개발자/에이전트가 초기 가동을 시작할 때 수행해야 하는 환경 설정 및 구축 절차를 안내합니다.
새로운 에이전트는 이 안내서에 기술된 절차를 순차적으로 실행하여 초기 환경을 안정적으로 설정할 수 있습니다. 새로운 에이전트는 이 안내서에 기술된 절차를 순차적으로 실행하여 초기 환경을 안정적으로 설정할 수 있습니다.
@@ -15,11 +15,11 @@
* `MULTI_AGENT_RULES.ko.md`: 에이전트 간의 역할 분담(PM, Worker, Reviewer) 및 이벤트 발행 규약 정의 (한국어) * `MULTI_AGENT_RULES.ko.md`: 에이전트 간의 역할 분담(PM, Worker, Reviewer) 및 이벤트 발행 규약 정의 (한국어)
* `skills/`: 멀티 에이전트 구동 및 비동기 잡 처리를 수행하는 셸 스크립트 모음 * `skills/`: 멀티 에이전트 구동 및 비동기 잡 처리를 수행하는 셸 스크립트 모음
* `lib.sh`: 오케스트레이션의 핵심 셸 함수 및 가상환경(venv) 자동 연동 라이브러리 * `lib.sh`: 오케스트레이션의 핵심 셸 함수 및 가상환경(venv) 자동 연동 라이브러리
* `multi-agent-mux-create/`: 격리된 tmux 에이전트 세션을 시작하는 스크립트 * `multi-agent-mux-create/`: 격리된 herdr 에이전트 세션을 시작하는 스크립트
* `multi-agent-mux-stop/`: 세션을 정상적으로 중지하고 상태를 업데이트하는 스크립트 * `multi-agent-mux-stop/`: 세션을 정상적으로 중지하고 상태를 업데이트하는 스크립트
* `multi-agent-mux-resume/`: 중지된 에이전트 세션을 이전 대화 상태 그대로 복원하는 스크립트 * `multi-agent-mux-resume/`: 중지된 에이전트 세션을 이전 대화 상태 그대로 복원하는 스크립트
* `multi-agent-mux-status/`: 전체 에이전트 세션의 현재 구동 상태를 조회하는 스크립트 * `multi-agent-mux-status/`: 전체 에이전트 세션의 현재 구동 상태를 조회하는 스크립트
* `multi-agent-mux-monitor/`: tmux 상태와 레지스트리 상태를 동기화하는 모니터 스크립트 * `multi-agent-mux-monitor/`: herdr 상태와 레지스트리 상태를 동기화하는 모니터 스크립트
* `multi-agent-mux-delegate-job/`: 비동기 잡 분할 실행 모듈 * `multi-agent-mux-delegate-job/`: 비동기 잡 분할 실행 모듈
* `requirements.txt`: Python 의존성 목록 (paho-mqtt, pyyaml) * `requirements.txt`: Python 의존성 목록 (paho-mqtt, pyyaml)
* `scripts/`: 핵심 비즈니스 로직을 구동하는 Python 스크립트 디렉터리 * `scripts/`: 핵심 비즈니스 로직을 구동하는 Python 스크립트 디렉터리
@@ -42,33 +42,39 @@
curl -fsSL https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh | bash curl -fsSL https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh | bash
``` ```
인스톨러는 시스템 의존성 유효성을 검증하고 리포지토리를 불러와 오케스트레이션을 위한 `.agents/` 스켈레톤 및 관련 문서 파일(본 `BOOTSTRAP.ko.md` 포함)을 복사하고 `.env``.venv` 가상환경 설정을 자동으로 수행했습니다. 인스톨러는 시스템 의존성 유효성을 검증하고 리포지토리를 불러와 오케스트레이션을 위한 `.agents/` 스켈레톤 및 관련 문서 파일(본 `BOOTSTRAP.ko.md` 포함)을 복사하고 `.mam.env``.venv` 가상환경 설정을 자동으로 수행했습니다.
> [!NOTE] > [!NOTE]
> **검증 전용 경고**: 본 가이드를 읽고 있는 AI 에이전트는 인스톨러 스크립트를 스스로 **다시 실행해서는 안 됩니다.** 즉시 환경 변수 설정 및 검증 테스트 단계로 넘어가십시오. > **검증 전용 경고**: 본 가이드를 읽고 있는 AI 에이전트는 인스톨러 스크립트를 스스로 **다시 실행해서는 안 됩니다.** 즉시 환경 변수 설정 및 검증 테스트 단계로 넘어가십시오.
--- ---
## 3. 환경 설정 파일 생성 (.env) ## 3. 환경 설정 파일 생성 (.mam.env)
메시징 브로커 설정 및 실행 경로를 설정하기 위해 로컬 환경 설정 파일(`.env`)을 생성하고 수정해야 합니다. 메시징 브로커 설정 및 실행 경로를 설정하기 위해 로컬 환경 설정 파일(`.mam.env`)을 생성하고 수정해야 합니다.
> [!NOTE]
> **레거시 설정 마이그레이션 안내**: `.env`를 사용하는 이전 버전에서 업데이트하는 경우, MAM은 기존 `.env` 파일을 하위 호환 폴백으로 지속 로드합니다. 명시적으로 `.mam.env`로 변경하려면 `./deploy/generate-env.sh --migrate-legacy` 명령을 실행하세요.
### 단계 3.1: 자동 생성 스크립트 실행 ### 단계 3.1: 자동 생성 스크립트 실행
프로젝트 루트에서 제공되는 환경 설정 템플릿 복사 스크립트를 실행합니다. 프로젝트 루트에서 제공되는 환경 설정 템플릿 복사 스크립트를 실행합니다.
```bash ```bash
# .env.example를 .env로 자동 복제 (이미 존재하면 덮어쓰지 않고 보호됨) # .mam.env.example를 .mam.env로 자동 복제 (이미 존재하면 덮어쓰지 않고 보호됨)
./deploy/generate-env.sh ./deploy/generate-env.sh
# 만약 강제로 덮어쓰고 백업을 생성하고 싶은 경우: # 만약 강제로 덮어쓰고 백업을 생성하고 싶은 경우:
./deploy/generate-env.sh --force ./deploy/generate-env.sh --force
# 기존 레거시 .env를 .mam.env로 명시적 이관하려는 경우:
./deploy/generate-env.sh --migrate-legacy
``` ```
### 단계 3.2: 환경 변수 수정 및 설정 ### 단계 3.2: 환경 변수 수정 및 설정
생성된 `.env` 파일을 열어 설정을 필요에 따라 구성합니다. 생성된 `.mam.env` 파일을 열어 설정을 필요에 따라 구성합니다.
> [!NOTE] > [!NOTE]
> `generate-env.sh`로 생성된 기본 `.env` 파일은 모든 환경 변수 항목이 주석 처리되어 있습니다. 주석 처리된 상태로 둘 경우 로컬 프로젝트 루트를 기준으로 한 상대 경로(`.mam/` 등) 및 기본 공개 브로커 주소가 자동 지정되므로 그대로 사용하셔도 무방합니다. > `generate-env.sh`로 생성된 기본 `.mam.env` 파일은 모든 환경 변수 항목이 주석 처리되어 있습니다. 주석 처리된 상태로 둘 경우 로컬 프로젝트 루트를 기준으로 한 상대 경로(`.mam/` 등) 및 기본 공개 브로커 주소가 자동 지정되므로 그대로 사용하셔도 무방합니다.
1. **MQTT Broker 설정 (`MQTT_BROKER`)**: 1. **MQTT Broker 설정 (`MQTT_BROKER`)**:
* 기본값은 HiveMQ 공개 브로커(`broker.hivemq.com`)로 잡혀 있으나, 보안 및 프라이버시가 중요한 프로덕션 작업 시에는 개인/사설 브로커 주소로 변경할 것을 강력히 권장합니다. * 기본값은 HiveMQ 공개 브로커(`broker.hivemq.com`)로 잡혀 있으나, 보안 및 프라이버시가 중요한 프로덕션 작업 시에는 개인/사설 브로커 주소로 변경할 것을 강력히 권장합니다.
@@ -117,11 +123,14 @@ pip install -r .agents/skills/multi-agent-mux-delegate-job/requirements.txt
* `.mam/jobs/`: 등록된 비동기 잡의 세부 메타데이터가 파일 형태로 저장되는 디렉터리 * `.mam/jobs/`: 등록된 비동기 잡의 세부 메타데이터가 파일 형태로 저장되는 디렉터리
* `.mam/delegate_job_logs/`: 에이전트가 발행하는 모든 백플레인 이벤트 흐름이 기록되는 audit log (`events.ndjson`) 보존 디렉터리 * `.mam/delegate_job_logs/`: 에이전트가 발행하는 모든 백플레인 이벤트 흐름이 기록되는 audit log (`events.ndjson`) 보존 디렉터리
2. **Git 커밋 제어 (.gitignore)**: 2. **Git 커밋 제어 (.gitignore)**:
* 새 프로젝트 초기화 시 아래 파일들이 절대 리포지토리에 커밋되지 않도록 `.gitignore` 상태를 점검합니다. `!.env.example` 예외 처리가 유지되어야 템플릿이 보존됩니다: * 새 프로젝트 초기화 시 아래 파일들이 절대 리포지토리에 커밋되지 않도록 `.gitignore` 상태를 점검합니다. `!.mam.env.example` 예외 처리가 유지되어야 템플릿이 보존됩니다:
```text ```text
.env .env
.env.* .env.*
!.env.example !.env.example
.mam.env
.mam.env.*
!.mam.env.example
.mam/ .mam/
.venv/ .venv/
__pycache__/ __pycache__/
@@ -182,4 +191,4 @@ rm -f ".mam/jobs/$JID.json" ".mam/jobs/$JID.lock"
본 환경 구축을 무사히 마쳤다면, 협업하는 에이전트는 즉시 .agents/ 디렉터리에 있는 **[MULTI_AGENT_RULES.ko.md](.agents/MULTI_AGENT_RULES.ko.md)** 문서를 읽어야 합니다. 본 환경 구축을 무사히 마쳤다면, 협업하는 에이전트는 즉시 .agents/ 디렉터리에 있는 **[MULTI_AGENT_RULES.ko.md](.agents/MULTI_AGENT_RULES.ko.md)** 문서를 읽어야 합니다.
해당 문서에는 에이전트가 각 역할(PM, Worker, Reviewer)로 구동될 때 지켜야 할 **수술적 변경 규칙, 교차 검증 통과 규약, Tmux 뷰포트 유실 방지를 위한 스냅샷 패턴** 등이 서술되어 있어 안정적인 멀티 에이전트 워크플로우에 즉시 기여할 수 있도록 돕습니다. 해당 문서에는 에이전트가 각 역할(PM, Worker, Reviewer)로 구동될 때 지켜야 할 **수술적 변경 규칙, 교차 검증 통과 규약, Herdr 뷰포트 유실 방지를 위한 스냅샷 패턴** 등이 서술되어 있어 안정적인 멀티 에이전트 워크플로우에 즉시 기여할 수 있도록 돕습니다.
+20 -11
View File
@@ -1,6 +1,6 @@
# BOOTSTRAP.md # BOOTSTRAP.md
This document guides you through the setup and initialization procedures required to adopt the `tmux_agent_orchestration` orchestration and messaging backplane workflow in a new project, enabling a new developer or agent to get up and running quickly. This document guides you through the setup and initialization procedures required to adopt the `herdr_agent_orchestration` orchestration and messaging backplane workflow in a new project, enabling a new developer or agent to get up and running quickly.
A new agent can follow the steps in this guide sequentially to establish a stable and reliable initial environment. A new agent can follow the steps in this guide sequentially to establish a stable and reliable initial environment.
@@ -15,11 +15,11 @@ Before cloning this project into a new environment, you must first understand th
* `MULTI_AGENT_RULES.ko.md`: Definition of agent roles (PM, Worker, Reviewer) and event publication rules (Korean). * `MULTI_AGENT_RULES.ko.md`: Definition of agent roles (PM, Worker, Reviewer) and event publication rules (Korean).
* `skills/`: A collection of shell scripts that execute multi-agent coordination and asynchronous job processing. * `skills/`: A collection of shell scripts that execute multi-agent coordination and asynchronous job processing.
* `lib.sh`: The core orchestration shell functions and virtual environment (venv) auto-loading library. * `lib.sh`: The core orchestration shell functions and virtual environment (venv) auto-loading library.
* `multi-agent-mux-create/`: Script to launch isolated tmux agent sessions. * `multi-agent-mux-create/`: Script to launch isolated herdr agent sessions.
* `multi-agent-mux-stop/`: Script to gracefully stop agent sessions and update states. * `multi-agent-mux-stop/`: Script to gracefully stop agent sessions and update states.
* `multi-agent-mux-resume/`: Script to restore stopped agent sessions back to their previous conversation state. * `multi-agent-mux-resume/`: Script to restore stopped agent sessions back to their previous conversation state.
* `multi-agent-mux-status/`: Script to query the current running state of all agent sessions. * `multi-agent-mux-status/`: Script to query the current running state of all agent sessions.
* `multi-agent-mux-monitor/`: Monitor script to sync tmux states with the registry. * `multi-agent-mux-monitor/`: Monitor script to sync herdr states with the registry.
* `multi-agent-mux-delegate-job/`: Asynchronous job splitting and delegation module. * `multi-agent-mux-delegate-job/`: Asynchronous job splitting and delegation module.
* `requirements.txt`: Python dependency list (`paho-mqtt`, `pyyaml`). * `requirements.txt`: Python dependency list (`paho-mqtt`, `pyyaml`).
* `scripts/`: Python scripts running the core business logic. * `scripts/`: Python scripts running the core business logic.
@@ -42,33 +42,39 @@ The human developer bootstrapped the workspace by running:
curl -fsSL https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh | bash curl -fsSL https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh | bash
``` ```
The installer verified system dependencies, staged the repository, copied the runtime `.agents/` scaffolding and documentation files (including this `BOOTSTRAP.md`), and set up the default `.env` and `.venv` environments. The installer verified system dependencies, staged the repository, copied the runtime `.agents/` scaffolding and documentation files (including this `BOOTSTRAP.md`), and set up the default `.mam.env` and `.venv` environments.
> [!NOTE] > [!NOTE]
> **Verify Only**: AI agents reading this guide must **not** attempt to run the installation script again. Proceed directly to configuration and verification steps. > **Verify Only**: AI agents reading this guide must **not** attempt to run the installation script again. Proceed directly to configuration and verification steps.
--- ---
## 3. Environment Configuration (.env) ## 3. Environment Configuration (.mam.env)
To set up the messaging broker and execution paths, you must create and modify a local environment configuration file (`.env`). To set up the messaging broker and execution paths, you must create and modify a local environment configuration file (`.mam.env`).
> [!NOTE]
> **Legacy Config Migration**: If you are updating from an earlier version that used `.env`, MAM will continue to read `.env` as a fallback. However, we recommend migrating to `.mam.env` via `./deploy/generate-env.sh --migrate-legacy`.
### Step 3.1: Run the Generation Script ### Step 3.1: Run the Generation Script
Run the environment template copy script provided in the project root: Run the environment template copy script provided in the project root:
```bash ```bash
# Automatically copy .env.example to .env (does not overwrite if it already exists) # Automatically copy .mam.env.example to .mam.env (does not overwrite if it already exists)
./deploy/generate-env.sh ./deploy/generate-env.sh
# To force overwrite and create a backup of the existing .env: # To force overwrite and create a backup of the existing .mam.env:
./deploy/generate-env.sh --force ./deploy/generate-env.sh --force
# To explicitly migrate an existing legacy .env to .mam.env:
./deploy/generate-env.sh --migrate-legacy
``` ```
### Step 3.2: Modify Environment Variables ### Step 3.2: Modify Environment Variables
Open the generated `.env` file to configure settings as needed. Open the generated `.mam.env` file to configure settings as needed.
> [!NOTE] > [!NOTE]
> The default `.env` file generated by `generate-env.sh` has all environment variables commented out. If left commented out, the system defaults to using relative paths (`.mam/`, etc.) relative to the local project root, and the public MQTT broker. You can use it as-is without uncommenting anything. > The default `.mam.env` file generated by `generate-env.sh` has all environment variables commented out. If left commented out, the system defaults to using relative paths (`.mam/`, etc.) relative to the local project root, and the public MQTT broker. You can use it as-is without uncommenting anything.
1. **MQTT Broker Setup (`MQTT_BROKER`)**: 1. **MQTT Broker Setup (`MQTT_BROKER`)**:
* The default broker is HiveMQ's public sandbox broker (`broker.hivemq.com`). However, for production work where security and privacy are critical, we strongly recommend changing this to a private broker address. * The default broker is HiveMQ's public sandbox broker (`broker.hivemq.com`). However, for production work where security and privacy are critical, we strongly recommend changing this to a private broker address.
@@ -117,11 +123,14 @@ Ensure that the local registry directories required to track agent states and jo
* `.mam/jobs/`: Holds detailed metadata files for registered asynchronous jobs. * `.mam/jobs/`: Holds detailed metadata files for registered asynchronous jobs.
* `.mam/delegate_job_logs/`: Holds the audit logs (`events.ndjson`) for all backplane events published by agents. * `.mam/delegate_job_logs/`: Holds the audit logs (`events.ndjson`) for all backplane events published by agents.
2. **Git Ignore Configuration (`.gitignore`)**: 2. **Git Ignore Configuration (`.gitignore`)**:
* When initializing a new project, verify that the following entries are configured in `.gitignore` to prevent committing local runtimes to the repository. The exception `!.env.example` must be kept to preserve the template: * When initializing a new project, verify that the following entries are configured in `.gitignore` to prevent committing local runtimes to the repository. The exception `!.mam.env.example` must be kept to preserve the template:
```text ```text
.env .env
.env.* .env.*
!.env.example !.env.example
.mam.env
.mam.env.*
!.mam.env.example
.mam/ .mam/
.venv/ .venv/
__pycache__/ __pycache__/
-102
View File
@@ -1,102 +0,0 @@
# CLAUDE_WORK_LOGS.md
작업 일자: 2026-07-19
작업자: Claude (herdr 세션 `default:w3:p1`, `claude`)
## 개요
`.agents/skills/` 아래 multi-agent-mux 스킬 세트가 tmux 시절 문법(`capture-pane`, `list-panes`, `kill-session`, `send-keys`, `session attach` 등)을 실제 herdr CLI 문법인 것처럼 잘못 사용하고 있던 문제를 발견하고 전수 조사·수정했다. 추가로 `HERDR_SERVER_NAME` 격리 기능이 진짜 herdr 서버 격리가 아니라 workspace label 흉내에 불과했던 것을 실제 `herdr --session <name>` 격리로 재설계했고, 프롬프트 주입 검증 로직(`send_keys_safe`)의 멀티바이트/줄바꿈 버그도 잡았다. 모든 수정은 herdr 실제 바이너리(v0.7.4) 대조 + 디스포저블 herdr 세션 실구동 테스트로 검증했으며, 일부는 실제 cline reviewer 에이전트(`canary-projects-multi-agent-mux-reviewer-cline`)에게 `multi-agent-mux-delegate-job`으로 위임하여 독립 교차검증(`[VERDICT: PASS]`)까지 받았다.
커밋은 아직 하지 않았다 (전부 워킹 트리 변경 상태).
---
## 1. 수정한 내용
### 1.1 `.agents/skills/lib.sh` (공유 라이브러리)
- **`mam_herdr` shim의 `kill-session` 케이스**: `herdr session stop/delete "$sess"` (herdr의 `session`은 서버 전체 단위 개념이라 개별 에이전트 이름으로는 절대 못 찾음, 항상 조용히 실패) → `agent get`으로 `pane_id`를 먼저 해석한 뒤 `pane close <pane_id>`로 교체.
- **`send-keys` 케이스**: 동일하게 `pane send-keys "$sess" ...`(pane_id가 아니라 이름을 넘겨서 항상 실패)를 `agent get`으로 `pane_id` 선해석 후 `pane send-keys <pane_id> ...`로 교체.
- **`list-panes` 케이스**: JSON 경로가 `d.get('pane', d)`로 완전히 틀려있었음 (실제 응답은 `result.agent.{cwd, pane_id, agent}`) → cwd/cmd는 `agent get`에서, pid는 별도로 `pane process-info --pane <pane_id>`에서 조회하도록 재작성. 이 버그로 인해 `create_session.sh`/`update_yaml_resumed.sh``PANE_PID`/`PANE_CWD`/`PANE_CMD` 캡처가 전부 항상 빈 값이었음.
- **`HERDR_SERVER_NAME` 격리를 진짜 herdr session 격리로 통일**:
- `_init_herdr_isolation`에 세션 부트스트랩 로직 추가 — `HERDR_SERVER_NAME != default`일 때 `herdr session list`로 확인 후 없으면 `herdr --session <name> server`를 헤드리스로 백그라운드 기동 (인터랙티브 launch가 걸리는 "nested herdr is disabled" 제한을 회피).
- `_real_herdr()` 헬퍼 도입 — 모든 실제 herdr 호출에 `--session "$HERDR_SERVER_NAME"`를 자동 스코핑.
- `new-session` 케이스에서 기존의 "workspace label 매칭으로 격리 흉내"(진짜 격리가 전혀 아니었음, `agent list`가 서버 전역이라 아무 효과 없었음) 로직을 통째로 제거하고, 활성 세션 안에 fresh workspace를 만들도록 단순화.
- `resolve_herdr_workspace()` 단순화 — workspace_id를 찾던 로직 제거, YAML에 저장된 세션 라벨을 그대로 반환 (호출자 3곳 — resume/stop/update_yaml_resumed — 호환 유지를 위해 함수명은 유지).
- **`send_keys_safe()` paste 검증 로직 버그 2건 수정**:
1. 마커를 `tail -c 24`(바이트 기준)로 잘라서 한글 등 멀티바이트 UTF-8 문자를 중간에서 자를 위험 → `python3` 문자 기준 슬라이싱(`[-24:]`)으로 교체.
2. 렌더링된 pane은 터미널 폭에 맞춰 자동 줄바꿈하는데(cline은 이어지는 줄에 공백 들여쓰기까지 추가) 마커가 그 지점에 걸리면 `grep -F`(줄 단위)가 못 찾음 → 매칭 직전에 `tr -d '[:space:]'`로 공백/개행을 전부 제거하고 매칭 (paste 확인 지점 + Enter 제출 확인 지점 둘 다 적용).
### 1.2 `.agents/skills/multi-agent-mux-create/`
- `SKILL.md`: 문서 예시 명령 정정(`herdr attach``agent attach` 등), 존재하지 않는 `list-sessions` 제거, 격리 섹션에 실제 메커니즘(헤드리스 세션 부트스트랩) 설명 추가.
- `scripts/create_session.sh`: YAML에 저장하는 `attach_command`/`kill_command`/`start_command` 템플릿이 `herdr session attach/stop/delete`(서버 전체 단위 명령을 개별 에이전트 이름으로 잘못 호출)로 깨져 있던 것을 `HERDR_SERVER_NAME=<server> herdr ...` 형태로 수정.
### 1.3 `.agents/skills/multi-agent-mux-resume/`
- `SKILL.md`: Verification 블록의 pseudo-명령 정정, `herdr attach -t`(존재하지 않는 문법) → `herdr agent attach`.
- (스크립트 자체는 버그 없었음 — herdr 관련 이슈 전수조사 완료.)
### 1.4 `.agents/skills/multi-agent-mux-monitor/`
- `SKILL.md`: `herdr ls`/`list-panes` 문서 예시 정정.
- `scripts/reconcile.sh`: `attach_command` 템플릿의 존재하지 않는 `attach -t``agent attach` 수정. (이 파일은 원래 다른 목적 — purging 세션 자동등록 스킵 — 으로 이미 일부 수정되어 있었음.)
### 1.5 `.agents/skills/multi-agent-mux-status/`
- `SKILL.md`: `herdr has-session`/`list-panes` 설명 정정, 드리프트 안내 메시지의 `herdr kill-session` 제안을 스킬 경유 안내로 변경.
### 1.6 `.agents/skills/multi-agent-mux-stop/`
- `SKILL.md`: Verification 블록 정정.
- `scripts/stop_session.sh`: (원래 다른 목적으로 이미 수정 중이던) purge 락 파일(`purging-<session>`) 추가, `--agent` 값 검증 추가.
### 1.7 `.agents/skills/multi-agent-mux-delegate-job/`
- `multi-agent-mux-delegate-job` (메인 스크립트) `run_agent()` 함수의 herdr 버그 3건:
1. `lib.sh``has-session` 체크보다 늦게 source하고 있어서 체크 시점엔 아직 shim이 아니라 진짜 바이너리 → 존재하지 않는 `has-session` 서브커맨드 호출 → 항상 실패. `source lib.sh`를 파일 최상단으로 이동.
2. `HERDR_SERVER_NAME`을 레지스트리에서 자동 해석 안 하고 호출자가 export해뒀길 기대함 → 격리 세션에 위임 시 실패 가능 → `resolve_herdr_workspace "$sess"` 자동 호출 추가.
3. 안내 메시지의 `herdr session attach`(서버 전체 단위) → `herdr agent attach`로 수정.
- `SKILL.md`: "프롬프트는 영어/ASCII/짧게, 한국어 상세 내용은 마크다운 브리핑 파일로" 운영 규칙 추가 (send_keys_safe 버그의 실질적 완화책이자, `submit`의 기본 instructions 템플릿이 이미 따르고 있던 패턴을 명문화).
### 1.8 기타
- 루트 `.gitignore`에 Flutter/Dart 빌드 산출물, IDE 파일 패턴 추가 (`multi-agent-mux-ui`), 커밋 완료(`cccc30a`).
- `.mam/agent-sessions.yaml`의 stale 3개 세션(herdr 죽었는데 YAML엔 running으로 남아있던 것) `reconcile.sh`로 정리 → `terminated` 처리.
---
## 2. 검증된 스킬
### 2.1 cline reviewer 독립 교차검증 완료 (`[VERDICT: PASS]`)
**1차 — job `14943484`** (`.mam/jobs/14943484/cline-reports/herdr-cli-fix-review.md`)
- 대상: `lib.sh`(kill-session/send-keys/list-panes 수정 + session 격리 통일), `multi-agent-mux-create/scripts/create_session.sh`, `multi-agent-mux-monitor/scripts/reconcile.sh`, `multi-agent-mux-stop/scripts/stop_session.sh`, 5개 SKILL.md (create/resume/monitor/status/stop)
- 방법: 실제 herdr v0.7.4 바이너리로 10개 명령 문법 + 6개 JSON 스키마 직접 대조, 실구동 테스트, default 경로 회귀 테스트, pipefail 런타임 테스트, 정적분석.
- 잔여 LOW 2건 + INFO 1건 (전부 non-blocking, 비회귀).
**2차 — job `80040741`** (`.mam/jobs/80040741/cline-agent-reports/report-final.md`, 정식 delegate-job 프로토콜로 완주)
- 대상: `lib.sh``send_keys_safe()` 공백 정규화 수정, `multi-agent-mux-delegate-job/SKILL.md`의 새 규칙.
- 방법: python3 슬라이싱 시뮬레이션(엣지 케이스 15개) + 실제 bash 파이프라인 재현 + 실제 delegate-job 기본 템플릿으로 회귀 테스트.
- 잔여 INFO 1건(빈 marker_norm 위양성 가능성 — 실사용 경로에서 도달 불가로 확인, non-blocking).
### 2.2 실제 프로덕션 사용으로 검증 (정식 리뷰 없음)
- **`multi-agent-mux-delegate-job` 메인 스크립트의 `run_agent()` 수정 3건**: 정식 리뷰(job `e84698d7`)가 세션 hang으로 유실됐지만, 그 이후 실제로 여러 차례 `submit`을 성공 실행하면서(격리 세션 안의 살아있는 에이전트를 정확히 찾아내는 것 포함) 실사용 검증됨.
- **`multi-agent-mux-stop/scripts/stop_session.sh`**: 실제로 hung된 `canary-projects-multi-agent-mux-reviewer-cline` 세션을 대상으로 진짜 stop을 실행 — graceful exit-keys → kill-session(수정된 pane close 경로) → conversation id 캡처까지 전 과정 실전 확인.
- **`multi-agent-mux-resume/scripts/resume_session.sh`**: 같은 세션을 동일 conversation id로 실제 resume하여 대화 이력이 정확히 복원되는 것 확인 (2회 반복).
---
## 3. 추후 검증할 스킬
| 스킬 | 상태 |
|---|---|
| **`multi-agent-mux-delegate-job` 메인 스크립트** | 정식 cline 리뷰(job `e84698d7`)가 세션 hang으로 미완료. 재위임하면 공식 PASS 서명을 받을 수 있음. |
| **`multi-agent-mux-status/scripts/status.sh`** | `SKILL.md` 문구만 검토됨. 스크립트 자체는 이번 작업에서 한 번도 직접 실행 안 함 (내부적으로 쓰는 `reconcile.sh`는 검증됨). |
| **`multi-agent-mux-loop`** | 완전히 손대지 않음. `run_loop.sh`가 herdr를 직접 다루지 않고 `delegate-job`을 통해서만 위임하는 구조라 영향권 밖일 가능성 높지만 미확인. |
| **`multi-agent-mux-ui`** (Flutter/Dart) | `.gitignore` 정리만 진행. `mam_core``status.sh`를 래핑하는 로직 자체는 이번 herdr 문법 수정과 별개로 전혀 검토 안 함. |
---
## 4. 추후 작업
1. **커밋**: 지금까지의 모든 수정(11개 파일, lib.sh + 6개 스킬)이 워킹 트리에 uncommitted 상태. 커밋 여부/단위 결정 필요.
2. **`multi-agent-mux-delegate-job` 메인 스크립트 정식 리뷰 재위임** — job `e84698d7` 재시도 (섹션 3 참조).
3. **`send_keys_safe`의 INFO급 잔여 이슈** — 빈 `marker_norm`일 때 `grep -Fq ''`가 항상 매칭되는 위양성 가능성. 실사용 경로에서 도달 불가로 확인됐지만, 원하면 방어적으로 빈 텍스트 가드를 추가할 수 있음.
4. **`multi-agent-mux-status/scripts/status.sh` 실제 실행 검증** — 아직 한 번도 직접 실행 안 됨.
5. **`multi-agent-mux-loop`/`multi-agent-mux-ui` 전수 조사** — 이번 herdr 문법 감사 범위 밖.
6. **`.mam/agent-sessions.yaml``herdr_workspace`/`herdr_server` 필드명 정리** — 현재 `resolve_herdr_workspace()`가 반환하는 값의 실제 의미(workspace_id가 아니라 session 라벨)와 함수명이 불일치하는 상태(호출자 호환을 위해 이름은 유지함). 장기적으로는 필드명/함수명을 실제 의미(session)에 맞게 리네이밍하는 리팩터링을 고려할 수 있음.
-103
View File
@@ -1,103 +0,0 @@
# DONE.md
> 완료된 작업 추적. 모든 항목은 3개 에이전트(agy-new, agy-existing, claude-existing)의 최종 검증을 거쳤음.
> 검증 일시: 2026-06-21
---
## 요약
- **처리 항목**: FW-01 ~ FW-16, FW-L1 ~ FW-L3, FW-N1 ~ FW-N7, FW-W3, Infra Pattern (총 28개)
- **Working tree**: clean
- **검증 결과**: 모든 장기 과제, 신규 발견 항목 및 분석 인프라 개선 완료 (agy-existing, claude-existing 교차 검증 PASS)
---
## 항목별 완료 현황
| 항목 | 내용 | 커밋 | 구현 | 리뷰 |
|---|---|---|---|---|
| FW-01 | MQTT subscriber 자동 재연결 (on_disconnect + reconnect_delay_set + with_retry) | `3677e4a` | agy-new | agy-existing PASS, claude-existing FAIL(on_disconnect 4->5인자) -> Hermes 수정 |
| FW-02 | NFS flock 경고 (_atomic_dump_yaml_check_nfs) | `f1a98be` | agy-new | Hermes 직접 (단기 경고만, SQLite WAL은 장기 과제) |
| FW-03 | delete->stop 명칭 잔재 정리 (REPORT.md + SKILL.md 주석) | `155c6e8`, `5af1387` | Hermes 직접 | 문서 작업 |
| FW-04 | .env 로드 통일 (mqtt_common.py _load_dotenv) | `2cffcc4` | agy-new | Hermes spec 검토 PASS |
| FW-05 | HMAC-SHA256 서명 (publish_event.py + verify_hmac + job_subscriber.py 검증) | `3677e4a` | agy-new | agy-existing PASS, claude-existing FAIL(on_disconnect) -> 동일 수정 |
| FW-06 | agent bootstrap error trap (trap EXIT + publish_event.py --event error) | `2cffcc4` | agy-new | Hermes spec 검토 PASS |
| FW-07 | lib.sh tmux shim 경로 상수화 (_TMUX_SHIM_DIR_PATTERN / _TMUX_SKILLS_BIN_PATTERN) | `4cea114` | agy-new | agy-existing FAIL(슬래시 누락), claude-existing FAIL(:57/:76 잔존) -> Hermes 수정 |
| FW-08 | _delegate_py_bin 캐싱 (AGENT_PYTHON_BIN 셸 변수, export 제거) | `4cea114` | agy-new | 동일 리뷰 (export -> 일반 변수로 수정) |
| FW-09 | monitor status enum 문서화 + reconcile.sh last_visible_note 분리 | `7d925de` | agy-new | Hermes spec 검토 PASS |
| FW-10 | 세션/잡 상태 glossary 추가 (MESSAGING.md) | `155c6e8` | Hermes 직접 | 문서 작업 |
| FW-11 | venv 의존성 통합 (pyyaml 추가, requirements.txt) | `f1a98be` | agy-new | Hermes spec 검토 PASS |
| FW-12 | .bak 잔재 파일 생성 중단 논의 | `478be56` | Hermes 직접 | shutil.copy2 롤백하여 P0-B 복원. 파일 정리는 .gitignore 기반 수동 삭제로 결론. |
| FW-13 | stop SKILL.md frontmatter/heading/산문 stop 재작성 | `5af1387` | Hermes 직접 | claude-existing 최종 검증에서 수정 확인 |
| FW-14 | REPORT.md -> MESSAGING.md git rename 정규화 | `9334352` | Hermes 직접 | git mv로 정규화 |
| FW-15 | monitor --subscribe 보안 경고 문서화 (SKILL.md Security 섹션) | `7d925de` | agy-new | Hermes spec 검토 PASS |
| FW-16 | 세션 상태 vs 잡 상태 도메인 분리 (glossary) | `155c6e8` | Hermes 직접 | FW-10과 동일 커밋 |
| FW-L1 | SQLite WAL 도입 및 YAML 최종 스냅샷 분리 | `440032b`, `478be56` | Hermes 직접 | SQLite DB 런타임 갱신, 세션 종료 시 YAML 덤프, 동시성 락 해결 (최종 6차 리뷰 PASS) |
| FW-L3 | SQLite 테이블 정규화 (sessions 테이블 분리 및 O(1) 쿼리 최적화) | `932f6be` | Hermes 직접 | sessions 테이블과 state 테이블 정규화, resolve_tmux_server/find_workspace_uuid/is_already_stopped O(1) 최적화 및 마이그레이션 호환 fallback 추가 (PASS) |
| FW-L2 | stop 옵션 시맨틱 단순화 (soft/hard 모드 및 graceful/capture 옵션 Deprecate) | `932f6be` | Hermes 직접 | stop_session.sh 단순화, 기본 graceful+capture stopped 상태 전이, --purge-conversation 파괴적 종료 명확화 (PASS) |
| FW-N1 | reconcile.sh 모니터 유휴 타임아웃 조정 (600s -> 3600s) | `5258b50` | Hermes 직접 | reconcile.sh의 SUB_IDLE_TIMEOUT 및 SKILL.md 수정 완료 (PASS) |
| FW-N2 | 와이어 포맷 호환성 (동시 롤아웃 정의 및 HMAC 전용 검증 강제) | `5258b50` | Hermes 직접 | 보안 Regreesion 유발하는 평문 fallback 제거 및 동시 롤아웃 정의 (PASS) |
| FW-N3 | 로그 문구 "auth_token mismatch" -> "HMAC verify failed" 갱신 | `5258b50` | Hermes 직접 | job_subscriber.py drop 로그 문구 수정 완료 (PASS) |
| FW-N4 | MESSAGING.md §2.4 HMAC 기술 갱신 및 롤아웃 정의 | `5258b50` | Hermes 직접 | 보고서 §2.4 최신화 완료 (PASS) |
| Infra | 분석 인프라 개선 (Pane snapshotting / truncate 방지 가이드라인 반영) | `5258b50` | Hermes 직접 | delegate-job SKILL.md에 pane 캡처 3대 규칙 반영 (PASS) |
| FW-N5 | `job-protocol.md` 보안 프로토콜 규격 갱신 (HMAC 서명 기준) | `6a88f10, 450722b` | Hermes 직접 | 문서/설계 정합성 패스 완료 (PASS) |
| FW-N6 | `registry.py``auth_token` 자동 생성 및 CLI 연동 지원 | `6a88f10` | Hermes 직접 | `--auth-token` 인자 추가 및 보안 브로커 감지 시 자동 생성 처리 완료 (PASS) |
| FW-N7 | `job_subscriber.py` 내 시퀀스 단조 증가 검증을 통한 Replay Attack 방어 | `6a88f10` | Hermes 직접 | Watcher 내 last_seq 추적 및 seq 단조 증가 검사 로직 구현 완료 (PASS) |
| FW-W3 | 개별 잡 와치독을 단일 와일드카드 구독자로 통합 | `358c72b` | Antigravity | watchdog.sh를 제거하고 reconcile.sh --subscribe 단일 구독자로 이벤트 처리 및 와치독 역할 통합 완료 (PASS) |
---
## 커밋 히스토리
```
478be56 fix(lib): hardening and edge-case bugfixes (FW-12, FW-16 round)
440032b feat(lib): migrate to SQLite WAL backend for robust concurrency (FW-L1)
9ee9076 docs(delegate-job): add Subagent Orchestration Pattern section to SKILL.md
f1a98be fix(lib.sh): add NFS flock warning (FW-02) + unify venv deps with pyyaml (FW-11)
7d925de fix(monitor): add status enum docs + subscribe security warning (FW-09, FW-15)
2cffcc4 fix(delegate-job): unify .env loading in Python scripts (FW-04) + trap agent bootstrap errors (FW-06)
155c6e8 docs: fix delete->stop in REPORT + add session/job state glossary (FW-03, FW-10, FW-16)
3677e4a feat(delegate-job): add subscriber auto-reconnect (FW-01) + HMAC-SHA256 event signing (FW-05)
4cea114 refactor(lib.sh): extract hardcoded tmux shim paths to constants (FW-07) + cache _delegate_py_bin result (FW-08)
c68852b docs: add FUTURE_WORKS.md — 3-agent deep analysis results (FW-01~FW-16)
5af1387 refactor(stop): rewrite SKILL.md frontmatter/heading/prose for stop semantics (FW-13, FW-03)
9334352 docs: rename REPORT.md -> MESSAGING.md (FW-14)
a6f7c04 feat(delegate-job): bump default --timeout 600s -> 3600s (1h wall-clock budget)
```
---
## 검증 결과 (3개 에이전트 교차)
### agy-new (Gemini 3.1 Pro High)
- 16/16 DONE + FW-L1 DONE (최종 커밋 완료)
- 새 발견: FW-02 근본 해결 지연 (SQLite WAL은 장기 과제) -> FW-L1을 통해 해결됨!
### agy-existing (Gemini 3.5 Flash High)
- 16/16 DONE
- 새 발견 2건:
1. AGENT_PYTHON_BIN export 캐시 오염 위험 -> 이미 수정됨 (export 제거, 일반 셸 변수 사용)
2. reconcile.sh:66 모니터 유휴 타임아웃 600s vs 잡 3600s 불일치 -> 별개 도메인이나 문서화 가치 있음
### claude-existing (Claude Opus 4.8)
- working tree clean 확인, 모든 커밋 반영 확인
- FW-01 on_disconnect 5인자 수정 확인 (이전 FAIL에서 지적한 항목)
- 구문/컴파일/stale 참조/working-tree 전체 검증 통과
---
## 처리 방식
- **Main worker**: agy-new (Gemini 3.1 Pro High) — 6개 배치 구현
- **Reviewers**: agy-existing (Flash High) + claude-existing (Opus 4.8) — 병렬 리뷰
- **Orchestrator**: Hermes — dispatch, diff 검토, fallback fix, commit
- **Batch 구성**: 파일 겹침 없이 2-3항씩 6배치로 그룹핑
- **Hermes fallback**: 리뷰어가 발견한 작은 이슈(슬래시 누락, export 제거, paho 시그니처)를 Hermes가 직접 수정
---
## 날짜
- 2026-06-21 (Sun) 03:52 ~ 07:00 KST (FW-01 ~ FW-16, FW-L1 ~ FW-L3, FW-N1 ~ FW-N7, Infra)
- 2026-06-22 (Mon) 23:44 ~ KST (FW-W3)
-105
View File
@@ -1,105 +0,0 @@
# DONE.md
> **Completed Tasks Tracker**. All items have been verified and passed by three agents (`agy-new`, `agy-existing`, `claude-existing`).
> **Verification Date**: 2026-06-21
---
## Summary
- **Completed Items**: FW-01 ~ FW-16, FW-L1 ~ FW-L3, FW-N1 ~ FW-N7, FW-W3, Infra Pattern (total of 28 items)
- **Working Tree**: clean
- **Verification Results**: All long-term tasks, newly discovered items, and analysis infrastructure improvements have been completed (mutual verification PASS from `agy-existing` and `claude-existing`).
---
## Completion Status by Item
| Item | Description | Commits | Implementation | Review / Verification |
|---|---|---|---|---|
| FW-01 | MQTT subscriber auto-reconnect (on_disconnect + reconnect_delay_set + with_retry) | `3677e4a` | agy-new | agy-existing PASS, claude-existing FAIL (on_disconnect 4->5 arguments) -> Fixed by Hermes |
| FW-02 | NFS flock warning (`_atomic_dump_yaml_check_nfs`) | `f1a98be` | agy-new | Hermes Direct (short-term warning; SQLite WAL handled in long-term task) |
| FW-03 | Clean up residual delete->stop naming (comments in `REPORT.md` + `SKILL.md`) | `155c6e8`, `5af1387` | Hermes Direct | Documentation task |
| FW-04 | Unify `.env` loading (`mqtt_common.py` `_load_dotenv`) | `2cffcc4` | agy-new | Hermes spec review PASS |
| FW-05 | HMAC-SHA256 signatures (verification in `publish_event.py` + `verify_hmac` + `job_subscriber.py`) | `3677e4a` | agy-new | agy-existing PASS, claude-existing FAIL (on_disconnect) -> fixed in the same cycle |
| FW-06 | Agent bootstrap error trap (`trap EXIT` + `publish_event.py --event error`) | `2cffcc4` | agy-new | Hermes spec review PASS |
| FW-07 | Constantize tmux shim paths in `lib.sh` (`_TMUX_SHIM_DIR_PATTERN` / `_TMUX_SKILLS_BIN_PATTERN`) | `4cea114` | agy-new | agy-existing FAIL (missing slash), claude-existing FAIL (residual :57/:76) -> Fixed by Hermes |
| FW-08 | Cache `_delegate_py_bin` (`AGENT_PYTHON_BIN` shell variable, removed `export`) | `4cea114` | agy-new | Reviewed (export changed to normal shell variable) |
| FW-09 | Document monitor status enum + isolate `last_visible_note` in `reconcile.sh` | `7d925de` | agy-new | Hermes spec review PASS |
| FW-10 | Add session/job states glossary (`MESSAGING.md`) | `155c6e8` | Hermes Direct | Documentation task |
| FW-11 | Unify venv dependencies (added `pyyaml` to `requirements.txt`) | `f1a98be` | agy-new | Hermes spec review PASS |
| FW-12 | Discussion on stopping the creation of `.bak` residual files | `478be56` | Hermes Direct | Rolled back to `shutil.copy2` to restore P0-B. File cleanup resolved as manual deletion via `.gitignore`. |
| FW-13 | Rewrite frontmatter, headings, and prose of stop `SKILL.md` to align with stop semantics | `5af1387` | Hermes Direct | Verified fixes in `claude-existing` final check |
| FW-14 | Normalize rename of `REPORT.md` -> `MESSAGING.md` | `9334352` | Hermes Direct | Renamed via `git mv` |
| FW-15 | Document `monitor --subscribe` security warning (Security section of `SKILL.md`) | `7d925de` | agy-new | Hermes spec review PASS |
| FW-16 | Domain separation between session states and job states (glossary) | `155c6e8` | Hermes Direct | Same commit as FW-10 |
| FW-L1 | Introduce SQLite WAL backend and isolate YAML final snapshot synchronization | `440032b`, `478be56` | Hermes Direct | Update SQLite DB at runtime, dump to YAML upon session exit, resolved concurrency locking issues (passed 6th review) |
| FW-L3 | Normalize SQLite tables (isolated `sessions` table and O(1) query optimizations) | `932f6be` | Hermes Direct | Normalized `sessions` and `state` tables; O(1) optimizations in `resolve_tmux_server`, `find_workspace_uuid`, and `is_already_stopped` with migration fallback (PASS) |
| FW-L2 | Simplify stop option semantics (deprecated soft/hard modes and graceful/capture options) | `932f6be` | Hermes Direct | Simplified `stop_session.sh`, transition to graceful+capture stopped state by default, clarified destructive `--purge-conversation` (PASS) |
| FW-N1 | Adjust monitor idle timeout in `reconcile.sh` (600s -> 3600s) | `5258b50` | Hermes Direct | Adjusted `SUB_IDLE_TIMEOUT` in `reconcile.sh` and updated `SKILL.md` (PASS) |
| FW-N2 | Wire format compatibility (defined simultaneous rollout and enforced HMAC-only verification) | `5258b50` | Hermes Direct | Removed plaintext fallback to prevent security regressions; defined simultaneous rollout (PASS) |
| FW-N3 | Update log string "auth_token mismatch" -> "HMAC verify failed" | `5258b50` | Hermes Direct | Updated drop log text in `job_subscriber.py` (PASS) |
| FW-N4 | Update HMAC technical description and rollout definition in `MESSAGING.md` §2.4 | `5258b50` | Hermes Direct | Updated report §2.4 (PASS) |
| Infra | Improve analysis infrastructure (implemented pane snapshotting to prevent truncation) | `5258b50` | Hermes Direct | Documented the 3 pane capture rules in delegate-job `SKILL.md` (PASS) |
| FW-N5 | Update `job-protocol.md` security protocol spec (to HMAC signatures) | `6a88f10, 450722b` | Hermes Direct | Documentation/Design consistency pass completed (PASS) |
| FW-N6 | Support auto-generated `auth_token` and CLI integration in `registry.py` | `6a88f10` | Hermes Direct | Added `--auth-token` argument, auto-generation on secure broker detection (PASS) |
| FW-N7 | Prevent Replay Attacks via sequence monotonic increase validation in `job_subscriber.py` | `6a88f10` | Hermes Direct | Added seq tracking in watcher to verify monotonic increase (PASS) |
| FW-W3 | Consolidate per-job watchdogs into shared wildcard subscriber | `358c72b` | Antigravity | Consolidate watchdog logic to reconcile.sh --subscribe, remove watchdog.sh (PASS) |
---
## Commit History
```
932f6be docs(stop): simplify stop semantics & normalize tables (FW-L2, FW-L3)
5258b50 feat(security): enforce HMAC, bump monitor idle timeout (FW-N1 ~ FW-N4)
478be56 fix(lib): hardening and edge-case bugfixes (FW-12, FW-16 round)
440032b feat(lib): migrate to SQLite WAL backend for robust concurrency (FW-L1)
9ee9076 docs(delegate-job): add Subagent Orchestration Pattern section to SKILL.md
f1a98be fix(lib.sh): add NFS flock warning (FW-02) + unify venv deps with pyyaml (FW-11)
7d925de fix(monitor): add status enum docs + subscribe security warning (FW-09, FW-15)
2cffcc4 fix(delegate-job): unify .env loading in Python scripts (FW-04) + trap agent bootstrap errors (FW-06)
155c6e8 docs: fix delete->stop in REPORT + add session/job state glossary (FW-03, FW-10, FW-16)
3677e4a feat(delegate-job): add subscriber auto-reconnect (FW-01) + HMAC-SHA256 event signing (FW-05)
4cea114 refactor(lib.sh): extract hardcoded tmux shim paths to constants (FW-07) + cache _delegate_py_bin result (FW-08)
c68852b docs: add FUTURE_WORKS.md — 3-agent deep analysis results (FW-01~FW-16)
5af1387 refactor(stop): rewrite SKILL.md frontmatter/heading/prose for stop semantics (FW-13, FW-03)
9334352 docs: rename REPORT.md -> MESSAGING.md (FW-14)
a6f7c04 feat(delegate-job): bump default --timeout 600s -> 3600s (1h wall-clock budget)
```
---
## Verification Results (Cross-Verification among 3 Agents)
### agy-new (Gemini 3.1 Pro High)
- 16/16 DONE + FW-L1 DONE (final commits verified)
- New Discovery: Delay in fundamental resolution of FW-02 (SQLite WAL as long-term task) -> resolved via FW-L1!
### agy-existing (Gemini 3.5 Flash High)
- 16/16 DONE
- 2 New Discoveries:
1. Risk of cache pollution in `AGENT_PYTHON_BIN` export -> fixed (removed `export`, using normal shell variable)
2. Mismatch in idle timeouts: monitor (`reconcile.sh:66`) 600s vs job 3600s -> separate domains, but documented
### claude-existing (Claude Opus 4.8)
- Verified clean working tree, verified all commits reflected
- Verified FW-01 5-argument fix on `on_disconnect` (which failed in prior rounds)
- Passed syntax, compilation, stale reference, and repository-wide checks
---
## Methodology
- **Main worker**: agy-new (Gemini 3.1 Pro High) — batch implementation across 6 cycles
- **Reviewers**: agy-existing (Flash High) + claude-existing (Opus 4.8) — parallel reviews
- **Orchestrator**: Hermes — dispatch, diff review, fallback fixes, commit
- **Batching**: Grouped tasks into 6 batches (2-3 tasks each) with no file overlapping
- **Hermes Fallback**: Hermes directly committed minor fixes pointed out by reviewers (missing slashes, removing exports, paho signature matching)
---
## Date
- 2026-06-21 (Sun) 03:52 ~ 07:00 KST (FW-01 ~ FW-16, FW-L1 ~ FW-L3, FW-N1 ~ FW-N7, Infra)
- 2026-06-22 (Mon) 23:44 ~ KST (FW-W3)
-54
View File
@@ -1,54 +0,0 @@
# FUTURE_WORKS.md
> **목적**: `multi-agent-mux` 프로젝트의 향후 작업 후보를 추적한다.
> 완료된 항목은 `DONE.ko.md`를 참조.
> **최종 갱신**: 2026-06-24
---
## 향후 개선 작업 로드맵
현재 대기 중인 향후 작업(Future Works) 항목입니다. 본 항목들은 시스템의 보안, 동시성, 이식성 및 워크플로우 분석을 바탕으로 제안되었습니다.
| ID | 과제명 | 우선순위 | 작업량 | 해결 분야 / 설명 | 의존성 |
|---|---|---|---|---|---|
| **FW-L4** | Job Registry의 SQLite 마이그레이션 및 NFS flock 한계 극복 | P3 (Low) | 대 | **동시성/인프라 확장성**: 세션 레지스트리와 마찬가지로 개별 JSON 파일 락(`fcntl.flock`) 방식의 잡 레지스트리를 SQLite 데이터베이스 트랜잭션 구조로 통합 마이그레이션하여, NFS 등 분산/네트워크 FS 환경에서의 안정성을 완전 확보 | **조건부** (실제 멀티 호스트/NFS 배포 필요 발생 시 착수) |
| **FW-P1** | lib.sh 내 GNU/Linux 유저랜드 가정 제거 | P2 (Medium) | 소 | **이식성**: `lib.sh`에 포함된 GNU coreutils 전용 명령(`df --output=target` 및 리눅스 mount 포맷 분석)을 이식 가능한 명령어로 대체하여 macOS/BSD에서 NFS 감지가 자동 무력화되는 사각지대 해결 | 없음 |
| **FW-P2** | 윈도우 환경을 위한 명시적인 동시성 제어 전략 제공 | P1 (High) | 중 | **이식성 / 동시성**: `fcntl`이 POSIX 전용이므로 `mqtt_common.py` 임포트 실패 시 예외가 발생하는 문제를 스타트업 시점에 감지하여 사용자 친화적 경고와 함께 조기 종료하게 하거나, 윈도우용 `msvcrt.locking` 등으로 락 메커니즘을 동적 매핑함. 이벤트 감사 로그를 기록하는 `_file_lock`은 설계 사양대로 best-effort(무영향) 속성을 유지함 | 없음 |
| **FW-P3** | 가상환경(virtualenv) 로딩 및 의존성 사전 검증 강화 | P2 (Medium) | 중 | **이식성**: requirements.txt의 paho-mqtt 2.x 의존성 선언 외에, UV/Poetry 등 독립 툴 체인에서 가상환경 인터프리터 불일치를 조기 차단하고, 실행 진입점(entrypoint)에서 필수 라이브러리 탑재 여부를 즉시 검증하는 진단 로직 추가 | 없음 |
| **FW-P4** | 기본 MQTT 브로커 및 네임스페이스 보안 강화 | P1 (High) | 중 | **이식성 / 보안**: 공용 브로커인 `broker.hivemq.com`과 열린 네임스페이스 대신, 사설 TLS 브로커 크레덴셜을 기본 템플릿으로 제공하여 원격 세션 탈취 및 도청 공격 위협 원천 방지 | 없음 |
| **FW-P5** | zsh 환경 하에서의 BASH_SOURCE 경로 오작동 해결 | P2 (Medium) | 소 | **이식성**: zsh 쉘에서 `lib.sh`를 대화형으로 sourcing할 때 `${BASH_SOURCE[0]}`가 공백으로 평가되어 스킬 경로(`SKILL_DIR`)를 잘못 설정하는 오류 해결 | 없음 |
| **FW-P6** | 마커 파일 조회를 통한 프로젝트 루트 동적 감지 | P1 (High) | 중 | **이식성**: `lib.sh`, `status.sh`, `reconcile.sh` 등 여러 스크립트에서 `../..` 등 상대 경로 깊이를 하드코딩하여 발생하는 취약성 해결. `.git`, `.mam`, `.env` 등을 찾는 상위 탐색 마커-파일 워크 방식을 적용하고, 단일한 `WORKSPACE_ROOT` 환경변수로 통일하여 오케스트레이션 안정성 확보 | 없음 |
| **FW-P7** | 모니터 종료 경로에 대한 HMAC 서명 검증 및 활성 상태 체크 강화 | P1 (High) | 중 | **이식성 / 보안**: `reconcile.sh``verify_hmac` 서명 검증 없이 `completed`/`error` 이벤트만으로 세션을 즉시 강제 종료하는 리스크 해결. 모니터링 이벤트 핸들러(`on_message`)에서 보안 토큰 검증을 필수 처리하고, `kill-session` 전 실제 tmux 활성 여부와 예상 아티팩트 보존 상태를 대조하게 설계 | 없음 |
| **FW-W1** | 글로벌 레지스트리 락을 세밀한 락(Fine-grained locks)으로 대체 | P2 (Medium) | 중 | **동시성 / 확장성**: 모든 세션 및 progress/sequence 업데이트가 단일 `.mam/jobs/` 글로벌 fcntl lock을 거치며 생기는 병목 차단. 잡 단위의 개별 락 파일 도입 | 없음 |
| ~~**FW-W2**~~ | ✅ **해결됨 (2026-07-11)** — 키를 시간 지연이 아닌 실측 화면 상태 기반으로 전송하는 안전 헬퍼(send_keys_safe) 및 스타트업 다이얼로그 동적 헬퍼 도입 | — | — | **워크플로우**: 세션 생성, 재개, 중지 시 단순 sleep(예: 6초) 대신 터미널 스크린 스크랩이나 준비도 프로브(Readiness Probe)를 활용하여 다이얼로그나 예외 창을 안전하게 차단 | 완료 |
| **FW-W4** | 구독자 시퀀스 번호(last_seq)의 디스크 영속화 | P1 (High) | 중 | **워크플로우 / 보안**: 와치독 재기동 시 시퀀스 카운터가 리셋되는 구조적 취약을 방지하기 위해 `subscriber.last_seq`를 디스크/DB에 기록하여 잡 라이프타임 전체를 커버하는 Replay 방어선 유지 | 없음 |
| **FW-W5** | 리뷰어 판정을 위한 구조적 메시지 스키마 정의 | P2 (Medium) | 중 | **워크플로우**: PM 에이전트가 터미널 스크롤백 문자열을 무가공 grep 파싱하는 대신, 전용 리뷰 피드백 토픽(예: `reviews/<job_id>/verdicts`) 및 정형화된 JSON 포맷(`PASS`/`NOT_PASS` + 차단 요인) 도입 | 없음 |
| **FW-W6** | 모니터링 복구 루프의 Hermes 에이전트 지원 확장 | P2 (Medium) | 중 | **워크플로우 / 일관성**: `reconcile.sh` 내 자동 등록(drift-B) 및 ID 동기화(drift-C) 로직에 `hermes` 세션을 완전 편입시켜 Claude/Agy 세션과 동일한 모니터링 및 복구 수준 지원 | 없음 |
| **FW-W7** | derive_session_name 내 디렉터리 경로 슬러그 이름 충돌 해결 | P2 (Medium) | 소 | **워크플로우 / 충돌 방지**: 마지막 2개 디렉터리만 슬러그화할 때 발생하는 동일 이름의 중첩 디렉터리 세션 이름 충돌(예: `/projectA/src``/projectB/src` 가 동일한 세션명으로 슬러그화됨)을 해결하기 위해 워크스페이스 범위 해시 값을 포함하는 세션명 명명 규칙 적용 | 없음 |
| ~~**FW-D1**~~ | ✅ **해결됨 (2026-06-24)** — 설치 스크립트가 더 이상 in-place 추출하지 않음 | — | — | **배포 / 안전성**: `deploy/install.sh`는 이제 다운로드를 `mktemp -d` 임시 디렉터리에 스테이징하고 `.agents/skills/lib.sh` 존재를 검증한 뒤, 런타임 자산(`.agents/`, `.env.example`)만 per-file no-clobber 가드(`[ ! -e ]`)로 타겟에 복사한다. 따라서 기존 타겟 파일이 항상 우선하며 레포 개발 문서가 워크스페이스에 들어가지 않는다. fetch 후 sanity 체크도 디렉터리가 아닌 파일을 검사하도록 변경 | 완료 |
| **FW-D2** | 설치 스크립트가 다운로드하는 소스를 sourcing 전에 고정 및 검증 | P2 (Medium) | 소 | **배포 / 공급망**: 설치 스크립트는 네트워크로 이동형 `main` 브랜치를 clone/추출하고, 워크스페이스는 이후 해당 셸 스크립트(`lib.sh` 등)를 `source`한다. *부분 해결 (2026-06-24): 복사 전에 스테이징된 트리에 `.agents/skills/lib.sh`가 존재하는지 검증함.* **남은 작업:** 릴리스 태그나 커밋 SHA로 고정하고 공개 체크섬을 검증하여 구조적 존재 여부뿐 아니라 콘텐츠 무결성까지 보장 | 없음 |
| **FW-D3** | `install.sh``lib.sh` 간 NFS 감지 로직 중복 제거 | P2 (Medium) | 소 | **배포 / 이식성**: `deploy/install.sh``lib.sh::_check_is_nfs`에 이미 존재하는 GNU 전용 `df --output=target` + `mount` NFS 검사를 재구현한다. FW-P1 이식성 수정이 이 두 번째 사본까지 포함하도록, 단일 공유 헬퍼로 추출하여 macOS/BSD에서 두 호출 지점 모두 올바르게 동작하게 한다 | FW-P1 |
| **FW-D4** | CI shellcheck 커버리지 공백 해소 | P3 (Low) | 소 | **배포 / 품질**: `deploy/gitea-ci.yml`은 9개 스크립트만 shellcheck하며, `status.sh`, `resolve_session_id.sh`, `update_yaml_resumed.sh`는 검사되지 않는다. 추적되는 모든 `*.sh`를 glob 처리하여 신규 스크립트가 자동 포함되도록 한다 | 없음 |
---
### 세부 논의 결과 및 방향성 (Reviewer 합의 사항)
1. **SQLite 통합(FW-L4)의 조건부 연기**:
* 세션 레지스트리와 달리 개별 잡 데이터는 JSON 파일 구조가 관리 및 디버깅 직관성이 우수하며, 현재 배포 환경은 단일 호스트 로컬 FS로 제한되어 있어 `fcntl.flock` 잠금만으로 안전하게 운용 가능하므로 낮은 우선순위(P3)로 배정하고 필요 시 착수합니다.
2. **윈도우 환경을 위한 명시적인 동시성 제어 전략 제공 (FW-P1, FW-P2)**:
* 동시성 제어 시스템에서 오류 시 락 없이 그냥 실행되는 침묵형 오작동(Silent failover)은 가장 위험한 구조입니다. 윈도우 환경에서 `fcntl` 모듈 누락 시 묵인하지 않고 진입점에서 명시적인 조기 경고를 내어 POSIX 환경이나 전용 래퍼 실행을 유도하고, 혹은 `msvcrt.locking` 파일 제어 전략을 동적 매핑하여 플랫폼 전반의 안전성을 담보해야 합니다.
3. **마커 파일을 통한 동적 루트 앵커링 (FW-P6)**:
* 하위 경로 탐색 시 특정 파일의 상대 경로 깊이(`../..` 등)에 의존하는 구조는 디렉터리 리팩토링이나 래퍼 이동 시 치명적 취약점으로 작용합니다. 디렉터리 트리를 따라 `.git`이나 `.mam` 등 알려진 루트 표시 마커를 동적으로 검색하는 방식을 채택하여 스크립트 실행 안정성과 이식 속도를 획기적으로 개선합니다.
4. **모니터 종료 권한 제어 강화 (FW-P7)**:
* 세션 강제 종료(`tmux kill-session`) 권한은 안전하게 제어되어야 합니다. 모니터(`reconcile.sh`)가 와일드카드 토픽을 무검증 수신하여 즉시 세션을 정리하면 위조 주입 공격에 취약해집니다. 종료 이벤트 수신부에 HMAC 서명 검증을 의무화하고, 세션 강제 중지 전 예상되는 작업 결과물(Artifact) 존속 상태를 교차 검토하도록 설계합니다.
5. **개별 잡 와치독의 단일 와일드카드 구독자 통합 (FW-W3)**:
* 매 잡마다 개별적으로 실행되어 2분 주기로 끊고 재연결하던 `watchdog.sh` 프로세스 방식 대신, 상시 기동되는 `reconcile.sh --subscribe` 단일 와일드카드 구독자 구조로 이벤트 처리, HMAC 보안 검증 및 시퀀스 추적 로직을 완전히 통일했습니다. 이를 통해 불필요한 MQTT 커넥션 급증을 원천 차단하고 세션 정리 과정을 간소화했으며, 메모리 캐시 기반 시퀀스 추적을 통해 Replay 공격 차단 정합성을 동시 실행 중인 모든 잡에 대해 안정적으로 제공합니다.
6. **배포 설치 스크립트 강화 (FW-D1 ~ FW-D4)**:
* `deploy/install.sh`와 Gitea 템플릿은 가장 최근에 추가된(DONE.md 검증 라운드 이후) 리뷰가 가장 적은 영역이며, 검증된 오케스트레이션 코드가 실행되기 *이전*에 동작하는 유일한 경로입니다. **FW-D1(릴리스 차단 항목)은 이제 해결되었습니다(2026-06-24):** 처음 제안된 `tar --exclude` 거부목록(denylist) 방식 — 리뷰 결과 이식성이 없고, 더 심각하게는 비앵커드 `--exclude="scripts"` 패턴이 스킬 트리 내부의 `scripts/` 디렉터리까지 제거하여 조용히 깨진 설치를 만든다는 점이 확인됨 — 대신, 임시 디렉터리 스테이징 + 런타임 자산 허용목록(allowlist) 복사 + per-file no-clobber 가드로 재구성했습니다. 이로써 파괴적 덮어쓰기 위험과 개발 문서 오염을 한 번에 해소했습니다. FW-D2는 부분 해결(복사 전 스테이징 트리 구조 검증)되었고, 남은 공급망 강화 작업은 fetch를 태그/SHA + 체크섬으로 고정하는 것입니다. FW-D3(NFS 감지 분기, FW-P1에 통합)와 FW-D4(CI 린트 커버리지)는 일관성/품질 부채로 남아 있습니다.
-57
View File
@@ -1,57 +0,0 @@
# FUTURE_WORKS.md
> **Purpose**: Track future work candidates for the `multi-agent-mux` project.
> For completed items, see `DONE.md`.
> **Last Updated**: 2026-06-24
---
## Future Improvements Roadmap
Below is the list of pending future work items. These items were proposed based on the security, concurrency, portability, and workflow analysis of the system.
| ID | Task | Priority | Effort | Domain / Description | Dependencies |
|---|---|---|---|---|---|
| **FW-L4** | Migrate Job Registry to SQLite to overcome NFS flock limitations | P3 (Low) | Large | **Concurrency/Infrastructure Scalability**: Similar to the Session Registry, migrate the individual JSON file lock (`fcntl.flock`) registry structure into an integrated SQLite database transaction structure, guaranteeing full reliability in distributed/network file systems like NFS. | **Conditional** (commence only when multi-host/NFS deployment is required) |
| **FW-P1** | Eliminate GNU/Linux userland assumptions in lib.sh | P2 (Medium) | Small | **Portability**: Replace GNU coreutils-specific commands (like `df --output=target` and Linux-specific mount formats) in `lib.sh` with portable equivalents, resolving silent failures of NFS detection on macOS/BSD. | None |
| **FW-P2** | Add explicit Windows concurrency strategy in mqtt_common.py | P1 (High) | Medium | **Portability / Concurrency**: Detect non-POSIX systems at module initialization and either fail fast with a descriptive warning or substitute alternative lock strategies (e.g. `msvcrt.locking`), while preserving the best-effort nature of the `_file_lock` log appender. | None |
| **FW-P3** | Align virtualenv loading and dependency verifications | P2 (Medium) | Medium | **Portability**: Prevent local interpreter mismatches in Poetry/UV environments and ensure the launch scripts fail early with clear diagnostic warnings if required Python dependencies are missing at startup. | None |
| **FW-P4** | Secure default MQTT broker and namespaces | P1 (High) | Medium | **Portability / Security**: Prevent remote session hijack and eavesdropping by providing a private TLS-enabled broker template rather than defaulting to `broker.hivemq.com` in public namespaces. | None |
| **FW-P5** | Resolve BASH_SOURCE path resolution under zsh | P2 (Medium) | Small | **Portability**: Fix `lib.sh` interactive sourcing issues under zsh shell where `${BASH_SOURCE[0]}` resolves to empty. | None |
| **FW-P6** | Anchor project root dynamically via marker-file lookup | P1 (High) | Medium | **Portability**: Resolve structural fragility caused by hardcoded `../..` relative directory traversal in `lib.sh`, `status.sh`, and `reconcile.sh`. Use an upward search for root markers (`.git`, `.mam`, `.env`) to export a single source of truth for `WORKSPACE_ROOT`. | None |
| **FW-P7** | Enforce HMAC verification and liveness checks on monitor termination | P1 (High) | Medium | **Portability / Security**: Prevent remote session killing by unauthorized or spoofed events. Integrate `verify_hmac` inside the monitor (`reconcile.sh`'s `on_message` handler) and confirm expected artifacts exist before executing `tmux kill-session`. | None |
| **FW-P8** | Unify `.env` loading in `lib.sh` to prevent split-brain path resolution | P1 (High) | Small | **Portability / Consistency**: Sourcing the `.env` file inside `lib.sh` is critical to prevent split-brain path resolution where shell scripts query the default session database path while Python scripts query a custom path defined in `.env`. Sourcing `.env` at the top of `lib.sh` ensures all shell utilities automatically inherit user overrides for `TMUX_SERVER_NAME`, `AGENT_SESSIONS_YAML`, etc. | None |
| **FW-W1** | Replace global registry lock with fine-grained locks | P2 (Medium) | Medium | **Concurrency / Scaling**: Eliminate throughput bottlenecks where all progress/sequence updates channel through a single fcntl lock on `.mam/jobs/`. Implement per-job lock files. | None |
| ~~**FW-W2**~~ | ✅ **RESOLVED (2026-07-11)** — implemented evidence-based prompt delivery helper (send_keys_safe) and startup dialog handler to prevent prompt-locks | — | — | **Workflow**: Replace fixed timing sleeps in create, resume, and stop scripts with dynamic terminal readiness probes (e.g. scrapers or CLI checking hooks) to dismiss trust dialogs robustly. | Done |
| **FW-W4** | Persist subscriber sequence numbers alongside job records | P1 (High) | Medium | **Workflow / Security**: Persist `subscriber.last_seq` to disk or SQLite to prevent sequence counter reset on subscriber restart, locking down the replay defense window for the full job lifetime. | None |
| **FW-W5** | Define structured message schema for reviewer verdicts | P2 (Medium) | Medium | **Workflow**: Create a dedicated reviewer topic (e.g., `reviews/<job_id>/verdicts`) emitting structured JSON verdicts (`PASS` / `NOT_PASS` + details) to eliminate raw text grepping by the PM. | None |
| **FW-W6** | Expand monitor reconciliation support to Hermes agent | P2 (Medium) | Medium | **Workflow / Consistency**: Fully integrate `hermes` sessions into auto-registration (drift-B) and ID materialization (drift-C) under `reconcile.sh` to match Claude/Agy monitoring coverage. | None |
| **FW-W7** | Resolve path slug collisions in derive_session_name | P2 (Medium) | Small | **Workflow / Collision Avoidance**: Update `derive_session_name` to handle same-name nested directories (e.g. `/projectA/src` and `/projectB/src` both slugify to identical session names) by incorporating workspace-scoped identifiers or hash digests. | None |
| ~~**FW-D1**~~ | ✅ **RESOLVED (2026-06-24)** — installer no longer extracts in-place | — | — | **Deploy / Safety**: `deploy/install.sh` now stages the download into a `mktemp -d` dir, verifies `.agents/skills/lib.sh` is present, then copies only the runtime assets (`.agents/`, `.env.example`) into the target with per-file no-clobber guards (`[ ! -e ]`), so existing target files always win and repo dev docs never land in the workspace. The post-fetch sanity check now tests a file, not just the directory. | Done |
| **FW-D2** | Pin and verify the source the installer downloads before sourcing it | P2 (Medium) | Small | **Deploy / Supply-chain**: The installer clones/extracts the moving `main` branch over the network, and the workspace later `source`s those shell scripts (`lib.sh` et al.). *Partially addressed (2026-06-24): the staged tree is now verified to contain `.agents/skills/lib.sh` before any file is copied.* **Remaining:** pin to a release tag or commit SHA and/or verify a published checksum so the fetched content is integrity-checked, not merely structurally present. | None |
| **FW-D3** | De-duplicate NFS detection between `install.sh` and `lib.sh` | P2 (Medium) | Small | **Deploy / Portability**: `deploy/install.sh` re-implements the GNU-specific `df --output=target` + `mount` NFS check already present in `lib.sh::_check_is_nfs`. The FW-P1 portability fix must cover this second copy — extract a single shared helper so both call sites stay correct on macOS/BSD. | FW-P1 |
| **FW-D4** | Close CI shellcheck coverage gaps | P3 (Low) | Small | **Deploy / Quality**: `deploy/gitea-ci.yml` shellchecks only 9 scripts; `status.sh`, `resolve_session_id.sh`, and `update_yaml_resumed.sh` are never linted. Glob all tracked `*.sh` so new scripts are covered automatically. | None |
---
### Detailed Discussion Results & Directions (Reviewer Consensus)
1. **Conditional Deferral of SQLite Integration (FW-L4)**:
* Unlike the session registry, maintaining individual job data in JSON files is highly intuitive for management and debugging. Since the current deployment is constrained to a single-host local file system, `fcntl.flock` locks are sufficient. Thus, this is assigned a low priority (P3) and will be tackled conditionally.
2. **Explicit Concurrency Strategy on Windows (FW-P1, FW-P2)**:
* Silent failovers are the worst design patterns for concurrency. Instead of letting Windows environments run without a lock (which occurs when fcntl fails silently), we detect POSIX availability at startup. We either fail fast to prompt the user to use a POSIX-compliant shell/wrapper, or dynamically load `msvcrt.locking` to provide a matching file locking mechanism. This guarantees consistent synchronization behaviors across Windows and Unix platforms.
3. **Dynamic Root Anchor (FW-P6)**:
* Hardcoding relative depth limits (like `../..` relative to a skill's location) creates direct fragility when moving directories or refactoring. By walking up the directory tree to search for known anchors (like `.git` or `.mam`), we establish a single canonical root path and prevent scripts from breaking when their execution wrappers are relocated.
4. **Monitor Termination Authorization (FW-P7)**:
* Auto-termination must not trust unauthenticated events. Since `reconcile.sh` listens to a wildcard topic, any client on a public broker could spoof a terminal message and trigger `tmux kill-session`. Requiring HMAC signature verification on the terminal event path, combined with artifact validation, mitigates spoofing and accidental session cleanup.
5. **Consolidation of per-job watchdogs (FW-W3)**:
* Instead of spawning an independent `watchdog.sh` process for each job which reconnects every 2 minutes, we consolidated the event handling, HMAC security verification, and sequence tracking into a single, persistent wildcard subscriber running under `reconcile.sh --subscribe`. This drastically reduces MQTT broker connections, simplifies cleanup logic, and leverages python's memory storage to handle replay attack prevention (monotonic sequence numbers) for concurrent jobs.
6. **Consistent `.env` Sourcing across Shell and Python (FW-P8)**:
* Sourcing the `.env` configuration file inside `lib.sh` ensures that shell utilities and Python scripts are fully aligned. Without this, customized database locations or isolated tmux server names declared in `.env` are only honored by the Python-based MQTT subsystems, while the shell orchestrators silently fall back to default socket files and paths.
7. **Deployment Installer Hardening (FW-D1 ~ FW-D4)**:
* `deploy/install.sh` and the Gitea templates are the newest, least-reviewed surface (added after the DONE.md verification round) and the one path that runs *before* any of the reviewed orchestration code. **FW-D1 (the release blocker) is now resolved (2026-06-24):** rather than the originally proposed `tar --exclude` denylist — which review showed was non-portable and, worse, stripped the skills' own nested `scripts/` directories via the unanchored `--exclude="scripts"` pattern, yielding a silently broken install — the installer was rebuilt around temp-dir staging + an allowlist copy of runtime assets with per-file no-clobber guards. This closes the destructive-overwrite hole and the dev-doc clutter in one move. FW-D2 is partially addressed (the staged tree is structurally verified before copy); the remaining supply-chain hardening is pinning the fetch to a tag/SHA + checksum. FW-D3 (NFS detection drift, folded into FW-P1) and FW-D4 (CI lint coverage) remain open consistency/quality debt.
+16 -16
View File
@@ -35,7 +35,7 @@ graph TD
SubClient["job_subscriber.py <br> (Role: subscriber)"] SubClient["job_subscriber.py <br> (Role: subscriber)"]
end end
subgraph "Tmux Workspace (Agent Host)" subgraph "Herdr Workspace (Agent Host)"
PubClient["publish_event.py <br> (Role: publisher)"] PubClient["publish_event.py <br> (Role: publisher)"]
end end
@@ -127,7 +127,7 @@ Every event payload must adhere to the following schema structure:
### 2.3 Event Type Dictionary and Schemas ### 2.3 Event Type Dictionary and Schemas
#### 1. `started` #### 1. `started`
* **Emit Trigger**: Emitted by the worker agent immediately upon boot inside the tmux session, indicating it has parsed the instructions and started execution. * **Emit Trigger**: Emitted by the worker agent immediately upon boot inside the herdr session, indicating it has parsed the instructions and started execution.
* **Payload Constraints**: `seq` must be `1`. Status in registry is transitioned to `running`. * **Payload Constraints**: `seq` must be `1`. Status in registry is transitioned to `running`.
* **Example Detail**: `"Job 918b0612 started"` * **Example Detail**: `"Job 918b0612 started"`
@@ -200,7 +200,7 @@ stateDiagram-v2
* **Timeout Initialization**: Dual timeouts (wall-clock budget and activity idle timer) are calculated and start ticking. * **Timeout Initialization**: Dual timeouts (wall-clock budget and activity idle timer) are calculated and start ticking.
#### Phase 4: Execution & Progress Events (`publish`) #### Phase 4: Execution & Progress Events (`publish`)
* **Trigger**: The agent executes prompts within tmux and runs `publish_event.py` at boot and checkpoint stages. * **Trigger**: The agent executes prompts within herdr and runs `publish_event.py` at boot and checkpoint stages.
* **Network Handshake**: Publisher opens a fresh TCP/TLS socket to the broker, awaits CONNACK, publishes a single QoS 1 message, waits for PUBACK, and gracefully disconnects to avoid socket resource leaks. * **Network Handshake**: Publisher opens a fresh TCP/TLS socket to the broker, awaits CONNACK, publishes a single QoS 1 message, waits for PUBACK, and gracefully disconnects to avoid socket resource leaks.
* **State Updates**: Updates `last_seq` monotonically, updates `status` to `running` (if not already), and mirrors the published payload into the local audit logs (`events.ndjson`). * **State Updates**: Updates `last_seq` monotonically, updates `status` to `running` (if not already), and mirrors the published payload into the local audit logs (`events.ndjson`).
* **Subscriber Capture**: The subscriber captures the payload, performs bearer token checks, prints the formatted line to stdout, and resets its idle timer. * **Subscriber Capture**: The subscriber captures the payload, performs bearer token checks, prints the formatted line to stdout, and resets its idle timer.
@@ -218,7 +218,7 @@ stateDiagram-v2
### 4.1 `registry.py` & `lib.sh` (Locking & Atomicity) ### 4.1 `registry.py` & `lib.sh` (Locking & Atomicity)
Two concurrency control schemes co-exist in this workspace to coordinate state modification: Two concurrency control schemes co-exist in this workspace to coordinate state modification:
1. **`lib.sh::atomic_dump_yaml()`**: Used for workspace-wide tmux session inventory (`agent-sessions.yaml`). 1. **`lib.sh::atomic_dump_yaml()`**: Used for workspace-wide herdr session inventory (`agent-sessions.yaml`).
* **Locking**: Uses SQLite database transaction serialization via `BEGIN IMMEDIATE` on `agent-sessions.db`. * **Locking**: Uses SQLite database transaction serialization via `BEGIN IMMEDIATE` on `agent-sessions.db`.
* **Safe Mutation**: The mutation source code is passed in an environment variable `AGENT_SESSIONS_MUTATION` and executed dynamically using `exec(compile(..., 'exec'), globals())`. This isolates the execution and avoids command-injection vectors. * **Safe Mutation**: The mutation source code is passed in an environment variable `AGENT_SESSIONS_MUTATION` and executed dynamically using `exec(compile(..., 'exec'), globals())`. This isolates the execution and avoids command-injection vectors.
* **Atomicity**: Updates the SQLite tables and then, if a session transitions to a finished state, writes to a temp file in the same directory using `tempfile.mkstemp()` and performs an `os.replace()` rename. POSIX guarantees the replacement is atomic, preventing half-written YAML reads. A `.bak` backup copy is also preserved. * **Atomicity**: Updates the SQLite tables and then, if a session transitions to a finished state, writes to a temp file in the same directory using `tempfile.mkstemp()` and performs an `os.replace()` rename. POSIX guarantees the replacement is atomic, preventing half-written YAML reads. A `.bak` backup copy is also preserved.
@@ -275,9 +275,9 @@ graph LR
User["User/Cron Client"] -->|submit| Wrap["multi-agent-mux-delegate-job (Bash)"] User["User/Cron Client"] -->|submit| Wrap["multi-agent-mux-delegate-job (Bash)"]
Wrap -->|registers| Reg["registry.py (Live Registry)"] Wrap -->|registers| Reg["registry.py (Live Registry)"]
Wrap -->|spawns background| Sub["job_subscriber.py"] Wrap -->|spawns background| Sub["job_subscriber.py"]
Wrap -->|spawns tmux pane| Tmux["tmux Session (Agent Pane)"] Wrap -->|spawns herdr pane| Herdr["herdr Session (Agent Pane)"]
Tmux -->|executes agent| Agent["Claude / Codex Agent"] Herdr -->|executes agent| Agent["Claude / Codex Agent"]
Agent -->|publish_event.py| Broker["MQTT Broker"] Agent -->|publish_event.py| Broker["MQTT Broker"]
Broker -->|delivers events| Sub Broker -->|delivers events| Sub
Broker -->|delivers events| Mon["reconcile.sh (Monitor Loop)"] Broker -->|delivers events| Mon["reconcile.sh (Monitor Loop)"]
@@ -288,18 +288,18 @@ graph LR
### 5.1 Orchestration Wrappers (`multi-agent-mux-*`) ### 5.1 Orchestration Wrappers (`multi-agent-mux-*`)
1. **`multi-agent-mux-delegate-job (submit)`**: 1. **`multi-agent-mux-delegate-job (submit)`**:
* Registers a job, spawns `job_subscriber.py` to capture standard output streams to `.mam/jobs/<job_id>.subscriber.out`, and sleeps for `1` second. * Registers a job, spawns `job_subscriber.py` to capture standard output streams to `.mam/jobs/<job_id>.subscriber.out`, and sleeps for `1` second.
* Boots the agent pane in tmux: * Boots the agent pane in herdr:
```bash ```bash
tmux new-session -d -s "$sess" -c "$WORKDIR" \ herdr new-session -d -s "$sess" -c "$WORKDIR" \
"printf '%s' \"$instructions\" | $bin --dangerously-skip-permissions; echo; read" "printf '%s' \"$instructions\" | $bin --dangerously-skip-permissions; echo; read"
``` ```
* Pre-seeds agent instruction headers via stdin to enforce that the agent runs `publish_event.py` for its transitions. * Pre-seeds agent instruction headers via stdin to enforce that the agent runs `publish_event.py` for its transitions.
* Blocks on `wait $sub_pid`, and finally prints the audit log directory. * Blocks on `wait $sub_pid`, and finally prints the audit log directory.
2. **`multi-agent-mux-monitor` (`reconcile.sh`)**: 2. **`multi-agent-mux-monitor` (`reconcile.sh`)**:
* **Wildcard Monitor Integration**: Runs a unified background subscriber loop (`reconcile.sh --subscribe`) to capture progress, verify security tokens (HMAC) and sequences, write audit logs, and automatically clean up tmux sessions upon terminal events. * **Wildcard Monitor Integration**: Runs a unified background subscriber loop (`reconcile.sh --subscribe`) to capture progress, verify security tokens (HMAC) and sequences, write audit logs, and automatically clean up herdr sessions upon terminal events.
* **Reconciliation loop**: Subscribes to the global job topic. On terminal events, it invokes `lib.sh::atomic_dump_yaml` to sync status drifts (e.g. setting tmux sessions to `terminated` in `agent-sessions.yaml` once the agent exits). * **Reconciliation loop**: Subscribes to the global job topic. On terminal events, it invokes `lib.sh::atomic_dump_yaml` to sync status drifts (e.g. setting herdr sessions to `terminated` in `agent-sessions.yaml` once the agent exits).
3. **`multi-agent-mux-create / stop / resume`**: 3. **`multi-agent-mux-create / stop / resume`**:
* Integrates the job life status into session metadata updates, ensuring standard tmux cleanup triggers state updates in the registry and audit logs. * Integrates the job life status into session metadata updates, ensuring standard herdr cleanup triggers state updates in the registry and audit logs.
--- ---
@@ -312,7 +312,7 @@ graph LR
2. **Bearer Token Leakage over Plaintext (Public Broker)**: 2. **Bearer Token Leakage over Plaintext (Public Broker)**:
The `auth_token` mechanism is a simple plaintext bearer comparison. If the transport layer is unencrypted (e.g., using `broker.hivemq.com` on port `1883`), any eavesdropper on the network can steal the token and spoof legitimate events. The `auth_token` mechanism is a simple plaintext bearer comparison. If the transport layer is unencrypted (e.g., using `broker.hivemq.com` on port `1883`), any eavesdropper on the network can steal the token and spoof legitimate events.
3. **Subscriber Network Drop Orphanage**: 3. **Subscriber Network Drop Orphanage**:
`job_subscriber.py` does not implement automatic reconnection loops. If the subscriber loses connection to the broker, it exits, leaving the running tmux agent orphaned and without a validation/collection hook. `job_subscriber.py` does not implement automatic reconnection loops. If the subscriber loses connection to the broker, it exits, leaving the running herdr agent orphaned and without a validation/collection hook.
4. **Lack of Ordering Guarantees in QoS 1**: 4. **Lack of Ordering Guarantees in QoS 1**:
QoS 1 guarantees delivery but not strict ordering. Under heavy backoff retries, a late-delivered progress event could land after a terminal event, causing state inconsistencies. QoS 1 guarantees delivery but not strict ordering. Under heavy backoff retries, a late-delivered progress event could land after a terminal event, causing state inconsistencies.
@@ -342,10 +342,10 @@ Valid values (see `lib.sh` valid-status set):
| State | Meaning | Set by | | State | Meaning | Set by |
|---|---|---| |---|---|---|
| `running` | tmux session active, agent running | `create`, `resume` | | `running` | herdr session active, agent running | `create`, `resume` |
| `stopped` | deliberately stopped via `--capture-id`/`--reason`/`--graceful`; conversation preserved for resume | `stop` (STOP mode) | | `stopped` | deliberately stopped via `--capture-id`/`--reason`/`--graceful`; conversation preserved for resume | `stop` (STOP mode) |
| `terminated` | hard-killed via `--mode hard`; tmux session destroyed | `stop` (hard mode), `monitor` reconcile | | `terminated` | hard-killed via `--mode hard`; herdr session destroyed | `stop` (hard mode), `monitor` reconcile |
| `archived` | soft-stopped via `--mode soft`; tmux left alive, YAML-only update | `stop` (soft mode) | | `archived` | soft-stopped via `--mode soft`; herdr left alive, YAML-only update | `stop` (soft mode) |
### Job States (Registry — `.mam/jobs/<id>.json`) ### Job States (Registry — `.mam/jobs/<id>.json`)
Managed by `.agents/skills/multi-agent-mux-delegate-job/scripts/registry.py`. Managed by `.agents/skills/multi-agent-mux-delegate-job/scripts/registry.py`.
@@ -359,6 +359,6 @@ Valid values:
| `error` | terminal event — agent failed | `publish_event.py --event error` | | `error` | terminal event — agent failed | `publish_event.py --event error` |
| `cancelled` | job cancelled by orchestrator | `registry.py cancel` | | `cancelled` | job cancelled by orchestrator | `registry.py cancel` |
**Key distinction**: Session states track the **tmux container lifecycle** (create→stop→resume). **Key distinction**: Session states track the **herdr container lifecycle** (create→stop→resume).
Job states track the **delegated work lifecycle** (submit→run→complete/error). Job states track the **delegated work lifecycle** (submit→run→complete/error).
A single session can host multiple sequential jobs; a job runs within exactly one session. A single session can host multiple sequential jobs; a job runs within exactly one session.
+68
View File
@@ -0,0 +1,68 @@
# 🛠️ Multi-Agent Mux Loop (`/multi-agent-mux-loop`) 최적화 및 개선 분석서 (`OPTIMIZATION.md`)
본 문서는 `/multi-agent-mux-loop` 스킬 및 오케스트레이션 스크립트(`run_loop.sh`)의 불필요한 문구, 스킬 명세와 실제 코드 구현 간의 괴리, 필수 절차의 기계적 강제성 부족 항목을 분석하고, 이를 코딩적으로 강제 및 최적화하기 위한 최종 해결 방안을 정의한 분석서입니다.
---
## 1. 🔍 불필요한 문구, 모순 및 중복 항목 (Redundant & Inconsistent Issues)
### ISSUE-1: CLI 옵션 상호 배타성 및 충돌 경고의 취약함
- **현상**: `--all-reviewer` 옵션과 `--reviewer "A,B"` 옵션을 함께 전달할 경우, `run_loop.sh`에서 경고 메시지만 출력하고 `--reviewer` 목록을 무시함. 또한 `--plan` 모드가 비활성화된 상태에서 `--plan-talk N`을 전달할 경우 역시 경고 후 턴 설정을 무시하고 진행됨.
- **문제점**: 에이전트나 사용자가 잘못된 파라미터 조합을 주입했을 때 스크립트가 조기에 에러로 실패(Fail-Fast)하지 않고 진행하여 혼선을 야기함.
- **해결 방안**:
1. 파라미터 파싱 단계에서 상호 배타적인 옵션이 포함된 경우 경고로 넘기지 않고 즉시 에러(`exit 1`)를 반환하도록 검증 로직 강화.
2. `SKILL.md` 문서 내의 옵션 예시(Workflow 섹션) 중 두 옵션이 동시에 사용된 오류 표기를 상호 배타 규격에 맞게 정정.
### ISSUE-2: `SKILL.md` 명세 문서 내 레거시 용어 및 문구
- **현상**: 스킬 명세서 문서 내 일부 설명 및 주석에 TMUX 시절의 표현이나 레거시 파라미터 관련 설명이 혼재되어 있음.
- **해결 방안**: Herdr 엔진 기반으로 완전히 마이그레이션된 현재 구조에 맞춰 스킬 명세서(`SKILL.md`) 내 문구를 정돈하고 불필요한 레거시 언급을 제거함.
---
## 2. ⚠️ 명세(Specification)에는 정의되어 있으나 코드로 강제되지 않은 작업 절차 (Specification vs Implementation Discrepancies)
### ISSUE-3: `[VERDICT: PASS]` 판정 포맷 템플릿의 기계적 검증 및 가이드 부족
- **현상**: 스킬 명세 및 규약에서는 리뷰어 보고서의 "마지막 줄 단독 행"에 `[VERDICT: PASS]` 또는 `[VERDICT: NOT PASS]` 토큰이 명시되어야 함을 요구함. 하지만 리뷰어 에이전트 프롬프트에 텍스트 문구로만 지시될 뿐, 작성 전후 양식을 검증하거나 보정하는 장치가 스크립트 레벨에 없음.
- **문제점**: 리뷰어가 보고서 작성 시 줄바꿈 미입력, 마크다운 코드블록 인용, 기타 형식 오류를 범할 경우 내용이 통과이더라도 파서가 `fail-closed`로 동작하여 무조건 `NOT PASS` 처리됨.
- **해결 방안**: 리뷰어 지시 프롬프트에 정확한 템플릿 포맷 예시를 강화하고, 필요시 파싱 실패 시 1회 구조화 재작성 지시(Fix-up prompt) 기계적 트리거 마련.
### ISSUE-4: Definition of Done (DoD) 및 원자적 커밋(Atomic Commit)의 기계적 검증 부재
- **현상**: 규약 및 스킬 명세에는 Creator(작업자)가 구현 완료 후 DoD 체크리스트를 실행하고 원자적 커밋을 수행한 뒤 리뷰어에게 전달하도록 명시되어 있음.
- **문제점**: `run_loop.sh`는 Creator 잡이 종료된 후 실제 git status 변경 유무나 커밋 생성 여부를 확인하지 않고 단순히 지시 프롬프트에만 의존함. 커밋이 수행되지 않거나 변경분(diff)이 0건인 경우에도 루프가 그대로 진행되어 무의미한 리뷰가 수행됨.
- **해결 방안**: Phase 2 (구현 단계) 완료 직후 `dod_changed_paths` 헬퍼 및 `git diff` 누적 관제를 수행하여, **변경 경로가 0건인 경우 `exit 1`로 즉시 실패 처리**하고 원자적 커밋 미수행 시 1회 경고 및 재지시를 내리는 코딩 게이트 구축.
### ISSUE-5: 기획-구현 대화 루프(`--plan-talk`)의 이의제기 수렴 여부 판단 부재
- **현상**: `--plan-talk N` 설정 시 Planner와 Creator 간의 이의제기(Challenge) 및 계획 갱신(Refine) 대화가 N회 진행됨.
- **문제점**: Creator의 이의제기가 실제로 Planner에 의해 수용 및 합의되었는지 논리적 종결 여부를 확인하지 않고, 무조건 지정된 턴 수(N)를 기계적으로 소모한 후 다음 단계로 진행함.
- **해결 방안**: Planner 갱신 리포트에 `[AGREEMENT: REACHED]` 같은 수렴 판정 토큰을 도입하거나, 이의제기가 없는 경우 N회 턴 전이라도 조기 종료(Early Break)할 수 있는 로직 추가.
### ISSUE-6: 타당하지 않은 리뷰 피드백 거부/반론 프로토콜의 스크립트 미지원
- **현상**: `MULTI_AGENT_RULES.md` 1장 규약에는 "개발 팀장이 리뷰어의 타당하지 않은 피드백을 거부하고 명확한 이유를 회신할 수 있다"고 명시되어 있음.
- **문제점**: `run_loop.sh`는 리뷰어의 `NOT PASS` 피드백 전체를 Creator에게 일방적으로 주입할 뿐, Creator가 특정 피드백을 거부하거나 반론을 제기하여 상호 조율하는 이의제기 채널이 코딩적으로 구현되어 있지 않음.
- **해결 방안**: Creator 교정 단계 프롬프트에 반론 작성 템플릿을 허용하고, 반론 발생 시 Planner/Reviewer에게 재검토를 요청하는 이의제기 브랜칭 로직 설계.
---
## 3. 🛡️ 오케스트레이션 위임 및 안전성/동시성 강제안 (Orchestration Enforcement & Reliability)
### ISSUE-7: 동일 워크스페이스 내 중복 루프 기동 방지 락 (Race-Free Lock) 설계 정교화
- **현상**: 동일 작업 트리에서 다수의 `run_loop.sh` 스크립트가 병렬 기동될 경우 SQLite DB 갱신 경합 및 YAML 데이터 오염이 일어날 수 있음.
- **문제점**: 단순 PID 파일 존재 여부만 체크할 경우, PID Rollover(프로세스 ID 재사용) 또는 `mkdir`과 PID 기록 사이의 생성 창(Grace Window)에서 살아있는 락을 타 프로세스가 훔쳐가는 "락 도난(Live-lock theft)" 현상 발생.
- **해결 방안**:
1. 락 소유자 레코드를 단순 `PID`에서 **`PID + 시작시각(lstart) + 워크스페이스`** 3중 구조로 결합하여 PID 재사용을 결정적으로 차단.
2. `mkdir` 직후 생성 창 유예 대기(Sleep Grace Period)를 부여하여 락 도난 방지.
3. `ps` CLI 부재 시 Fails-Open(락 무시) 대신 **Fails-Safe(락 존중 + 경고)** 로 전환하여 DB/YAML 오염 원천 방지.
### ISSUE-8: 비동기 잡 모니터링 타임아웃 및 헬스체크 최적화
- **현상**: `wait_for_job` 기본 타임아웃이 3900초(65분)로 설정되어 있어, 에이전트 세션 패닉이나 사망 시 오케스트레이터가 과도하게 오랫동안 대기함.
- **해결 방안**: 모니터링 수집 루프 내에서 herdr 세션의 라이브 상태(`alive`)를 매 주기마다 핑(Ping) 확인하여 세션 사망 시 즉시 `fail-fast` 하도록 개선.
### ISSUE-9: 조건부 오케스트레이션 위임 가드 (Invocation-Aware Scoped Guard)
- **현상**: 오케스트레이터(Antigravity)가 평상시에는 Main Creator로서 코드 및 문서를 직접 집필해야 하지만, `/multi-agent-mux-loop` 슬래시 커맨드/스킬이 인보크된 상황에서도 이를 인지하지 못하고 에이전트들에게 위임하는 대신 직접 수정을 시도하는 지침 이탈 발생.
- **문제점**: 오케스트레이터의 파일 직접 수정 권한을 무조건 뺏으면(1번 방안 부작용) 일반 작업이 불가능해지고, 자연어 지침에만 의존하면 슬래시 커맨드 호출 시 위임을 건너뛰는 모순 발생.
- **해결 방안**:
- **평상시 (일반 요청)**: 오케스트레이터가 **Main Creator**로서 소스 및 마크다운 파일 직접 작성/수정 도구(`write_to_file`, `replace_file_content`)를 자유롭게 사용하여 단독 구현 수행.
- **`/multi-agent-mux-loop` 호출 시 (스킬 활성화 상태)**: 스킬 인터셉터 가드(Guardrail)가 작동하여 직접 수정 도구 호출을 거부(Interception)하고, **"슬래시 커맨드가 인보크되었으므로 직접 수정을 중단하고 `run_loop.sh`를 실행하여 위임하십시오"**라는 에러를 반환해 `run_loop.sh` 자율 위임 실행을 코딩적으로 강제.
---
*본 분석서는 Planner(`claude`)와 Creator(`agy`)의 협업 계획(Job `96b6e07b`) 및 리뷰어 만장일치 PASS 합의를 바탕으로 최종 작성된 수합 최적화 명세서입니다.*
-74
View File
@@ -1,74 +0,0 @@
# 📋 PLAN_HERDR.md: herdr 기반 멀티플렉서 백엔드 전환 작업 계획서
이 문서는 기존 `tmux` 기반의 에이전트 라이프사이클 관리를 Rust 기반의 에이전트 인지형 멀티플렉서인 **herdr**로 전면 전환하기 위한 도입 배경, 아키텍처 전략 및 상세 작업 단계들을 정의합니다.
---
## 1. 🔍 도입 배경 및 필요성
현재 운영 중인 `tmux` 기반 백엔드는 훌륭한 호환성을 제공하지만, 다음과 같은 구조적 한계와 간헐적인 프롬프트 유실 오류(Prompt-lock)를 동반합니다.
### 🔴 기존 tmux 환경의 한계
* **대략적인 정적 상태 감지 (Coarse Quiescence)**: 입력을 주입하기 전에 터미널이 키를 수락할 수 있는 휴지 상태인지 확인하기 위해, 셸 스크립트 상에서 `capture-pane`을 0.1~0.5초 주기로 돌려 화면 변경 여부를 체크합니다. 이로 인해 CPU 자원이 급증하는 멀티 에이전트 구동 상황에서 입력을 유실하거나 `Enter` 키가 씹히는 현상이 발생합니다.
* **TUI 모달 상태 기계 파싱의 비효율**: 에이전트가 띄운 다이얼로그(예: 인증, 신뢰 확인)를 인식하기 위해 터미널 하단 20줄의 문자열을 정규식으로 직접 파싱하므로, 에이전트 버전업에 따른 TUI 레이아웃 변경에 매우 취약합니다.
### 🟢 herdr 도입 시 기대 효과
* **PTY 레벨의 밀리초(ms) 단위 이벤트 제어**: `herdr`은 Rust 네이티브로 작성되어 PTY(가상 터미널) 입출력 스트림의 유휴 상태를 서브-밀리초 레벨로 감지합니다. 이로 인해 프롬프트 주입 실패 및 명령 유실 오류가 **근본적으로 제로(0)에 가깝게 줄어듭니다.**
* **에이전트 상태 인지 API**: 에이전트 프로세스의 상태(Working, Idle, Blocked, Done)를 멀티플렉서 레벨에서 해석해 소켓 API로 제공하므로, 지저분한 화면 파싱 코드 없이 정교한 자율 관제가 가능합니다.
---
## 2. 🔀 형상 관리 및 배포 전략
두 백엔드(tmux/herdr)를 단일 코드베이스에서 듀얼 스위칭(`if/else`) 방식으로 지원하면 코드가 과도하게 무거워지고 버그 가능성이 높아집니다. 따라서 **독립된 브랜치 구조**로 깨끗하게 이원화하여 제공합니다.
* **`main` 브랜치 (tmux 기반)**:
* **목표**: 어디서나 즉시 실행 가능한 고호환성 프로덕션 버전.
* **의존성**: 추가 설치가 필요 없는 표준 `tmux` 환경.
* **`herdr` 브랜치 (herdr 기반)**:
* **목표**: 대화식 락 오류가 완벽히 통제되는 워크스테이션(macOS/Linux) 최적화 고안전성 버전.
* **의존성**: `herdr` CLI 및 Unix 소켓 API 환경.
---
## 3. 🎯 상세 구현 마일스톤 및 작업 계획
### 📍 Milestone 1: 개발 환경 구성 및 의존성 진단
* [ ] **브랜치 격리**: `git checkout -b herdr` 브랜치 생성 및 격리 개발 공간 확보.
* [ ] **인스톨러 개정 (`deploy/install_mam.sh`)**:
* 호스트 의존성 체크 대상에 `herdr` 추가 (`tmux` 진단 제거).
* `herdr`이 미설치된 경우, 공식 설치 가이드라인(`https://herdr.dev/install.sh`) 안내 출력 및 조기 종료 처리.
* `.mam/` 격리 폴더 및 환경설정 배포 규칙을 `herdr` 스펙에 맞게 조정.
### 📍 Milestone 2: 로우레벨 어댑터 전면 리팩토링 (`lib.sh`)
* [ ] **명령어 매핑**: `lib.sh` 내의 모든 `tmux` API 호출을 `herdr` 명령으로 전면 개정.
* `_tmux new-session` ➡️ `herdr run -d --name "$SESSION_NAME" -- "$CMD_FULL"`
* `_tmux capture-pane` ➡️ `herdr capture --name "$SESSION_NAME"`
* `_tmux send-keys` ➡️ `herdr send-keys --name "$SESSION_NAME" "$KEYS"`
* `_tmux kill-session` ➡️ `herdr kill --name "$SESSION_NAME"`
* [ ] **정적 상태 감지 함수 재작성 (`_pane_quiescent`)**:
* `herdr`이 기본 제공하는 세션 상태 조회 API를 파싱하여 PTY 정적 상태 여부를 판별하도록 대폭 경량화 및 고도화.
* [ ] **인풋 주입 엔진 고도화 (`send_keys_safe`)**:
* 복잡한 버퍼 제어(`set-buffer`/`paste-buffer`) 대신, `herdr` API를 경유한 다이렉트 프롬프트 주입 방식으로 단순화.
### 📍 Milestone 3: 에이전트 라이프사이클 관리 도구 이관
* [ ] **`create_session.sh` 수정**:
* `herdr` 기동 방식 및 pane PID 수집 로직 교체.
* `.mam/agent-sessions.yaml` 메타데이터 규격을 `herdr` 사양(예: `tmux_server` ➡️ `herdr_workspace`)에 맞게 정렬.
* [ ] **`resume_session.sh` 수정**:
* 죽은 `herdr` 프로세스를 감지하고 저장된 대화 ID와 함께 `herdr run`으로 복원하는 흐름 이식.
* [ ] **`stop_session.sh` 수정**:
* 에이전트 세션의 깔끔한 graceful 종료 및 최종 TUI 캡처 흐름을 `herdr` 규격으로 전환.
### 📍 Milestone 4: 검증 및 루프 완주
* [ ] **정적 분석**: `bash -n``shellcheck` 신규 경고 0건 검증.
* [ ] **오케스트레이션 루프 검증 (`run_loop.sh`)**:
* `run_loop.sh` 내부의 `delegate_job_safe` 실행을 `herdr` 세션 기반으로 연동하여 100% 자율 루프 구동 확인.
* 피어 리뷰어(`cline`, `claude`)들로부터 최종 `[VERDICT: PASS]` 서명 획득.
---
## 4. 📈 사후 관리 및 형상 병합 정책
* `herdr` 브랜치의 개발 및 검증이 완주되어 `PASS` 서명이 누적되면, `deploy/INSTALL.md``README.md` 문서를 개정하여 각 브랜치별 설치 절차를 문서화합니다.
* `main` 브랜치의 공통 규칙 버그 수정 사항(예: `AGENTS.md` 수정 등)은 주기적으로 `herdr` 브랜치로 `git merge`하여 정책적 일치성을 유지합니다.
-108
View File
@@ -1,108 +0,0 @@
# 📑 자율 반복 정제 루프 스킬 (`multi-agent-mux-loop`) 개발 계획서
이 문서는 멀티 에이전트 자율 오케스트레이션 루프(`multi-agent-mux-loop`)의 **최종 안전/가드레일 옵션 규격을 포함하여 완벽하게 정제된 마스터 계획서**입니다.
리뷰어 에이전트들의 교차 2차 피드백(Verdict 파싱, 자가 리뷰 방지, 타임아웃 보강)을 완벽하게 수렴하여 정교하게 갱신되었습니다.
---
## 1. ⚙️ 최종 스킬 명령 및 전체 옵션 세트 명세 (CLI Spec)
```bash
$ bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \
[--plan] \
[--plan-talk N] \
[--reviewer "reviewer-1,reviewer-2"] \
[--all-reviewer] \
[--max-loop N] \
[--verbose] \
[--cleanup] \
--target-agent "<creator-session-name>" \
--task "수행할 작업 목표"
```
### 📥 옵션 상세 리스트 및 가드레일 제약
| 옵션명 | 기본값 | 분류 | 역할 및 안전 조치 |
|---|---|---|---|
| `--plan` | 비활성 | 기능 | Planner 에이전트를 기동하여 협력 계획 수립 및 토론 단계 개시. (비활성화 시 기존 계획서를 로드하며, 계획서가 없는 경우 Creator가 직접 계획 및 설계를 수립하여 구동) |
| `--plan-talk N` | `1` | 안전 | 플래너-작업자 간 토론 왕복 횟수 상한선. 토큰 낭비 무한 토론 차단. |
| `--reviewer "A,B"` | 비활성 | 기능 | 지정된 peer 리뷰어 에이전트 세션(들)에 피드백 루프 의뢰 (주 작업자 세션은 강제 제외). |
| `--all-reviewer` | 비활성 | 기능 | 레지스트리 상의 모든 `role: reviewer` 세션들을 전수 자동 수집하여 의뢰 (주 작업자 세션은 강제 제외). |
| `--max-loop N` | `3` | **안전 (필수)** | 반려(`NOT PASS`) 시 최대 수정 횟수 제한. **토큰 비용 폭주 방지 가드레일.** |
| `--verbose` | 비활성 | 편의 | 단계별 타임라인 진행 상태 및 잡 매핑 로그의 실시간 상세 출력. |
| `--cleanup` | 비활성 | 편의 | 루프 완료 후 성공한 임시 잡 파일(`.mam/jobs/`)들의 자동 클린업 청소. |
| `--target-agent` | (필수) | 인프라 | 구현을 처리할 주 개발자(Creator) 세션 이름 명시. |
| `--task` | (필수) | 인프라 | 자율 루프에 전달할 최종 구현 지시사항 텍스트. |
---
## 🔄 2. 자율 오케스트레이션 상세 파이프라인 (Sequence Flow)
```mermaid
sequenceDiagram
autonumber
actor User as 사용자 / run_loop.sh
participant Plan as Planner Agent
participant Dev as Creator Agent
participant Rev as Reviewer Agents
User->>User: run_loop.sh 기동 (옵션 세트 검증 및 대상 예외 필터링)
%% Planning & Challenge discussion
alt --plan 지정 시
User->>Plan: delegate-job (계획 수립 지시)
Plan-->>User: 계획서 도출 완료
loop 지정된 --plan-talk 횟수 동안 반복 (기본 1회)
User->>Dev: delegate-job (계획서 비판적 검토 및 이의제기 지시)
Dev->>Plan: 계획서의 맹점 1가지 이상 Challenge 메일 교환
Plan-->>Dev: 수정 반영 및 최종 계획 합의
end
else --plan 미지정
alt 기존 계획 존재 시
User->>Dev: 기존 계획서 로드 및 구현 지시
else 계획 미존재 시
User->>Dev: Self-planning 지시 (스스로 계획/설계 수립하여 구현)
end
end
%% Execution
User->>Dev: delegate-job (작업 지시)
Dev-->>User: 구현 완료 (git diff 발생)
%% Peer-Review Loop with Max-Loop constraint
loop 최대 --max-loop 횟수 동안 반복 (기본 3회)
alt 리뷰어 옵션 지정 시 (--reviewer or --all-reviewer)
User->>Rev: delegate-job (정식 peer 코드 리뷰 위임)
Rev-->>User: [VERDICT: PASS] 또는 [VERDICT: NOT PASS] 태그 리포트 제출
alt 100% PASS 충족 시
Note over User,Rev: 루프 즉시 탈출 (성공)
else NOT PASS 검출 시
User->>Dev: 피드백 전달 및 수정 지시 (피드백 난이도에 따라 Planner 우회 계획 갱신 적용)
end
else 리뷰어 미지정
User->>Dev: Self-Review 지시 (자가 검증 및 자율 종결)
end
end
%% Cleanup & Final Report
alt --cleanup 지정 시
User->>User: 임시 잡 폴더 청소
end
User-->>User: 최종 결과 요약 출력 및 마감
```
---
## 🛠️ 3. 개발 로직 및 안전 파싱 체크포인트
### 1) Verdict 판정 파서 안전 가이드라인 (Fail-Closed & Precedence)
* **NOT PASS 우선권**: 리뷰 리포트 본문 내에 `[VERDICT: NOT PASS]` 가 단 한 번이라도 등장하면, `[VERDICT: PASS]` 문구 존재 여부와 상관없이 무조건 **NOT PASS**로 처리하여 오독 필터링을 방지합니다.
* **Fail-Closed 기본 실패주의**: 태그 누락이나 malformed 리포트로 인해 두 토큰이 모두 스캔되지 않을 경우, 통과시키지 않고 **NOT PASS(실패)** 로 취급하여 루프 무한 기동 및 맹점 통과를 원천 차단합니다.
* **템플릿 명시**: 리뷰어 위임 잡 발행 시, 최종 결과 요약 행에 정형화된 태그 `[VERDICT: PASS]` 혹은 `[VERDICT: NOT PASS]`를 리포트 본문 하단에 반드시 기재하도록 프롬프트 템플릿에 명시적으로 추가합니다.
### 2) 자가 리뷰 방지 가드 (Exclusion Rule)
* `--all-reviewer` 혹은 `--reviewer` 목록을 소집할 때, 해당 작업을 수행한 대상 개발자 세션인 `$TARGET_AGENT`**리뷰어 매핑 목록에서 강제로 배제(Exclude)** 하도록 파싱 쉘 스크립트에서 필터링을 적용합니다.
### 3) 쉘 예외 처리 및 대기 타임아웃 (Error Guard & Timeout)
* `grep -oP``find | head` 시 매칭이 없을 때 `set -eo pipefail`에 의해 쉘 스크립트 전체가 비명횡사하지 않도록 `|| true` 가드 및 공백 체크문을 엄밀히 적용합니다.
* `wait_for_job` 함수 실행 시 타임아웃 가드레일(`WAIT_TIMEOUT`, 기본값 3600초)을 명시적으로 설계하여 무한 루프 행(Hang) 현상을 차단합니다.
+15 -15
View File
@@ -1,6 +1,6 @@
# tmux-agent-orchestration (다중 에이전트 Tmux 오케스트레이션 및 메시징 백플레인) # herdr-agent-orchestration (다중 에이전트 Herdr 오케스트레이션 및 메시징 백플레인)
Tmux와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전트 오케스트레이션 및 메시징 백플레인** 프레임워크입니다. Claude, Hermes 등 다중 LLM 백엔드 에이전트 전반에 걸쳐 장시간 수행되는 작업(코드 생성, 리팩토링, 보안 검토 등)을 조정, 격리 및 감사할 수 있도록 설계되었습니다. Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전트 오케스트레이션 및 메시징 백플레인** 프레임워크입니다. Claude, Hermes 등 다중 LLM 백엔드 에이전트 전반에 걸쳐 장시간 수행되는 작업(코드 생성, 리팩토링, 보안 검토 등)을 조정, 격리 및 감사할 수 있도록 설계되었습니다.
--- ---
@@ -8,8 +8,8 @@ Tmux와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전
최근의 에이전트 워크플로우는 세션 타임아웃, 프로세스 격리 부재, 터미널 뷰포트 잘림(스크롤백 한계로 인한 디버그 로그 유실), 복잡한 동시성 경쟁 등의 문제를 자주 겪습니다. 최근의 에이전트 워크플로우는 세션 타임아웃, 프로세스 격리 부재, 터미널 뷰포트 잘림(스크롤백 한계로 인한 디버그 로그 유실), 복잡한 동시성 경쟁 등의 문제를 자주 겪습니다.
**tmux-agent-orchestration**은 다음과 같은 솔루션을 통해 이를 해결합니다: **herdr-agent-orchestration**은 다음과 같은 솔루션을 통해 이를 해결합니다:
1. **Tmux 기반 프로세스 격리:** 개별적으로 격리된 tmux 환경 내부에 에이전트 CLI 세션을 띄워, 백그라운드에서 끊김 없이 장시간 작업이 영속되도록 보장합니다. 1. **Herdr 기반 프로세스 격리:** 개별적으로 격리된 herdr 환경 내부에 에이전트 CLI 세션을 띄워, 백그라운드에서 끊김 없이 장시간 작업이 영속되도록 보장합니다.
2. **비동기 이벤트 기반 아키텍처:** MQTT 브로커를 메시징 백플레인으로 활용하여, 에이전트 간 상태 전이 단계(`started`, `progress`, `completed`, `error`)를 긴밀하게 제어 및 조정합니다. 2. **비동기 이벤트 기반 아키텍처:** MQTT 브로커를 메시징 백플레인으로 활용하여, 에이전트 간 상태 전이 단계(`started`, `progress`, `completed`, `error`)를 긴밀하게 제어 및 조정합니다.
3. **Multi-Agent Mux (MAM):** 파일 기반의 어드바이저리 락(`fcntl`) 및 SQLite WAL 데이터베이스(`.mam/agent-sessions.db`)를 결합하여, 동시성 작업 선점 경쟁을 방지하고 에이전트 세션의 라이프사이클을 드리프트 없이 관리합니다. 3. **Multi-Agent Mux (MAM):** 파일 기반의 어드바이저리 락(`fcntl`) 및 SQLite WAL 데이터베이스(`.mam/agent-sessions.db`)를 결합하여, 동시성 작업 선점 경쟁을 방지하고 에이전트 세션의 라이프사이클을 드리프트 없이 관리합니다.
4. **리뷰어 기반 고신뢰 검증 루프:** Worker 에이전트가 구현한 코드 변경 사항에 대해 상이한 강점을 지닌 전문 검증 에이전트(예: 논리 흐름을 정밀 검토하는 Claude, 셸 문법 및 안전성을 빠르게 확인하는 Hermes)들로부터 최종 `PASS` 판정을 획득한 뒤 머지하도록 교차 검증 루프를 자동화합니다. 4. **리뷰어 기반 고신뢰 검증 루프:** Worker 에이전트가 구현한 코드 변경 사항에 대해 상이한 강점을 지닌 전문 검증 에이전트(예: 논리 흐름을 정밀 검토하는 Claude, 셸 문법 및 안전성을 빠르게 확인하는 Hermes)들로부터 최종 `PASS` 판정을 획득한 뒤 머지하도록 교차 검증 루프를 자동화합니다.
@@ -20,11 +20,11 @@ Tmux와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전
모든 오케스트레이션 스킬들은 `.agents/skills/` 디렉터리 하위에 정의되어 있습니다: 모든 오케스트레이션 스킬들은 `.agents/skills/` 디렉터리 하위에 정의되어 있습니다:
* **`multi-agent-mux-create`**: 격리된 tmux 세션을 생성하고 특정 에이전트 CLI를 백그라운드에서 구동합니다. 프로세스 PID 캡처, 메타데이터 레지스트리 업데이트 및 에이전트 인증 검증을 처리합니다. * **`multi-agent-mux-create`**: 격리된 herdr 세션을 생성하고 특정 에이전트 CLI를 백그라운드에서 구동합니다. 프로세스 PID 캡처, 메타데이터 레지스트리 업데이트 및 에이전트 인증 검증을 처리합니다.
* **`multi-agent-mux-stop`**: 에이전트 CLI 세션을 정상 종료 키 입력(`/exit` 또는 `Exit`)을 통해 안전하게 닫고, 격리된 대화 히스토리 및 데이터베이스 로그를 삭제(purge)하는 클린업 작업을 수행합니다. * **`multi-agent-mux-stop`**: 에이전트 CLI 세션을 정상 종료 키 입력(`/exit` 또는 `Exit`)을 통해 안전하게 닫고, 격리된 대화 히스토리 및 데이터베이스 로그를 삭제(purge)하는 클린업 작업을 수행합니다.
* **`multi-agent-mux-resume`**: 디스크 또는 캐시에서 특정 워크스페이스의 세션 UUID를 조회하여 기존 대화 상태(`claude -r <uuid>` 또는 `hermes --resume <uuid>`) 그대로 세션을 복구하고 재개합니다. * **`multi-agent-mux-resume`**: 디스크 또는 캐시에서 특정 워크스페이스의 세션 UUID를 조회하여 기존 대화 상태(`claude -r <uuid>` 또는 `hermes --resume <uuid>`) 그대로 세션을 복구하고 재개합니다.
* **`multi-agent-mux-status`**: 활성화된 모든 세션의 실시간 작동 상태를 쿼리하여 PID 정합성, 실행 명령 포맷, tmux 실제 상태와 데이터베이스 간의 동기화 드리프트를 감지합니다. * **`multi-agent-mux-status`**: 활성화된 모든 세션의 실시간 작동 상태를 쿼리하여 PID 정합성, 실행 명령 포맷, herdr 실제 상태와 데이터베이스 간의 동기화 드리프트를 감지합니다.
* **`multi-agent-mux-monitor`**: 백그라운드에서 Kanban Reconcile 프로세스로 실행되어, 실시간 tmux 세션 변화를 모니터링하고 `.mam/agent-sessions.yaml` 메타데이터 파일에 상태를 동기화합니다. * **`multi-agent-mux-monitor`**: 백그라운드에서 Kanban Reconcile 프로세스로 실행되어, 실시간 herdr 세션 변화를 모니터링하고 `.mam/agent-sessions.yaml` 메타데이터 파일에 상태를 동기화합니다.
* **`multi-agent-mux-delegate-job`**: 태스크를 비동기식 독립 잡으로 위임 및 관리하는 핵심 모듈입니다: * **`multi-agent-mux-delegate-job`**: 태스크를 비동기식 독립 잡으로 위임 및 관리하는 핵심 모듈입니다:
* `registry.py`: 파일 락(`fcntl`)을 활용해 경쟁 조건 없이 잡을 원자적으로 등록 및 점유(claim)합니다. * `registry.py`: 파일 락(`fcntl`)을 활용해 경쟁 조건 없이 잡을 원자적으로 등록 및 점유(claim)합니다.
* `job_subscriber.py`: MQTT 백플레인 채널을 구독하여 실시간 상태 이벤트를 수집하고 이를 감사 로그(audit trail)에 기록합니다. * `job_subscriber.py`: MQTT 백플레인 채널을 구독하여 실시간 상태 이벤트를 수집하고 이를 감사 로그(audit trail)에 기록합니다.
@@ -37,7 +37,7 @@ Tmux와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전
이 시스템은 크게 두 가지 계층(Layer)을 통해 다중 워크스페이스에서 작동하는 LLM 에이전트들을 조율합니다: 이 시스템은 크게 두 가지 계층(Layer)을 통해 다중 워크스페이스에서 작동하는 LLM 에이전트들을 조율합니다:
1. **Layer A — Tmux 오케스트레이션 (lib.sh + status/resume/stop/create)**: 워크스페이스별 에이전트 세션을 독립된 tmux 인스턴스로 분리 실행하고, `.mam/agent-sessions.yaml` 및 SQLite 데이터베이스(`.mam/agent-sessions.db`)를 통해 에이전트 세션 메타데이터의 단일 참조 지점(Single Source of Truth)을 유지합니다. 1. **Layer A — Herdr 오케스트레이션 (lib.sh + status/resume/stop/create)**: 워크스페이스별 에이전트 세션을 독립된 herdr 인스턴스로 분리 실행하고, `.mam/agent-sessions.yaml` 및 SQLite 데이터베이스(`.mam/agent-sessions.db`)를 통해 에이전트 세션 메타데이터의 단일 참조 지점(Single Source of Truth)을 유지합니다.
2. **Layer B — 비동기 잡 위임 (delegate-job)**: 에이전트에 특정 태스크를 전송하고 비동기 이벤트 채널(MQTT)을 통해 진행 상황과 완료 여부를 모니터링합니다. 2. **Layer B — 비동기 잡 위임 (delegate-job)**: 에이전트에 특정 태스크를 전송하고 비동기 이벤트 채널(MQTT)을 통해 진행 상황과 완료 여부를 모니터링합니다.
두 레이어는 파일 I/O 처리를 위한 하나의 핵심 관문인 `lib.sh::atomic_dump_yaml`을 공유합니다. 모든 YAML/DB 쓰기 작업은 SQLite 데이터베이스 트랜잭션 락과 데이터 스키마 유효성 검증을 거칩니다. 두 레이어는 파일 I/O 처리를 위한 하나의 핵심 관문인 `lib.sh::atomic_dump_yaml`을 공유합니다. 모든 YAML/DB 쓰기 작업은 SQLite 데이터베이스 트랜잭션 락과 데이터 스키마 유효성 검증을 거칩니다.
@@ -74,12 +74,12 @@ Tmux와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전
+--------+ +--------+
``` ```
### 🔒 Tmux 서버 격리 (Tmux Server Isolation) ### 🔒 Herdr 서버 격리 (Herdr Server Isolation)
에이전트 세션 간의 충돌 및 시스템 전역 tmux 프로세스와의 혼선을 막기 위해 독립된 서버 소켓 환경을 보장합니다: 에이전트 세션 간의 충돌 및 시스템 전역 herdr 프로세스와의 혼선을 막기 위해 독립된 서버 소켓 환경을 보장합니다:
* **워크스페이스별 심(Shim):** `_init_tmux_isolation``_resolve_real_tmux_path` 함수가 `/tmp/multi-agent-tmux-shim/<TMUX_SERVER_NAME>/tmux` 경로에 독립된 심 디렉터리를 구성하고, 일반 tmux 명령 실행 시 자동으로 `tmux -L <server>` 형태의 독립 소켓 서버를 사용하게 만듭니다. * **워크스페이스별 심(Shim):** `_init_herdr_isolation``_resolve_real_herdr_path` 함수가 `/tmp/multi-agent-herdr-shim/<HERDR_SERVER_NAME>/herdr` 경로에 독립된 심 디렉터리를 구성하고, 일반 herdr 명령 실행 시 자동으로 `herdr -L <server>` 형태의 독립 소켓 서버를 사용하게 만듭니다.
* **PATH 환경변수 변조:** 자식 프로세스를 생성할 때 `PATH` 변수 맨 앞에 심 디렉터리 경로를 삽입합니다. 이로 인해 에이전트의 내부 셸에서 수행되는 모든 `tmux` 명령어는 해당 격리 서버 소켓으로 강제 제약됩니다. * **PATH 환경변수 변조:** 자식 프로세스를 생성할 때 `PATH` 변수 맨 앞에 심 디렉터리 경로를 삽입합니다. 이로 인해 에이전트의 내부 셸에서 수행되는 모든 `herdr` 명령어는 해당 격리 서버 소켓으로 강제 제약됩니다.
* **환경 복구:** `TMUX_SERVER_NAME``default`로 설정하는 경우 PATH 오버라이드가 정리되고 기본 전역 tmux 서버를 사용하게 됩니다. * **환경 복구:** `HERDR_SERVER_NAME``default`로 설정하는 경우 PATH 오버라이드가 정리되고 기본 전역 herdr 서버를 사용하게 됩니다.
### 🛡️ 동시성 설계 및 쓰기 직렬화 ### 🛡️ 동시성 설계 및 쓰기 직렬화
@@ -157,7 +157,7 @@ sequenceDiagram
├── deploy/ # 배포 및 설치 도구 패키지 폴더 ├── deploy/ # 배포 및 설치 도구 패키지 폴더
│ ├── INSTALL.md # 설치 가이드 및 퀵스타트 매뉴얼 │ ├── INSTALL.md # 설치 가이드 및 퀵스타트 매뉴얼
│ ├── install_mam.sh # 로컬/클론 인스톨러 스크립트 │ ├── install_mam.sh # 로컬/클론 인스톨러 스크립트
│ ├── generate-env.sh # 환경 파일(.env) 템플릿 복사 스크립트 │ ├── generate-env.sh # 환경 파일(.mam.env) 템플릿 복사 스크립트
│ ├── install.sh # 원격/네트워크 인스톨러 스크립트 │ ├── install.sh # 원격/네트워크 인스톨러 스크립트
│ ├── update.sh # 업데이트 헬퍼 스크립트 │ ├── update.sh # 업데이트 헬퍼 스크립트
│ └── remove.sh # 삭제/언인스톨 헬퍼 스크립트 │ └── remove.sh # 삭제/언인스톨 헬퍼 스크립트
@@ -173,7 +173,7 @@ sequenceDiagram
자세한 빌드 절차는 **[BOOTSTRAP.md](./BOOTSTRAP.md)** 문서를 참조하십시오. 아래는 간략한 요약입니다: 자세한 빌드 절차는 **[BOOTSTRAP.md](./BOOTSTRAP.md)** 문서를 참조하십시오. 아래는 간략한 요약입니다:
1. **환경 설정 파일(.env) 생성:** 1. **환경 설정 파일(.mam.env) 생성:**
```bash ```bash
./deploy/generate-env.sh ./deploy/generate-env.sh
``` ```
+14 -14
View File
@@ -1,6 +1,6 @@
# tmux-agent-orchestration # herdr-agent-orchestration
An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Tmux and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (e.g., Claude, Hermes). An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (e.g., Claude, Hermes).
--- ---
@@ -8,8 +8,8 @@ An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane*
Modern agentic workflows often suffer from session timeout, lack of process isolation, terminal viewport truncation (scrollback limits), and complex concurrency issues. Modern agentic workflows often suffer from session timeout, lack of process isolation, terminal viewport truncation (scrollback limits), and complex concurrency issues.
**tmux-agent-orchestration** addresses these problems by providing: **herdr-agent-orchestration** addresses these problems by providing:
1. **Tmux-based Process Isolation:** Spawning LLM client sessions inside dedicated, isolated tmux environments to support persistent background runs. 1. **Herdr-based Process Isolation:** Spawning LLM client sessions inside dedicated, isolated herdr environments to support persistent background runs.
2. **Asynchronous Event-Driven Architecture:** Leveraging an MQTT broker as a message backplane to coordinate state transitions (`started`, `progress`, `completed`, `error`) between collaborating agents. 2. **Asynchronous Event-Driven Architecture:** Leveraging an MQTT broker as a message backplane to coordinate state transitions (`started`, `progress`, `completed`, `error`) between collaborating agents.
3. **Multi-Agent Mux (MAM):** Combining local file-based locks (fcntl) and an ACID-compliant SQLite WAL database (`.mam/agent-sessions.db`) to manage concurrent job claims and track running agent sessions without drift. 3. **Multi-Agent Mux (MAM):** Combining local file-based locks (fcntl) and an ACID-compliant SQLite WAL database (`.mam/agent-sessions.db`) to manage concurrent job claims and track running agent sessions without drift.
4. **Automated Review & Quality Loop:** Implementing parallel reviewer loops where worker agents must receive a `PASS` rating from various specialized verification agents (e.g., Claude for high-level logic, Hermes for shell syntax/safety) before merging code. 4. **Automated Review & Quality Loop:** Implementing parallel reviewer loops where worker agents must receive a `PASS` rating from various specialized verification agents (e.g., Claude for high-level logic, Hermes for shell syntax/safety) before merging code.
@@ -30,7 +30,7 @@ Alternatively, if you have already cloned the repository locally, run the instal
bash deploy/install.sh bash deploy/install.sh
``` ```
The idempotent installer automatically validates system dependencies (tmux, python3, and PyYAML), creates the python virtual environment (`.venv`), installs dependencies, copies `.env.example` as `.env`, and initializes the `.agents/` scaffolding. The idempotent installer automatically validates system dependencies (herdr, python3, and PyYAML), creates the python virtual environment (`.venv`), installs dependencies, copies `.mam.env.example` as `.mam.env`, and initializes the `.agents/` scaffolding.
--- ---
@@ -38,11 +38,11 @@ The idempotent installer automatically validates system dependencies (tmux, pyth
All orchestration functionalities are structured under the `.agents/skills/` directory: All orchestration functionalities are structured under the `.agents/skills/` directory:
* **`multi-agent-mux-create`**: Spawns isolated tmux sessions running specified agent CLI wrappers. It captures system processes, updates metadata registries, and enforces authentication checks. * **`multi-agent-mux-create`**: Spawns isolated herdr sessions running specified agent CLI wrappers. It captures system processes, updates metadata registries, and enforces authentication checks.
* **`multi-agent-mux-stop`**: Gracefully terminates agent CLI sessions (using key macros like `/exit` or `Exit`) and handles disk purge operations (removing conversation JSON files and SQLite logs for deleted workspaces). * **`multi-agent-mux-stop`**: Gracefully terminates agent CLI sessions (using key macros like `/exit` or `Exit`) and handles disk purge operations (removing conversation JSON files and SQLite logs for deleted workspaces).
* **`multi-agent-mux-resume`**: Restores stopped sessions by resolving workspace UUIDs from disk or cache, and invokes the underlying agent using session-resume parameters (e.g., `claude -r <uuid>` or `hermes --resume <uuid>`). * **`multi-agent-mux-resume`**: Restores stopped sessions by resolving workspace UUIDs from disk or cache, and invokes the underlying agent using session-resume parameters (e.g., `claude -r <uuid>` or `hermes --resume <uuid>`).
* **`multi-agent-mux-status`**: Queries the running states of all active sessions, detecting PID mismatches, command signatures, and drifts between actual tmux instances and the registry database. * **`multi-agent-mux-status`**: Queries the running states of all active sessions, detecting PID mismatches, command signatures, and drifts between actual herdr instances and the registry database.
* **`multi-agent-mux-monitor`**: A long-running Kanban reconcile worker that dynamically monitors tmux sessions and synchronizes states to `.mam/agent-sessions.yaml`. * **`multi-agent-mux-monitor`**: A long-running Kanban reconcile worker that dynamically monitors herdr sessions and synchronizes states to `.mam/agent-sessions.yaml`.
* **`multi-agent-mux-delegate-job`**: The core asynchronous task distribution module containing: * **`multi-agent-mux-delegate-job`**: The core asynchronous task distribution module containing:
* `registry.py`: Atomically registers and claims jobs using file advisory locks (`fcntl`). * `registry.py`: Atomically registers and claims jobs using file advisory locks (`fcntl`).
* `job_subscriber.py`: Connects to the MQTT backplane, captures live events, and appends them to audit trails. * `job_subscriber.py`: Connects to the MQTT backplane, captures live events, and appends them to audit trails.
@@ -55,7 +55,7 @@ All orchestration functionalities are structured under the `.agents/skills/` dir
The system coordinates LLM agents across multiple workspaces through two core layers: The system coordinates LLM agents across multiple workspaces through two core layers:
1. **Layer A — Tmux Orchestration (lib.sh + status/resume/stop/create)**: Runs the agents (one tmux session per agent-workspace combination) and maintains an authoritative registry in `.mam/agent-sessions.yaml` (+ `.mam/agent-sessions.db`). 1. **Layer A — Herdr Orchestration (lib.sh + status/resume/stop/create)**: Runs the agents (one herdr session per agent-workspace combination) and maintains an authoritative registry in `.mam/agent-sessions.yaml` (+ `.mam/agent-sessions.db`).
2. **Layer B — Async Job Delegation (delegate-job)**: Dispatches a task to an agent and observes progress and completion via an event channel. 2. **Layer B — Async Job Delegation (delegate-job)**: Dispatches a task to an agent and observes progress and completion via an event channel.
These two layers share one lock-guarded chokepoint for file I/O: `lib.sh::atomic_dump_yaml`. Every write is protected by an exclusive SQLite database transaction lock and schema validation. These two layers share one lock-guarded chokepoint for file I/O: `lib.sh::atomic_dump_yaml`. Every write is protected by an exclusive SQLite database transaction lock and schema validation.
@@ -92,12 +92,12 @@ These two layers share one lock-guarded chokepoint for file I/O: `lib.sh::atomic
+--------+ +--------+
``` ```
### 🔒 Tmux Server Isolation ### 🔒 Herdr Server Isolation
To prevent workspace tmux processes from interfering with each other or with system tmux servers, the framework enforces isolated tmux environments: To prevent workspace herdr processes from interfering with each other or with system herdr servers, the framework enforces isolated herdr environments:
* **Per-Workspace Shim:** `_init_tmux_isolation` and `_resolve_real_tmux_path` instantiate a per-workspace shim directory under `/tmp/multi-agent-tmux-shim/<TMUX_SERVER_NAME>/tmux` that intercepts tmux commands and wraps them in `tmux -L <server>`. * **Per-Workspace Shim:** `_init_herdr_isolation` and `_resolve_real_herdr_path` instantiate a per-workspace shim directory under `/tmp/multi-agent-herdr-shim/<HERDR_SERVER_NAME>/herdr` that intercepts herdr commands and wraps them in `herdr -L <server>`.
* **PATH Rewriting:** The `PATH` environment variable is dynamically prepended with the shim path in all child processes. This ensures any `tmux` invocation within the agent's process tree is restricted to its isolated socket server. * **PATH Rewriting:** The `PATH` environment variable is dynamically prepended with the shim path in all child processes. This ensures any `herdr` invocation within the agent's process tree is restricted to its isolated socket server.
* **Environment Restoration:** If `TMUX_SERVER_NAME` is set to `default`, the PATH override is removed, reverting to the default global tmux server. * **Environment Restoration:** If `HERDR_SERVER_NAME` is set to `default`, the PATH override is removed, reverting to the default global herdr server.
### 🛡️ Concurrency Design & Write Serialization ### 🛡️ Concurrency Design & Write Serialization
-101
View File
@@ -1,101 +0,0 @@
# 📋 Recommended Multi-Agent Session Architecture Guide
본 문서는 `multi-agent-mux` 환경에서 오케스트레이션 루프(`multi-agent-mux-loop`)를 활용해 고품질 소프트웨어를 개발할 때 가장 권장되는 **3-에이전트 역할 분리 아키텍처**와 설정 방법 및 추천 이유에 대해 설명합니다.
---
## 👥 1. 추천 3-에이전트 구성 (Roles & Configuration)
`multi-agent-mux` 환경에서는 다음 세 가지 전문 세션을 생성하여 상시 기동해 두는 것이 가장 이상적입니다.
```mermaid
graph TD
User([사용자/Orchestrator]) <--> AGY_Parent[Antigravity Parent]
AGY_Parent -->|1. 계획 수립 위임| Planner[Planner 세션 <br> claude]
AGY_Parent -->|2. 구현 위임| Creator[Creator 세션 <br> agy]
AGY_Parent -->|3. 교차 검증 위임| Reviewer[Reviewer 세션 <br> cline]
Planner -->|설계/피드백 루프| Creator
Creator -->|구현 완료| Reviewer
Reviewer -->|Verdict PASS/NOT PASS| Planner
```
### ① Planner 에이전트
* **역할 (Role)**: `planner-reviewer`
* **주요 임무**: 전체 아키텍처 아웃라인 설계, 구현 계획서 수립, 이의 제기 수렴 및 계획 개정(Refinement).
* **추천 에이전트 종류**: `claude` (긴 추론 맥락과 설계 완성도가 높음)
* **생성 명령어**:
```bash
# planner-reviewer 역할로 claude 세션 기동
bash .agents/skills/multi-agent-mux-create/multi-agent-mux-create \
--agent claude \
--role planner-reviewer \
--name canary-projects-multi-agent-mux-planner-reviewer-claude
```
### ② Creator 에이전트 (주작업자)
* **역할 (Role)**: `creator`
* **주요 임무**: 계획서상의 제약조건 검토 및 이의제기(Challenge), 실제 코드베이스 구현 편집, DoD 자가 검증.
* **추천 에이전트 종류**: `agy` (기민한 도구 실행 속도 및 로컬 파일 편집 최적화)
* **생성 명령어**:
```bash
# creator 역할로 agy 세션 기동
bash .agents/skills/multi-agent-mux-create/multi-agent-mux-create \
--agent agy \
--role creator \
--name canary-projects-multi-agent-mux-creator-agy
```
### ③ Reviewer 에이전트
* **역할 (Role)**: `reviewer`
* **주요 임무**: 구현된 변경분(`git diff`)과 구현 계획서를 기반으로 빌드 가능성, 린트, 로직 유실 교차 피어 리뷰.
* **추천 에이전트 종류**: `cline` (안정적인 컴파일 도구 활용 및 린터 체크 강점)
* **생성 명령어**:
```bash
# reviewer 역할로 cline 세션 기동
bash .agents/skills/multi-agent-mux-create/multi-agent-mux-create \
--agent cline \
--role reviewer \
--name canary-projects-multi-agent-mux-reviewer-cline
```
---
## 💡 2. 왜 3개의 에이전트 분리를 강력히 추천하는가?
부모 에이전트(Antigravity)가 오케스트레이션과 코드 개발을 모두 처리하지 않고, 별도의 격리된 3개의 역할 세션을 두는 데에는 다음과 같은 명확한 공학적 이유가 있습니다.
### ① 대화창 컨텍스트(Context Window) 오염 방지
* **디테일의 지옥**: 에이전트가 코드를 탐색하고, 컴파일 오류를 잡고, 수많은 파일라인을 편집하는 세부 구현 과정은 수십만 토큰에 달하는 방대한 런타임 로그와 코드를 누적시킵니다.
* **해결책**: 만약 오케스트레이터(부모 에이전트)가 이를 직접 수행하면 사용자님과의 대화창 컨텍스트가 구현 로그로 가득 차, 이전에 의논했던 아키텍처 제약이나 중요 요구사항을 쉽게 잊어버립니다. 역할을 격리함으로써 각 세션은 자신의 세부 구현 컨텍스트만 소비하고 소멸합니다.
### ② 비동기 개발 자율성 (Asynchronous Autonomy)
* **대기 시간 최소화**: 오케스트레이션 루프가 설계 검토, 피드백, 자가 수정 등을 수차례 반복하며 백그라운드(tmux)에서 스스로 문제를 해결해 나가는 동안, 사용자님은 저(부모 에이전트)와 멈춤 없이 계속해서 고수준 설계 및 다른 기능에 대한 논의를 이어나갈 수 있습니다.
* **생산성 극대화**: 부모 에이전트가 코딩을 하느라 대화를 블로킹하는 현상이 발생하지 않습니다.
### ③ 교차 검증을 통한 객관성 확보 (Peer Review Objectivity)
* **작성자와 검증자의 분리**: 코드를 직접 짠 에이전트가 자기 자신의 코드를 완벽하게 리뷰하는 것은 불가능에 가깝습니다(인지 편향 발생).
* **해결책**: 구현을 전담한 `Creator`와, 이를 객관적인 삼자 관점에서 검토하는 `Reviewer` 세션을 철저히 독립시킴으로써 코드 품질 결함을 높은 확률로 선제 필터링할 수 있습니다.
### ④ 이기종 모델/도구의 결합 (Heterogeneous Collaboration)
* **각자 잘하는 분야의 극대화**:
* **Planner (Claude)**: 설계 및 아키텍처 정합성 수립에 특화
* **Creator (Antigravity/Agy)**: 신속하고 정확한 로컬 파일 편집 및 도구 호출에 특화
* **Reviewer (Cline)**: 린트 체크, 빌드 테스트 등 철저한 안전망 검증에 특화
* 이러한 하이브리드 조합을 구성할 때 루프 전체의 최종 도달 성공률이 가장 높게 나타납니다.
---
## 🛠️ 3. 3-에이전트 루프 실행 방법
에이전트들이 생성되어 기동(Running) 중인 경우, 다음과 같이 계획 수립(`--plan`) 및 전체 교차 리뷰(`--all-reviewer`) 옵션을 주어 자율 협업 개발을 시작할 수 있습니다.
```bash
bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \
--target-agent "canary-projects-multi-agent-mux-creator-agy" \
--plan \
--all-reviewer \
--task "여기에 개발하고자 하는 태스크의 최종 목표를 상세히 기술합니다."
```
이 루프는 **기획 ➡️ 작업자 이의제기 ➡️ 계획 개정 ➡️ 코드 개발 ➡️ 교차 피어 리뷰 ➡️ 피드백 수렴 재구현**의 전 과정을 자동으로 진행하여, 빌드 및 린트가 보장되는 코드를 저장소에 자동으로 커밋 및 병합합니다.
-141
View File
@@ -1,141 +0,0 @@
# Multi-Agent Mux (MAM) Architecture & Orchestration Report
본 보고서는 **Multi-Agent Mux (MAM)** 프로젝트의 개발 과정과 아키텍처를 바탕으로, 다중 자율 AI 에이전트(Claude, Agy/Antigravity, Cline, Hermes 등)를 효과적으로 오케스트레이션하기 위해 필요한 핵심 기능과 이를 구현한 방법론, 그리고 각 기능이 부재할 경우 발생하는 장애 문제점을 구체적 예시와 함께 종합 정리한 문서입니다.
---
## 1. 개요 (Executive Summary)
현대 AI 에이전트 워크플로우는 세션 타임아웃, 프로세스 격리 부재, 터미널 뷰포트 잘림, 단일 에이전트의 자기 과신(Self-Confidence) 오류 등 다양한 문제점을 내포하고 있습니다.
**Multi-Agent Mux**는 **Tmux/Herdr** 프로세스 관리자와 **MQTT 메시징 백플레인**을 결합하여, 여러 LLM 에이전트가 안전하게 프로세스를 격리받고 비동기로 이벤트를 주고받으며 **Planner ➡️ Creator ➡️ Reviewers** 간의 자율 검증 및 교차 리뷰 루프를 수행할 수 있도록 설계된 고신뢰성 오케스트레이션 프레임워크입니다.
---
## 2. 코어 스킬 구성 명세 (Core Skills Inventory)
프레임워크의 모든 핵심 제어 로직은 `.agents/skills/` 하위의 독립된 스킬 모듈로 응집되어 있으며, 각 스킬의 기능은 다음과 같습니다.
* **`multi-agent-mux-create`**
* 지정된 작업 공간에 최적화된 명칭으로 독립된 에이전트 세션을 새로 구동하고, 초기 메타데이터를 원자적으로 등록합니다.
* 에이전트 구동 즉시 워크스페이스 맥락과 자신의 역할(Role)을 스스로 파악하도록 자동 온보딩 지시서 주입을 지원합니다.
* **`multi-agent-mux-resume`**
* 이전에 중지되거나 중단된 에이전트의 대화 식별자(UUID)를 디스크 및 레지스트리에서 추적하여 이전 문맥 그대로 세션을 복원합니다.
* 에이전트 기동 직후 필요한 권한 우회 및 복구 수락 대화상자를 백그라운드 키스트로크 주입으로 자동 처리합니다.
* **`multi-agent-mux-stop`**
* 에이전트 TUI 화면 뷰포트를 최종 스냅샷으로 보존하고 `Safe Exit Key` ➡️ `SIGTERM` ➡️ `SIGKILL` 다단계 수순으로 안전하게 정지시킵니다.
* 에이전트의 대화 파일(JSONL/DB)을 디스크에 영구 보존하여 추후 언제든지 재개(`status=stopped`)할 수 있도록 상태를 기록합니다.
* **`multi-agent-mux-status`**
* 실제 실행 중인 OS 프로세스 현황과 레지스트리 기록 간의 불일치(Drift) 및 동작 상태를 안전하게 즉시 표기합니다.
* 데이터베이스나 프로세스 상태 변경을 유발하지 않는 순수 읽기 전용(Read-only) 모드로 시스템의 정합성을 검증합니다.
* **`multi-agent-mux-monitor`**
* 백그라운드에서 지속 구동되어 죽은 세션 격하, 신규 세션 자동 등록, 디스크 로그와 UUID 동기화를 자동 화해(Reconciliation)합니다.
* 운영체제 Tmux/Herdr 런타임과 YAML 레지스트리 데이터 간의 괴리를 실시간으로 자동 복구합니다.
* **`multi-agent-mux-delegate-job`**
* 타 에이전트에게 비동기 태스크를 위임하고, MQTT 메시징 백플레인을 통해 실행 상태(`started` ➡️ `completed`/`error`)를 관찰합니다.
* 단일 작업 위임(`direct`)부터 에이전트 간 1:1 토론(`discuss`), 검증 루프(`loop`)까지 다양한 비동기 작업 위임 패턴을 처리합니다.
* **`multi-agent-mux-loop`**
* 설계(Planner) ➡️ 구현(Creator) ➡️ 교차 검수(Reviewers)로 이어지는 자율 개발 및 검증 순환 루프를 오케스트레이션합니다.
* 지정되거나 자동 수집된 모든 리뷰어의 만장일치 `[VERDICT: PASS]` 판정을 얻을 때까지 자율 교정 작업을 자동 반복합니다.
---
## 3. Multi-Agent Orchestration을 위한 핵심 기능 및 구현 방법 (부재 시 장애 예시 포함)
다중 에이전트 협업 환경을 구축하기 위해 필수적이었던 8가지 주요 기능과, 각 기능이 부재할 경우 발생하는 실제 장애 문제점 및 구체적인 구현 기법입니다.
### 🔑 1) 프로세스 및 실행 환경 격리 (Environment Isolation)
* **기능 부재 시 발생하는 문제 (구체적 장애 예시)**:
* *인증 및 설정 파일 오염*: 서로 다른 에이전트(예: Claude와 Agy)가 동일한 기본 홈 디렉토리(`~/.gemini`, `~/.claude`)를 공유하면, 한 에이전트가 설정을 덮어쓰거나 로그인 세션을 건드려 상대방 에이전트의 인증이 갑자기 해제되는 장애가 발생합니다.
* *초기 대화상자 차단 패닉*: 예컨대 Agy 에이전트가 격리되지 않은 환경에서 구동될 경우 약관 동의(TOS)나 테마 설정 마법사 화면이 매번 출력되어 프롬프트 입력창이 상시 차단(Prompt Lockout)되는 현상이 일어납니다.
* **필요성**: 에이전트 간 설정 파일, 캐시, 세션 이름 및 환경 변수 충돌을 물리적으로 막아야 합니다.
* **구현 방법**:
* `herdr --session <name>` 명령을 활용해 에이전트 그룹마다 완전히 독립된 소켓과 서버 세션을 부여했습니다.
* 에이전트별로 워크스페이스 내에 임시 홈 디렉토리(`.mam/agent_homes/<UUID>`)를 프로비저닝하고 `HOME` 또는 `CLAUDE_CONFIG_DIR` 환경변수를 덮어씌워 완벽히 독립된 가상 홈 디렉토리를 제공했습니다.
### 🗃️ 2) 에이전트 관리를 위한 이중 레지스트리 분리 (`agent-sessions.yaml` vs `agent-sessions.db`)
* **기능 부재 시 발생하는 문제 (구체적 장애 예시)**:
* *YAML 단독 사용 시*: 여러 에이전트와 모니터링 프로세스가 동시 쓰기를 감행하다 텍스트 파일이 도중에 깨져 `YAML parsing syntax error`가 발생하여 전체 시스템 레지스트리가 즉시 마비됩니다.
* *DB 단독 사용 시*: 바이너리 데이터베이스로만 세션을 관리할 경우 개발자나 외부 도구가 `git status`, `cat`, 마크다운 뷰어 등으로 현재 에이전트 세션의 상태나 장애 원인을 직관적으로 디버깅할 수 없어 가시성이 완전 상실됩니다.
* **필요성**: 직관적 디버깅 가시성(Human-Readability)과 고성능 동시 트랜잭션 안전성(Transactional Integrity)이라는 두 가지 목적을 동시에 달성해야 합니다.
* **구현 방법**:
* `.mam/agent-sessions.db` (SQLite)를 내부 동시성 제어 및 원자적 상태 변경의 진실 원천(Single Source of Truth)으로 지정했습니다.
* DB 트랜잭션 커밋 완료 직후 PyYAML 기반 헬퍼 함수(`atomic_dump_yaml`)를 통해 정제된 데이터를 `.mam/agent-sessions.yaml` 파일로 원자적 동기화 덤프(`os.replace`)하여, 사람과 시스템 모두 만족하는 이중화 구조를 구축했습니다.
### 🔄 3) 역할별 에이전트 페르소나 기반 자율 품질 합의 및 리뷰 루프 (Persona-based Autonomous Consensus Loop)
* **기능 부재 시 발생하는 문제 (구체적 장애 예시)**:
* *단일 에이전트 자기 환각(Hallucination) 머지*: 코드 작성 에이전트 혼자 작업을 진행하고 자가 승인(Self-Approve)하도록 두면, 자신이 유발한 메모리 누수, 잘못된 예외 처리, 안 보이는 타입 오류를 "정상 구현됨"으로 착각하고 버그가 포함된 코드를 저장소에 그대로 커밋/머지하는 치명적 결과가 초래됩니다.
* **필요성**: 객관적인 제3의 에이전트들이 부여된 역할(Planner, Creator, Reviewer) 및 특화된 페르소나(Persona)에 따라 엄격한 품질 기준(DoD)으로 교차 검증하고 승인해야만 코드에 반영되도록 제어해야 합니다.
* **구현 방법**:
* **Planner(설계)**, **Creator(구현)**, **Reviewers(검수)**의 페르소나와 역할을 엄격히 분리했습니다.
* 리뷰어 리포트 맨 마지막 단독 행에 `[VERDICT: PASS]` 또는 `[VERDICT: NOT PASS]` 토큰 입력을 규칙화하고, 파서가 모든 active 리뷰어의 만장일치 PASS를 확인할 때까지 자동으로 교정 수순을 반복(`run_loop.sh`)하도록 오케스트레이션했습니다.
### 📋 4) 작업 단위 구분 및 관리를 위한 태스크 데이터 구조화 (Job Schema & Task Management)
* **기능 부재 시 발생하는 문제 (구체적 장애 예시)**:
* *작업 유실 및 상태 추적 불가*: 작업 위임 데이터 모델이 정의되어 있지 않으면, Orchestrator가 어떤 에이전트에게 무슨 지시문(`brief.md`)을 전달했는지, 현재 상태가 진행 중(`running`)인지, 성공(`completed`)인지, 에러(`error`)인지 추적할 수 없어 작업이 공중에 뜬 '고아(Orphaned) 상태'로 방치됩니다.
* *중복 태스크 수락 및 Race Condition*: 동일한 작업을 둘 이상의 에이전트가 동시에 중복 수락(Claim)하여 서로의 코드를 덮어쓰거나 엉뚱한 결과물을 교차 전송하는 상충 장애가 일어납니다.
* **필요성**: 에이전트 간 분동(Delegation)되는 작업 단위를 정형화하고, 각 태스크의 생명주기 및 충돌 없는 클레임 관리를 보장해야 합니다.
* **구현 방법**:
* 모든 위임 태스크마다 고유 ID(`job_id`)를 발급하고 `.mam/jobs/<job_id>.json` 파일에 작업 스키마(주 작업자, 부여된 역할, 실행 타입, 보안 토큰, 생명주기 상태 등)를 명확히 데이터 구조화했습니다.
* `registry.py` 모듈을 통해 작업 등록 및 클레임 시 `fcntl` 파일 자문 잠금(Advisory Lock)을 적용하여 다중 세션 간 작업 중복 수락 경쟁을 원자적으로 제어했습니다.
### 📡 5) 비동기 메시징 및 이벤트 백플레인 (Async Messaging Backplane)
* **기능 부재 시 발생하는 문제 (구체적 장애 예시)**:
* *동기 블로킹 타임아웃 폭망*: 동기식 HTTP 요청으로 위임할 경우, 코드 구현 및 복잡한 리팩토링으로 10분 이상 소요되는 에이전트 작업을 기다리다가 오케스트레이터의 HTTP 커넥션 타임아웃이 발생하여 전체 작업이 붕괴됩니다.
* *블라인드 진행 장애*: 비동기 이벤트 채널이 없으면 에이전트가 백그라운드에서 무한 루프에 빠졌는지, 정상 동작 중인지 알 수 있는 진행 표기(`progress`)를 수신할 수 없습니다.
* **필요성**: 위임자와 실행자 간에 커넥션을 묶어두지 않고 비동기로 실시간 이벤트 및 중간 진행 상태를 주고받아야 합니다.
* **구현 방법**:
* 경량 메시징 표준인 **MQTT 브로커**를 백플레인으로 채택하고 `publish_event.py``job_subscriber.py`를 연결했습니다.
* 구독을 먼저 개시하는 *Subscribe-before-Publish* 규칙과 terminal 이벤트의 `retain=True` 설정으로 메시지 유실을 방지했습니다.
* Payload 내에 단조 증가 시퀀스 번호(`seq`)와 HMAC-SHA256 서명을 포함시켜 리플레이 공격 및 메시지 변조를 차단했습니다.
### 📸 6) 컨텍스트 보존 및 화면 유실 방지 (Pane Snapshotting & Markdown Bridge)
* **기능 부재 시 발생하는 문제 (구체적 장애 예시)**:
* *뷰포트 잘림으로 인한 디버그 정보 유실*: 터미널(TUI) 스크롤백 한계(예: 2000줄)로 인해 에이전트가 출력한 핵심 디버그 스택 트레이스나 긴 린트 에러 메시지가 상단으로 밀려 올라가 유실됩니다.
* *입력 깨짐 및 줄바꿈 돌발 실행*: 100줄이 넘는 복잡한 프롬프트나 한글/특수문자를 터미널 입력창에 직접 문자열로 보낼 경우, 멀티바이트 깨짐이나 줄바꿈 문자로 인해 에이전트가 입력을 받다 말고 중간에 명령어를 기습 실행하는 참사가 일어납니다.
* **필요성**: 장시간 터미널 출력 히스토리를 완벽하게 보존하고, 정교한 입력 데이터를 손상 없이 전달해야 합니다.
* **구현 방법**:
* **3단계 Pane Snapshotting** (작업 전, 루프 중 주기적 캡처, 작업 종료 후)을 적용해 터미널 출력을 파일로 상시 백업했습니다.
* 복잡한 지시문이나 리뷰 리포트는 TUI 입력창에 직접 타이핑하지 않고, `.mam/jobs/<id>/brief.md` 또는 `report.md` 파일로 작성한 뒤 에이전트에게 마크다운 파일 경로 포인터만 전송하는 **파일 기반 통신 구조**를 구축했습니다.
### 🔒 7) 이슈 트래킹 및 감사를 위한 원자적 상태 이력 관리 (Issue Tracking & Atomic Concurrency)
* **기능 부재 시 발생하는 문제 (구체적 장애 예시)**:
* *이슈 추적 불능 및 블랙박스 장애*: 에이전트가 작업 도중 에러(exit code non-zero, 프롬프트 잠금, API 실패 등)로 붕괴했을 때, 어떤 세션에서 무슨 이유로 정지되었는지 이력이 남지 않아 문제 발생 지점을 찾지 못하고 원인 분석이 불가능해집니다.
* *갱신 유실(Lost Update)로 인한 감사 이력 파손*: 에이전트 A가 에러 발생 원인과 감사 로그(`status=error`, `stop_reason`)를 덤프하는 순간, 다른 모니터링 프로세스가 1초 전의 예전 정상 스냅샷으로 파일/DB를 덮어씌워 장애 이력이 완전히 증발합니다.
* **필요성**: 에이전트 작업 도중 예외나 문제 발생 시, 이슈 트래커(Issue Tracker)처럼 어느 지점에서 어떤 원인(StackTrace, Pane Snapshot, Job Detail)으로 오류가 났는지 완벽히 추적(Traceability)할 수 있어야 하며, 이 감사 데이터가 동시 쓰기 충돌로 훼손되지 않도록 원자적으로 보호되어야 합니다.
* **구현 방법**:
* 모든 비동기 위임 작업 및 세션 상태 변경 시 고유 작업 ID(`job_id`), 담당 에이전트명, 발생 시각, 상세 원인(`detail`), 뷰포트 스냅샷(`pane.capture`)을 `.mam/jobs/<id>/` 디렉토리 및 `.mam/agent-sessions.db`에 감사 이력(Audit Trail)으로 즉시 저장하도록 설계했습니다.
* SQLite의 `BEGIN IMMEDIATE` 배타 트랜잭션과 `atomic_dump_yaml`(`os.replace`)을 조합하여, 장애 원인 추적 이력 데이터가 다중 프로세스 충돌로 덮어씌워지거나 손상되지 않도록 원자적 동시성을 완벽히 제어했습니다.
### 🚀 8) 자동화된 배포 환경 구성 및 프로비저닝 (Deployment Automation & Provisioning)
* **기능 부재 시 발생하는 문제 (구체적 장애 예시)**:
* *환경설정 파편화 및 접속 불능*: 서버나 에이전트 런타임마다 필수 환경변수(`HERDR_SESSION_NAME`, MQTT 엔드포인트 등)가 누락되거나 수동 설정 오차로 인해, 에이전트 간 통신이 안 되거나 타 격리 세션에 오접속하여 운영 데이터를 덮어쓰는 시스템 파손 발생.
* *배포 재현성 상실 및 임포트 에러*: 파이썬 가상환경(`.venv`)이나 필수 라이브러리(`paho-mqtt`, `pyyaml`) 설치가 누락된 채 에이전트 세션이 기동되어, 작업 위임 직후 `ModuleNotFoundError`로 파이프라인이 즉시 마비되는 장애.
* **필요성**: 개발, 테스트, 운영 환경 전체에서 동일한 구동 환경과 패키지 의존성을 신속히 자동 구축하고 배포 재현성을 보장해야 합니다.
* **구현 방법**:
* `deploy/generate-env.sh` 자동화 스크립트를 제공하여 `.env``HERDR_SESSION_NAME` 환경 변수를 1초 만에 자동 생성하고 시스템에 유기적으로 주입하도록 구성했습니다.
* `.venv` 가상환경 구성 및 `requirements.txt` 의존성 패키지 자동 프로비저닝 단계를 선행 통합하여 완벽히 재현 가능한 배포 체계를 구현했습니다.
---
## 4. 실전 트러블슈팅 및 튜닝 사례 (Troubleshooting Cases)
시스템 구축 및 검증 과정에서 직접 발굴하여 해결한 주요 실전 기술 사례입니다.
### 🛠️ 사례 1: Agy(Antigravity) 에이전트의 TOS/약관 동의 화면 차단 문제 해결
* **증상**: `agy` 에이전트를 격리 홈(`HOME=$root`)에서 실행했을 때, 구동 시마다 **"Terms of Service & Data Use" 약관 동의 및 테마 선택 마법사**가 출력되며 프롬프트 입력이 블로킹됨.
* **원인**: `agy` 에이전트가 약관 및 초기 설정 상태를 사용자 계정 디렉토리(`~/.gemini/antigravity`, `~/Library/Application Support/Antigravity`, macOS `Preferences` plist 등)에서 가져오는데, 기존 격리 프로비저닝 로직에서 해당 경로들의 심볼릭 링크 시딩이 누락됨.
* **해결**: `lib.sh::provision_isolation` 함수를 수정하여 macOS의 `Preferences`(`com.google.antigravity.plist` 등), `Application Support/Antigravity`, `Group Containers`와 Linux의 XDG 표준 경로(`~/.config/Antigravity`, `~/.local/share/Antigravity`)를 격리 홈 디렉토리로 자동 링킹하도록 확장함으로써 플랫폼에 관계없이 약관 안내 화면을 건너뛰도록 원천 해결함.
### 🛠️ 사례 2: TUI 입력 버퍼 감지 오탐(False-Positive) 완화
* **증상**: `cline`이나 `claude` 에이전트에 긴 프롬프트(git diff 포함 지시문 등)를 주입할 때 `send_keys_safe: paste not visible` 타임아웃 오류가 발생하며 작업 위임이 중단됨.
* **원인**: `send_keys_safe` 유틸리티가 입력 텍스트의 끝 24글자 마커(`marker_norm`)가 화면 뷰포트에 출현했는지 검증하는데, `cline` TUI 등 일부 환경은 긴 텍스트 입력 시 입력창 스크롤로 인해 마커가 화면 밖으로 밀려나 오탐(False-Positive)이 발생함.
* **해결**: `cline``claude` 세션의 경우 실제 페이스트는 성공했으나 스크롤아웃으로 인해 검증이 실패하는 특성을 고려해, 엄격한 마커 노출 검사를 생략하고 C-m(엔터) 제출 대기 루프로 직행하도록 `lib.sh` 검증 조건문을 완화하여 오케스트레이션 루프의 연속성을 확보함.
---
## 5. 결론 및 향후 발전 방향
본 프레임워크는 프로세스 격리, 파일 기반 원자적 레지스트리, 비동기 MQTT 메시징, 그리고 마크다운 파일 통신을 결합함으로써 **다종 AI 에이전트를 결합한 안정적이고 강인한 자율 협업 오케스트레이션**을 성공적으로 입증했습니다.
추후 가상환경 의존성 자동 점검 강화, HMAC 보안 인증 키 자동 교환(FW-N6), 그리고 NFS 환경에서의 파일 락 추상화 레이어 보완을 통해 더욱 확장성 높은 에이전트 플랫폼으로 진화할 수 있습니다.
-126
View File
@@ -1,126 +0,0 @@
# Multi-Agent Mux: Skill Features and Architecture
이 문서는 `multi-agent-mux` 워크스페이스 내에 구현된 6개의 개별 스킬 및 공통 라이브러리의 핵심 기능, 상태 머신, CLI 사양, 그리고 상호 연동 방식을 종합 정리한 명세입니다. 스킬 최적화 및 팩토링 작업의 기준서로 사용됩니다.
---
## 1. 아키텍처 개요 (Architecture Overview)
`multi-agent-mux`는 다중 자율 에이전트(Claude, Agy, Cline, Hermes 등)를 격리된 Tmux 세션 환경에서 관리하고 상호 통신할 수 있게 돕는 시스템입니다.
* **중앙 상태 레지스트리**: `.mam/agent-sessions.yaml` 및 동기화된 `.mam/agent-sessions.db` (SQLite3)
* **격리 소켓**: 독립된 tmux 서버 소켓 지정 구동 가능 (예: `multi-agent-mux` 서버)
* **이벤트 버스**: MQTT 프로토콜 기반의 실시간 작업 상태 비동기 관찰 (`multi-agent-mux-delegate-job`)
---
## 2. 공통 라이브러리: `lib.sh` (Common Library)
모든 스킬 스크립트가 로드하여 사용하는 핵심 공유 헬퍼 라이브러리입니다.
* **상태 파일 원자적 덤프 (`atomic_dump_yaml`)**:
* NFS(네트워크 파일 시스템) 감지 시 SQLite `PRAGMA journal_mode=DELETE` 폴백, 로컬 환경에서는 `PRAGMA journal_mode=WAL` 설정.
* 독점 잠금(`BEGIN IMMEDIATE`)을 활성화해 멀티프로세스 환경에서 Read-Modify-Write 데이터 유실(lost update race condition) 방지.
* 트랜잭션 커밋 완료 후 `.bak` 백업 파일 생성 및 임시파일 생성 후 `os.replace` 원자적 대체 기법 적용.
* **에이전트 세션 실재성 판단 (`*_exists` 함수군)**:
* `claude`: 프로젝트 디렉터리 하위 `<uuid>.jsonl` 존재성
* `agy`: `.gemini/antigravity-cli/conversations/<uuid>.db` 존재성
* `hermes`: `~/.hermes/state.db``sessions` 테이블 내 존재성 (SQLite 쿼리 검증)
* `cline`: `.cline/data/sessions/<uuid>/<uuid>.json` 존재성
* **세션 ID 해석 엔진 (`find_workspace_uuid` 분기 구조)**:
* **Tier 1 (YAML 직접 조회)**: YAML 내 기록된 에이전트별 전용 필드(`claude_session_id_own` 등) 조회.
* **Tier 2 (디스크 잔해 스캔)**: 워크스페이스 디렉터리(`cwd` / `workspace_root`)와 매칭되는 디스크 상의 세션 로그 중 가장 최근 수정일(`mtime`) 기준 정렬 후 최신 UUID 반환.
* **Tier 3 (아이덴티티 캐시)**: 레지스트리 상단 `agent_identities` 캐시 데이터 연동.
---
## 3. 스킬별 상세 핵심 기능 (Skill Specifications)
### 3.1. `multi-agent-mux-create` (생성 스킬)
* **용도**: 신규 에이전트 동작용 격리된 Tmux 컨테이너 생성 및 레지스트리 신규 등록.
* **핵심 기능**:
* **사전 기능 검증 (Preflight Check)**:
* `claude`: `claude auth status`를 통한 로그인 상태(`"loggedIn": true`) 검증
* `agy`: `agy models`를 통한 API 연동 정상 상태 검증
* `hermes`: `hermes status`를 통한 연동 상태 검증
* `cline`: `cline history --json` 동작 및 설정 상태 사전 검증
* **Tmux 세션 생성 및 초기화**: 에이전트별 최적화된 화면 크기(`-x 140 -y 40`) 및 작업 디렉터리(`-c`)를 적용해 세션 백그라운드 생성.
* **초기 상태 YAML 등록**: 사용자 필수 지정 역할(`--role`), `status: running`, `pane` 세부정보(인덱스, PID, CWD, CMD_FULL), 시작 명령 및 `mcp_attachments` 기록.
* **역할 불변성 보장**: 에이전트 생성 시 부여된 역할(`role`)은 사후 수정이 불가하며, 임의 변경 시도 시 데이터 검증(`atomic_dump_yaml`) 단계에서 예외 처리되어 방어됨.
* **TUI 로딩 동적 감지 (Readiness Gating)**: 고정 지연(`sleep 6`)을 탈피하여 각 에이전트별 시작 화면 출력 문자열(예: `Antigravity`, `Hermes`, `Claude`, `Cline`)을 실시간으로 감지(`capture-pane` 폴링)하여 로딩 완료 시점을 동적으로 감지함.
* **자동 온보딩 및 지시사항 주입 (Auto Onboarding & Instruction Injection)**:
* `--onboard` 플래그를 통해 신규 팀장 에이전트 구동 직후 워크스페이스 맥락(README.md, MULTI_AGENT_RULES.md, git status/diff, 타 세션 역할 분석)을 자율적으로 파악하도록 표준 온보딩 지시서 잡을 자동 생성 및 인젝션함.
* `--submit-job``--onboard` 지시서 입력을 `tmux` 페이스트 버퍼 및 엔터 키스트로크(`inject_instructions`)를 통해 에이전트 TUI 스트림에 자동 전달함.
### 3.2. `multi-agent-mux-resume` (재개 스킬)
* **용도**: 중지되었거나 유실된 에이전트의 이전 컨텍스트 그대로 Tmux 세션 및 TUI 연결 복원.
* **핵심 기능**:
* **세션 ID 해석 위임**: `lib.sh::find_workspace_uuid`을 구동하여 대상 워크스페이스의 UUID 확인.
* **세션 복원 기동**:
* `claude`: `claude --dangerously-skip-permissions -r <UUID>`
* `agy`: `agy --dangerously-skip-permissions --conversation <UUID>`
* `hermes`: `hermes --resume <UUID>`
* `cline`: `cline -i --id <UUID>`
* **TUI 바이패스 자동화 (Claude)**: 기동 직후 백그라운드에서 `Enter``Down``Enter` 키스트로크를 주입하여 권한 우회 및 복구 확인 대화상자 자동 수락.
* **동기화**: `update_yaml_resumed.sh`를 구동해 상태를 `running`으로 전이하고 기동 시점에 맞춘 하위 자식 PID 갱신 및 기존 종료 메타데이터 제거.
### 3.3. `multi-agent-mux-stop` (종료 스킬)
* **용도**: 세션을 안전하게 정리하고, 상태 및 UUID를 안전하게 저장 및 동기화.
* **핵심 기능**:
* **종료 전 TUI 스냅숏 저장**: `tmux capture-pane`을 수행해 최종 화면 상태를 `last_visible_status_at_termination` 필드에 보존.
* **다단계 Graceful 종료 프로토콜**:
1. TUI 안전 종료 키스트로크 주입 (`/exit` 또는 `Exit`) 후 3초 대기.
2. 생존 시 `tmux kill-session` 전송 및 5초 대기.
3. 최후 수단으로 감지된 자식 PID에 `kill -9` 전송.
* **디스크 소거 (--purge-conversation)**:
* `resumable``false`로 설정하고 상태를 `terminated`로 기록.
* 에이전트별 데이터 경로에 접근해 해당 세션 파일 파쇄.
* `claude`: `<proj-key>/<uuid>.jsonl` 삭제
* `agy`: `conversations/<uuid>.db``brain/<uuid>` 폴더 삭제
* `hermes`: `sessions/session_<uuid>.json` 삭제 및 `state.db` 내 이력 삭제 (내부 독자 커넥션 `hconn` 사용으로 상위 YAML DB 충돌 차단)
* `cline`: `~/.cline/data/sessions/<uuid>` 폴더 소거
### 3.4. `multi-agent-mux-delegate-job` (위임 스킬)
* **용도**: 타 에이전트에게 비동기적으로 작업을 위임하고, MQTT 이벤트로 실행 상태 관찰.
* **핵심 기능**:
* **작업 지시 유형 (Delegation Types)**:
* `direct` (기본값): 단일 타겟 세션 기동 후 작업 전달 및 대기.
* `loop` (협업 루프): 구현자(Worker)의 작업 완료 후 검토자(Reviewer)가 코드 검수를 수행하여 `"PASS"` 의견이 나올 때까지 작업 수정을 자동 반복 지시.
* `discuss` (토론/합의): 두 에이전트 간 공동 토론을 추진하여 최종 기획 및 계획 합의 도출.
* **MQTT 이벤트 규격**: `publish_event.py``job_subscriber.py`를 매핑하여 `started``permission_required``progress``completed`/`error` 상태 전이 추적 및 자동 이중 타임아웃 검사 (전체 실행 예산 3600초 + 120초 유휴 타임아웃).
* **감사 로그 기록**: `.mam/delegate_job_logs/<job_id>/``meta.json`, `status.json` 및 원시 NDJSON 형식의 `events.ndjson`을 영속 기록.
### 3.5. `multi-agent-mux-status` (현황 스킬)
* **용도**: 레지스트리를 읽어와 실행 중인 모든 에이전트의 구동 세션 현황을 즉시 표기.
* **핵심 기능**:
* **읽기 전용 안정성**: DB 수정이나 상태 전이 유발 없이 순수 조회만 수행.
* 실시간 tmux 프로세스 상태 정보와 YAML 간의 이름 매핑 정합성을 검증하여 콘솔에 요약 출력.
### 3.6. `multi-agent-mux-monitor` (화해 스킬)
* **용도**: 운영체제 Tmux 런타임과 YAML 레지스트리 데이터 불일치를 백그라운드 루프로 감지해 자동 화해(Reconciliation) 처리.
* **핵심 기능**:
* **Drift 감지 및 복구 매뉴얼**:
* **Drift A (Crash/죽은 세션)**: YAML 상 `running`이나 실제 tmux 프로세스가 죽은 경우 감지 ➔ 상태를 `terminated`로 격하 조정.
* **Drift B (새 세션 감지)**: YAML에 없으나 tmux 상에 임의로 떠 있는 `*-creator-*` 세션을 레지스트리에 자동 등록 및 자식 PID 정보 갱신.
* **Drift C (실시간 UUID 갱신)**: 새로 시작된 에이전트가 첫 명령을 받아 세션 ID를 생성했을 때, 디스크 상의 세션 로그 중 가장 수정시간이 일치하는 최신 UUID를 찾아 `*_conversation_id_own` 필드에 주입.
* **Drift D (캐시 정합성 점검)**: 레지스트리 및 캐시 상의 세션 UUID가 실제 디스크에 존재하는지 검사하여 소거된 세션을 리포트.
---
## 4. 에이전트 상태 머신 (Agent State Machine)
시스템 전반에 걸쳐 에이전트 세션은 아래 흐름을 따라 전이됩니다.
```mermaid
stateDiagram-v2
[*] --> running : multi-agent-mux-create / Drift B
running --> stopped : multi-agent-mux-stop (default)
running --> terminated : multi-agent-mux-stop (--purge-conversation) / Drift A
stopped --> running : multi-agent-mux-resume
terminated --> [*]
```
## 5. 최적화 및 팩토링 작업 시 주의 사항
1. **원자적 쓰기 무력화 금지**: `lib.sh`에 설정된 `atomic_dump_yaml`은 다중 에이전트 병렬 기동 시 데이터 꼬임을 막는 중추 역할을 합니다. DB 잠금 및 트랜잭션 흐름을 훼손하지 않아야 합니다.
2. **Cline 및 Claude의 TUI 입력 바인딩 유지**: 세션 재개나 중지 시, 각 에이전트가 내부적으로 사용하는 프롬프트 제어 명령어(예: `/exit`, `--id <session>`)의 세세한 차이를 유지해야 예외 없이 동작합니다.
3. **데이터베이스 변수 충돌 주의**: 서브셸 또는 인라인 Python 스크립트 실행 시 전역 SQLite 커넥션(`conn`)의 이름 공간을 절대 오염시키지 마십시오. (예: `stop_session.sh` 버그 재발 방지).
-49
View File
@@ -1,49 +0,0 @@
# Test Infrastructure Specification
## Test Philosophy
We adopt an **opaque-box, requirement-driven** testing philosophy for the tmux-to-herdr migration scripts.
This approach ensures that the test suite validates external behaviors, input/output contracts, and side-effects rather than asserting internal code structure or layout. The scripts are treated as black boxes that:
- Accept CLI arguments and environment variables.
- Query/interact with the `herdr` daemon through the `_herdr` shim (using mock executable interception).
- Perform state mutations inside `.mam/agent-sessions.yaml` and `.mam/agent-sessions.db`.
- Interact with background agent runners (`claude`, `agy`, `hermes`, `cline`).
This guarantees that our test assertions remain stable even if the script implementation details are refactored, as long as the functional requirements are met.
## Feature Inventory
The test suite is structured around five core features, mapping out verification checks across Tiers 1, 2, and 3:
| Feature | Tier 1 (Unit Checks) | Tier 2 (Component Checks) | Tier 3 (Integration Checks) |
|---|---|---|---|
| **Create Session** | - `derive_session_name` slug generation checks<br>- Workspace-to-slug character translation<br>- Invalid workspace path filtering<br>- Role parameter sanity validations<br>- Session override string generation | - Verification of state serialization to YAML schema<br>- Isolation home directory structure validation<br>- SQLite DB connection verification<br>- Concurrency check for database registration lock<br>- Database schema validation on write | - Spawn session execution with mock `herdr` and mock agent<br>- TUI readiness wait check<br>- Cleanup trap execution on crash<br>- Argument validation logic verification<br>- Isolation directory creation checks |
| **Resume Session** | - Workspace UUID resolution order unit tests<br>- CLI session ID parser validations<br>- Check prioritization (yaml file -> disk scan -> cache)<br>- Workspace path boundary check<br>- Empty UUID handling logic | - Configuration restore verification<br>- Environment overrides assertion<br>- Integrity check on retrieved SQLite metadata<br>- Validation of session ownership verification<br>- Config parsing for resume options | - Run `resume_session.sh` with mock agents<br>- Intercept agent command structure inside mock `herdr`<br>- Verify agent receives correct conversation UUID flag<br>- Invalid/missing UUID recovery path test<br>- Workspace resume CLI args verification |
| **Stop Session** | - Session name verification check<br>- Purge verification confirmations logic<br>- Command derivation format validation<br>- Timeout calculation helper tests<br>- Reason logging serializer test | - Safe folder path validation (shutil protection)<br>- Database status field mutation serialization<br>- Isolation folder cleanup check<br>- Lock file release checks on stop<br>- Concurrency handling of stop mutations | - Execute `stop_session.sh` with graceful key delivery (`/exit`)<br>- Fallback to forcible termination (`herdr kill-session`) check<br>- Fallback to PID termination (`kill -9`) verify<br>- Purge files verification on disk (`--purge-conversation`)<br>- CLI flag verification with yes/no confirmation |
| **Status Query** | - JSON converter unit tests<br>- Diff formatter text generators<br>- Output alignment tests<br>- Table grid column math verify<br>- CLI status argument parse tests | - Status read locks verification<br>- Parsing of drift status classifications<br>- Concurrency read protection test<br>- Registry YAML-to-JSON structural translation<br>- Verification of database read access checks | - Running `status.sh` with `--json`<br>- Verify console output match formatting rules<br>- Verify exit status codes on different states<br>- Integration test with `reconcile.sh` read-only diff emission<br>- Verify status command doesn't trigger side effects |
| **Monitor/Reconcile** | - Drift state classification unit tests<br>- Signature verification checks<br>- Subscription topic parsing tests<br>- MQTT message structure validator<br>- HMAC validation logic tests | - Concurrency lock checks (`.mam/monitor.lock`)<br>- Verify YAML and SQLite database reconciliation logic<br>- DB validation on drift updates<br>- HMAC signature signature verification<br>- SQLite journal mode fallback check (WAL vs DELETE) | - Execute `reconcile.sh` in single-pass mode (`--once`)<br>- MQTT subscription execution with mock messages<br>- Verify auto-termination of orphaned herdr sessions<br>- Verify auto-registration of untracked herdr sessions<br>- Lock contention handling testing |
## Test Architecture
The E2E testing framework is built using **pytest** and relies on two main pillars to ensure hermetic and reproducible test runs:
1. **Environment Sandboxing**:
All tests run inside a temporary, isolated directory structure provided by the pytest `tmp_path` fixture. The workspace environment is sandboxed by:
- Creating a temporary `.mam/` directory.
- Using the `monkeypatch` fixture to override `AGENT_SESSIONS_YAML` pointing to the sandboxed path.
- Overriding relevant environment variables (like `HOME`, `WORKSPACE_ROOT`, etc.) to prevent tests from modifying the developer's system state.
2. **Mock Binaries Interception**:
To prevent tests from interacting with external systems or relying on running daemons:
- A mock `herdr` script is dynamically generated and placed in a temporary bin folder, which is prepended to the system `PATH`. This mock binary reads/writes to a JSON file (`mock_herdr_state.json`) which acts as the control pane for tests to assert that `herdr` was called with correct arguments and return mocked outputs (session list, capture-pane output, exit codes).
- Mock agent binaries (`claude`, `agy`, `hermes`, `cline`) are also generated and prepended to `PATH`. They emulate successful login verification commands (e.g. `claude auth status`) and mock conversation UUID generation on disk.
## Real-World Application Scenarios (Tier 4)
We define five key E2E scenarios representing end-to-end user workflows:
1. **Standard Agent Session Lifecycle**: Spawning a new worker agent session via `create_session.sh`, verifying it is registered correctly in the YAML database, checking its status via `status.sh`, and then gracefully stopping it via `stop_session.sh`.
2. **Session Disconnect and Resume**: Creating a session, simulating a network disconnect/agent pane termination (updating herdr state), calling `resume_session.sh` to restore it using the workspace-scoped UUID, and asserting that the session returns to the active state in both herdr and the registry.
3. **Drift Detection and Auto-Reconciliation**: Artificially introducing drift (e.g. terminating a herdr session manually from the backend while keeping it registered in the YAML registry, or starting a herdr session outside the scripts), running `reconcile.sh --once`, and verifying that orphaned sessions are terminated and registry state is updated.
4. **Parallel Session Operations with flock Locking**: Simulating concurrent creation/stop script invocations to verify that SQLite flock transactions block lost update races, and that the registry data remains consistent.
5. **Multi-Agent Orchestrator Review Loop**: Running the orchestrator loop (`run_loop.sh`) where a worker agent and a reviewer agent are spawned, reviewer verdicts (`PASS` and `NOT PASS`) are processed, loops are iterated, and planner escalation is triggered on failure.
## Coverage Thresholds
To ensure the test suite is comprehensive, we define the following coverage thresholds:
- **Tier 1 (Unit Tests)**: Minimum >=5 unit tests per feature (total >=25 unit tests).
- **Tier 2 (Component Tests)**: Minimum >=5 component tests per feature (total >=25 component tests).
- **Tier 3 (Integration Tests)**: Pairwise combination testing covering CLI options and environment overrides for all features.
- **Tier 4 (E2E Scenarios)**: At least 5 full real-world scenario tests implemented and passing.
-129
View File
@@ -1,129 +0,0 @@
# Test Ready Report
## Test Runner
- **Command**: `.venv/bin/pytest tests/`
- **Expected**: All tests pass with exit code 0
## Coverage Summary
- **1. Feature Coverage (Tier 1)**: 29 tests
- **2. Boundary & Corner (Tier 2)**: 26 tests
- **3. Cross-Feature (Tier 3)**: 5 tests
- **4. Real-World Application (Tier 4)**: 5 tests
- **Sanity Checks**: 2 tests
- **Challenger/M2 Unit**: 7 tests
- **Total**: 74 tests
## Feature Checklist
### 1. Create Session
- **Tier 1 (Unit Checks)**
- [x] `derive_session_name` slug generation checks
- [x] Workspace-to-slug character translation
- [x] Invalid workspace path filtering
- [x] Role parameter sanity validations
- [x] Session override string generation
- **Tier 2 (Component Checks)**
- [x] Verification of state serialization to YAML schema
- [x] Isolation home directory structure validation
- [x] SQLite DB connection verification
- [x] Concurrency check for database registration lock
- [x] Database schema validation on write
- **Tier 3 (Integration Checks)**
- [x] Spawn session execution with mock `herdr` and mock agent
- [x] TUI readiness wait check
- [x] Cleanup trap execution on crash
- [x] Argument validation logic verification
- [x] Isolation directory creation checks
- **Tier 4 (Real-World Application Scenarios)**
- [x] Standard Agent Session Lifecycle E2E test (Scenario 1)
- [x] Parallel Session Operations with flock Locking E2E test (Scenario 4)
### 2. Resume Session
- **Tier 1 (Unit Checks)**
- [x] Workspace UUID resolution order unit tests
- [x] CLI session ID parser validations
- [x] Check prioritization (yaml file -> disk scan -> cache)
- [x] Workspace path boundary check
- [x] Empty UUID handling logic
- **Tier 2 (Component Checks)**
- [x] Configuration restore verification
- [x] Environment overrides assertion
- [x] Integrity check on retrieved SQLite metadata
- [x] Validation of session ownership verification
- [x] Config parsing for resume options
- **Tier 3 (Integration Checks)**
- [x] Run `resume_session.sh` with mock agents
- [x] Intercept agent command structure inside mock `herdr`
- [x] Verify agent receives correct conversation UUID flag
- [x] Invalid/missing UUID recovery path test
- [x] Workspace resume CLI args verification
- **Tier 4 (Real-World Application Scenarios)**
- [x] Session Disconnect and Resume E2E test (Scenario 2)
### 3. Stop Session
- **Tier 1 (Unit Checks)**
- [x] Session name verification check
- [x] Purge verification confirmations logic
- [x] Command derivation format validation
- [x] Timeout calculation helper tests
- [x] Reason logging serializer test
- **Tier 2 (Component Checks)**
- [x] Safe folder path validation (shutil protection)
- [x] Database status field mutation serialization
- [x] Isolation folder cleanup check
- [x] Lock file release checks on stop
- [x] Concurrency handling of stop mutations
- **Tier 3 (Integration Checks)**
- [x] Execute `stop_session.sh` with graceful key delivery (`/exit`)
- [x] Fallback to forcible termination (`herdr kill-session`) check
- [x] Fallback to PID termination (`kill -9`) verify
- [x] Purge files verification on disk (`--purge-conversation`)
- [x] CLI flag verification with yes/no confirmation
- **Tier 4 (Real-World Application Scenarios)**
- [x] Standard Agent Session Lifecycle E2E test (Scenario 1)
- [x] Parallel Session Operations with flock Locking E2E test (Scenario 4)
### 4. Status Query
- **Tier 1 (Unit Checks)**
- [x] JSON converter unit tests
- [x] Diff formatter text generators
- [x] Output alignment tests
- [x] Table grid column math verify
- [x] CLI status argument parse tests
- **Tier 2 (Component Checks)**
- [x] Status read locks verification
- [x] Parsing of drift status classifications
- [x] Concurrency read protection test
- [x] Registry YAML-to-JSON structural translation
- [x] Verification of database read access checks
- **Tier 3 (Integration Checks)**
- [x] Running `status.sh` with `--json`
- [x] Verify console output match formatting rules
- [x] Verify exit status codes on different states
- [x] Integration test with `reconcile.sh` read-only diff emission
- [x] Verify status command doesn't trigger side effects
- **Tier 4 (Real-World Application Scenarios)**
- [x] Standard Agent Session Lifecycle E2E test (Scenario 1)
- [x] Drift Detection and Auto-Reconciliation E2E test (Scenario 3)
### 5. Monitor/Reconcile
- **Tier 1 (Unit Checks)**
- [x] Drift state classification unit tests
- [x] Signature verification checks
- [x] Subscription topic parsing tests
- [x] MQTT message structure validator
- [x] HMAC validation logic tests
- **Tier 2 (Component Checks)**
- [x] Concurrency lock checks (`.mam/monitor.lock`)
- [x] Verify YAML and SQLite database reconciliation logic
- [x] DB validation on drift updates
- [x] HMAC signature signature verification
- [x] SQLite journal mode fallback check (WAL vs DELETE)
- **Tier 3 (Integration Checks)**
- [x] Execute `reconcile.sh` in single-pass mode (`--once`)
- [x] MQTT subscription execution with mock messages
- [x] Verify auto-termination of orphaned herdr sessions
- [x] Verify auto-registration of untracked herdr sessions
- [x] Lock contention handling testing
- **Tier 4 (Real-World Application Scenarios)**
- [x] Drift Detection and Auto-Reconciliation E2E test (Scenario 3)
+1 -1
View File
@@ -6,7 +6,7 @@ This directory contains packaging templates and installation scripts to deploy t
## 📁 Deployment Directory Structure ## 📁 Deployment Directory Structure
* **`install.sh`**: A self-contained, idempotent remote shell installer (via curl) that checks system requirements (`herdr`, `python3`), detects NFS/network filesystem mounts, sets up a local python virtual environment (`.venv`), and initializes environment configuration (`.env`). * **`install.sh`**: A self-contained, idempotent remote shell installer (via curl) that checks system requirements (`herdr`, `python3`), detects NFS/network filesystem mounts, sets up a local python virtual environment (`.venv`), and initializes environment configuration (`.mam.env`).
* **`install_mam.sh`**: A local-clone installer that copies rules/skills (`.agents/`), `AGENTS.md`, and sets up environment bootstrap on target projects. * **`install_mam.sh`**: A local-clone installer that copies rules/skills (`.agents/`), `AGENTS.md`, and sets up environment bootstrap on target projects.
* **`generate-env.sh`**: Environment configuration bootstrap helper. * **`generate-env.sh`**: Environment configuration bootstrap helper.
* **`INSTALL.md`**: Detailed installation and quick-start user manual. * **`INSTALL.md`**: Detailed installation and quick-start user manual.
+33 -11
View File
@@ -1,33 +1,54 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# generate-env.sh — create a local .env from the committed .env.example template. # generate-env.sh — create a local .mam.env from the committed .mam.env.example template.
# #
# Behaviour: # Behaviour:
# - .env absent → copy .env.example to .env, print the path. # - .mam.env absent → copy .mam.env.example to .mam.env, print the path.
# - .env present → no-op (leaves your edits intact), exit 0. # - .mam.env present → no-op (leaves your edits intact), exit 0.
# - .env present --force → overwrite .env from .env.example (backs up to .env.bak). # - .mam.env present --force → overwrite .mam.env from .mam.env.example (backs up to .mam.env.bak).
# - --migrate-legacy → explicitly rename existing .env to .mam.env if absent.
# #
# Paths are resolved relative to this script (repo root = parent of deploy/), # Paths are resolved relative to this script (repo root = parent of deploy/),
# so it works regardless of the caller's cwd. # so it works regardless of the caller's cwd.
# #
# Usage: deploy/generate-env.sh [--force] [-h|--help] # Usage: deploy/generate-env.sh [--force] [--migrate-legacy] [-h|--help]
set -euo pipefail set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SRC="$REPO_ROOT/.env.example" SRC="$REPO_ROOT/.mam.env.example"
DST="$REPO_ROOT/.env" DST="$REPO_ROOT/.mam.env"
LEGACY_ENV="$REPO_ROOT/.env"
FORCE=0 FORCE=0
MIGRATE=0
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--force) FORCE=1; shift ;; --force) FORCE=1; shift ;;
--migrate-legacy) MIGRATE=1; shift ;;
-h|--help) -h|--help)
echo "Usage: $0 [--force]" echo "Usage: $0 [--force] [--migrate-legacy]"
echo " Create .env from .env.example. --force overwrites an existing .env." echo " Create .mam.env from .mam.env.example."
echo " --force overwrites an existing .mam.env (backs up to .mam.env.bak)."
echo " --migrate-legacy explicitly renames an existing legacy .env to .mam.env."
exit 0 ;; exit 0 ;;
*) echo "ERROR: unknown arg: $1" >&2; echo "Usage: $0 [--force]" >&2; exit 2 ;; *) echo "ERROR: unknown arg: $1" >&2; echo "Usage: $0 [--force] [--migrate-legacy]" >&2; exit 2 ;;
esac esac
done done
if [ "$MIGRATE" = "1" ]; then
if [ -f "$DST" ]; then
echo "ERROR: cannot migrate: $DST already exists." >&2
exit 1
fi
if [ ! -f "$LEGACY_ENV" ]; then
echo "ERROR: cannot migrate: legacy file $LEGACY_ENV not found." >&2
exit 1
fi
mv -f "$LEGACY_ENV" "$DST"
chmod 0600 "$DST" 2>/dev/null || true
echo "migrated: $LEGACY_ENV -> $DST"
exit 0
fi
[ -f "$SRC" ] || { echo "ERROR: template not found: $SRC" >&2; exit 1; } [ -f "$SRC" ] || { echo "ERROR: template not found: $SRC" >&2; exit 1; }
if [ -f "$DST" ] && [ "$FORCE" != "1" ]; then if [ -f "$DST" ] && [ "$FORCE" != "1" ]; then
@@ -37,9 +58,10 @@ fi
if [ -f "$DST" ] && [ "$FORCE" = "1" ]; then if [ -f "$DST" ] && [ "$FORCE" = "1" ]; then
cp -p "$DST" "$DST.bak" cp -p "$DST" "$DST.bak"
echo "backed up existing .env -> $DST.bak" echo "backed up existing .mam.env -> $DST.bak"
fi fi
cp "$SRC" "$DST" cp "$SRC" "$DST"
chmod 0600 "$DST" 2>/dev/null || true
echo "created: $DST" echo "created: $DST"
echo "Next: edit $DST and fill in any secrets (look for 'replace_me')." echo "Next: edit $DST and fill in any secrets (look for 'replace_me')."
+303 -74
View File
@@ -1,22 +1,28 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ============================================================================== # ==============================================================================
# install.sh — Multi-Agent Mux (MAM) Orchestration Installer # install.sh — Multi-Agent Mux (MAM) Orchestration Installer (Rev.2)
# ==============================================================================
# Idempotent, robust installer to bootstrap MAM orchestration skills
# and Python backplane dependencies on any local workspace.
# ============================================================================== # ==============================================================================
set -euo pipefail set -euo pipefail
# --- Configuration & Defaults --- # --- Configuration & Defaults ---
TARGET_DIR="" TARGET_DIR=""
FORCE_REFRESH="${MAM_FORCE_REFRESH:-0}" REFRESH="${MAM_REFRESH:-1}"
OVERWRITE_CUSTOM="${MAM_OVERWRITE_CUSTOM:-0}"
VENV_NAME=".venv" VENV_NAME=".venv"
MIN_PYTHON_VERSION="3.9" MIN_PYTHON_VERSION="3.9"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-f|--force|--refresh-skills) -f|--force|--refresh-skills)
FORCE_REFRESH=1 REFRESH=1
shift
;;
--no-refresh|--offline)
REFRESH=0
shift
;;
--overwrite-custom)
OVERWRITE_CUSTOM=1
shift shift
;; ;;
-h|--help) -h|--help)
@@ -24,7 +30,9 @@ while [[ $# -gt 0 ]]; do
Usage: $0 [options] [target_dir] Usage: $0 [options] [target_dir]
Options: Options:
-f, --force, --refresh-skills Force fetch and refresh framework skills under .agents/skills/ -f, --force, --refresh-skills Fetch and refresh skills (now default)
--no-refresh, --offline Skip fetching latest assets if assets exist
--overwrite-custom Force overwrite user-modified skill files (backup still created)
-h, --help Show this help message -h, --help Show this help message
EOF EOF
exit 0 exit 0
@@ -64,7 +72,6 @@ check_cmd() {
check_cmd herdr check_cmd herdr
check_cmd python3 check_cmd python3
# Verify Python Version
PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
PYTHON_MAJOR="${MIN_PYTHON_VERSION%%.*}" PYTHON_MAJOR="${MIN_PYTHON_VERSION%%.*}"
PYTHON_MINOR="${MIN_PYTHON_VERSION##*.}" PYTHON_MINOR="${MIN_PYTHON_VERSION##*.}"
@@ -75,10 +82,8 @@ else
exit 1 exit 1
fi fi
# Verify PyYAML (needed by system python3 for atomic state writes)
if ! python3 -c "import yaml" &>/dev/null; then if ! python3 -c "import yaml" &>/dev/null; then
echo "❌ Error: 'PyYAML' is not installed in the system python3. Please install it first" >&2 echo "❌ Error: 'PyYAML' is not installed in system python3." >&2
echo " (e.g., 'pip3 install PyYAML' or 'sudo apt-get install python3-yaml')." >&2
exit 1 exit 1
fi fi
echo "✅ PyYAML (system dependency) detected." echo "✅ PyYAML (system dependency) detected."
@@ -90,8 +95,6 @@ cd "$TARGET_DIR"
REPO_URL="${MAM_REPO_URL:-https://git.godopu.com/tmpl/multi-agent-mux.git}" REPO_URL="${MAM_REPO_URL:-https://git.godopu.com/tmpl/multi-agent-mux.git}"
ARCHIVE_URL="${MAM_ARCHIVE_URL:-https://git.godopu.com/tmpl/multi-agent-mux/archive/main.tar.gz}" ARCHIVE_URL="${MAM_ARCHIVE_URL:-https://git.godopu.com/tmpl/multi-agent-mux/archive/main.tar.gz}"
# Helper to verify presence of all core runtime files.
# Keying off a set of core files helps detect and recover from partial/interrupted installations.
check_assets_present() { check_assets_present() {
local dir="${1:-.}" local dir="${1:-.}"
local core_files=( local core_files=(
@@ -109,7 +112,6 @@ check_assets_present() {
return 0 return 0
} }
# Helper to classify framework-owned skill definitions vs user-owned project assets
is_framework_owned() { is_framework_owned() {
case "$1" in case "$1" in
.agents/skills/*) return 0 ;; .agents/skills/*) return 0 ;;
@@ -117,108 +119,242 @@ is_framework_owned() {
esac esac
} }
# Fetch the orchestration assets if missing or if skill refresh is requested. # Fetch orchestration assets if REFRESH=1 or if core assets are missing.
# if [ "$REFRESH" -eq 1 ] || [ "${MAM_SKIP_VENV:-0}" -eq 1 ] || ! check_assets_present "."; then
# Safety model (FW-D1): we NEVER extract the repo archive directly into the
# target. Running inside an existing project must not overwrite the target's
# own files (README.md, FUTURE_WORKS.md, AGENTS.md, MULTI_AGENT_RULES.md) or litter
# it with development docs. Instead we stage the download into a throwaway temp dir,
# verify it, then copy runtime assets: framework skills (.agents/skills/*) are updated,
# while user-owned documents use per-file no-clobber guards so pre-existing target files win.
if [ "$FORCE_REFRESH" -eq 1 ] || ! check_assets_present "."; then
echo "📥 Staging orchestration assets from Gitea repository..." echo "📥 Staging orchestration assets from Gitea repository..."
STAGE_DIR="$(mktemp -d)" STAGE_DIR="$(mktemp -d)"
trap 'rm -rf "$STAGE_DIR"' EXIT trap 'rm -rf "$STAGE_DIR"' EXIT
if command -v git &>/dev/null; then FETCH_METHOD="archive"
if [ -d "$REPO_URL" ]; then
echo "🌐 Copying local working tree into a staging area..."
cp -R "$REPO_URL/." "$STAGE_DIR/"
FETCH_METHOD="local"
elif command -v git &>/dev/null; then
echo "🌐 Cloning repository (shallow) into a staging area..." echo "🌐 Cloning repository (shallow) into a staging area..."
git clone --depth 1 "$REPO_URL" "$STAGE_DIR" git clone --depth 1 "$REPO_URL" "$STAGE_DIR"
FETCH_METHOD="git"
elif command -v curl &>/dev/null; then elif command -v curl &>/dev/null; then
echo "🌐 Downloading and extracting archive into a staging area..." echo "🌐 Downloading and extracting archive into a staging area..."
curl -fsSL "$ARCHIVE_URL" | tar -xz --strip-components=1 -C "$STAGE_DIR" curl -fsSL "$ARCHIVE_URL" | tar -xz --strip-components=1 -C "$STAGE_DIR" \
--exclude='*/.agents/reports/*' --exclude='*/.agents/references/*' \
--exclude='*/MESSAGING.md' --exclude='*/BOOTSTRAP.md' --exclude='*/BOOTSTRAP.ko.md' 2>/dev/null || true
FETCH_METHOD="archive"
else else
echo "❌ Error: neither 'git' nor 'curl' is available to fetch the skills." >&2 echo "❌ Error: neither 'git' nor 'curl' is available to fetch the skills." >&2
exit 1 exit 1
fi fi
# Verify the staged tree before we trust and copy from it.
if ! check_assets_present "$STAGE_DIR"; then if ! check_assets_present "$STAGE_DIR"; then
echo "❌ Error: fetched source is missing core runtime assets. Aborting (no files copied)." >&2 echo "❌ Error: fetched source is missing core runtime assets. Aborting." >&2
exit 1 exit 1
fi fi
# Create metadata directory and initialize manifest before copying
mkdir -p .mam mkdir -p .mam
MANIFEST_FILE=".mam/install_manifest.txt" MANIFEST_FILE=".mam/install_manifest.txt"
touch "$MANIFEST_FILE" touch "$MANIFEST_FILE"
# Copy runtime assets (.agents/) into the target workspace. # Migrate legacy root layout for remove.sh and update.sh into .mam_deploy/ if owned
# Framework-owned skill files (.agents/skills/*) are updated/overwritten so that mkdir -p .mam_deploy
# latest skill definitions and metadata frontmatter take effect. for legacy_script in remove.sh update.sh; do
# User-owned documents (.agents/MULTI_AGENT_RULES*.md, .agents/INSTALL.md, etc.) use if [ -f "$legacy_script" ] && grep -Fqx "$legacy_script" "$MANIFEST_FILE" 2>/dev/null; then
# explicit no-clobber guards so pre-existing user files are untouched and unmanifested. mv -f "$legacy_script" ".mam_deploy/$legacy_script"
python3 -c '
import sys
path = sys.argv[1]
old_s = sys.argv[2]
new_s = sys.argv[3]
with open(path, "r") as f:
lines = f.readlines()
with open(path, "w") as f:
for line in lines:
if line.strip() == old_s:
f.write(new_s + "\n")
else:
f.write(line)
' "$MANIFEST_FILE" "$legacy_script" ".mam_deploy/$legacy_script" 2>/dev/null || true
fi
done
# Safe refresh & fingerprint checking logic
TS=$(date -u +%Y%m%dT%H%M%SZ)
PRESERVED_COUNT=0
MODIFIED_FILES=()
mkdir -p .agents mkdir -p .agents
( cd "$STAGE_DIR/.agents" && find . -type f -print ) | while IFS= read -r rel; do ( cd "$STAGE_DIR/.agents" && find . -type f -print ) | while IFS= read -r rel; do
case "$rel" in
./reports/*|./references/*) continue ;;
*.tmp|*.log|*.pyc|*/__pycache__/*) continue ;;
esac
dest=".agents/${rel#./}" dest=".agents/${rel#./}"
mkdir -p "$(dirname "$dest")" mkdir -p "$(dirname "$dest")"
if is_framework_owned "$dest"; then if is_framework_owned "$dest"; then
cp -f "$STAGE_DIR/.agents/$rel" "$dest" || { echo "❌ Error: Failed to copy $rel" >&2; exit 1; } # 3-way check using python3 inline
STAGING_FILE="$STAGE_DIR/.agents/$rel"
ACTION=$(python3 - "$dest" "$STAGING_FILE" ".mam/asset_hashes.txt" "$OVERWRITE_CUSTOM" <<'PY'
import sys, hashlib, os
target_path, staging_path, hash_db_path, force_overwrite = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] == "1"
def file_sha(path):
if not os.path.exists(path):
return None
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(65536):
h.update(chunk)
return h.hexdigest()
target_sha = file_sha(target_path)
staging_sha = file_sha(staging_path)
if target_sha is None:
print("COPY_NEW")
sys.exit(0)
if target_sha == staging_sha:
print("NO_OP")
sys.exit(0)
db_sha = None
if os.path.exists(hash_db_path):
with open(hash_db_path, "r") as f:
for line in f:
parts = line.strip().split(None, 1)
if len(parts) == 2 and parts[1] == target_path:
db_sha = parts[0]
break
if db_sha is None:
print("BOOTSTRAP_OVERWRITE")
elif target_sha == db_sha:
print("UPDATE_UNMODIFIED")
else:
if force_overwrite:
print("FORCE_OVERWRITE_CUSTOM")
else:
print("PRESERVE_CUSTOM")
PY
)
case "$ACTION" in
COPY_NEW|UPDATE_UNMODIFIED|BOOTSTRAP_OVERWRITE|FORCE_OVERWRITE_CUSTOM)
if [ "$ACTION" = "BOOTSTRAP_OVERWRITE" ] || [ "$ACTION" = "FORCE_OVERWRITE_CUSTOM" ]; then
BACKUP_DIR=".mam/skill-backups/$TS/$(dirname "$dest")"
mkdir -p "$BACKUP_DIR"
cp "$dest" "$BACKUP_DIR/"
fi
cp -f "$STAGING_FILE" "$dest"
if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then
echo "$dest" >> "$MANIFEST_FILE" echo "$dest" >> "$MANIFEST_FILE"
fi fi
;;
PRESERVE_CUSTOM)
BACKUP_DIR=".mam/skill-backups/$TS/$(dirname "$dest")"
mkdir -p "$BACKUP_DIR"
cp "$dest" "$BACKUP_DIR/"
echo "PRESERVED:$dest"
;;
NO_OP)
if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then
echo "$dest" >> "$MANIFEST_FILE"
fi
;;
esac
elif [ ! -e "$dest" ]; then elif [ ! -e "$dest" ]; then
cp "$STAGE_DIR/.agents/$rel" "$dest" || { echo "❌ Error: Failed to copy $rel" >&2; exit 1; } cp "$STAGE_DIR/.agents/$rel" "$dest"
if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then
echo "$dest" >> "$MANIFEST_FILE" echo "$dest" >> "$MANIFEST_FILE"
fi fi
fi fi
done | while IFS= read -r line; do
if [[ "$line" == PRESERVED:* ]]; then
echo "️ Local modification detected: ${line#PRESERVED:}" >&2
fi
done done
# Copy non-dev documents if they don't already exist. # Re-build asset_hashes.txt for all framework owned files
# We skip dev-specific docs like README.md, DONE.md, and FUTURE_WORKS.md. python3 - .mam/asset_hashes.txt <<'PY'
for doc in MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md AGENTS.md; do import os, hashlib, sys
hash_db_path = sys.argv[1]
hashes = []
for root, _, files in os.walk(".agents/skills"):
for file in files:
path = os.path.join(root, file)
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(65536):
h.update(chunk)
hashes.append(f"{h.hexdigest()} {path}\n")
with open(hash_db_path, "w") as f:
f.writelines(sorted(hashes))
PY
# Copy root docs (R-2 essential set)
ROOT_DOCS="AGENTS.md"
if [ "${MAM_INSTALL_DOCS:-minimal}" = "full" ]; then
ROOT_DOCS="AGENTS.md MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md"
fi
for doc in $ROOT_DOCS; do
if [ -f "$STAGE_DIR/$doc" ] && [ ! -e "$doc" ]; then if [ -f "$STAGE_DIR/$doc" ] && [ ! -e "$doc" ]; then
cp "$STAGE_DIR/$doc" . || { echo "❌ Error: Failed to copy $doc" >&2; exit 1; } cp "$STAGE_DIR/$doc" .
echo "$doc" >> "$MANIFEST_FILE" echo "$doc" >> "$MANIFEST_FILE"
fi fi
done done
if [ -f "$STAGE_DIR/deploy/remove.sh" ] && [ ! -e "remove.sh" ]; then # Install remove.sh and update.sh into .mam_deploy/
cp "$STAGE_DIR/deploy/remove.sh" remove.sh || { echo "❌ Error: Failed to copy remove.sh" >&2; exit 1; } mkdir -p .mam_deploy
chmod +x remove.sh if [ -f "$STAGE_DIR/deploy/remove.sh" ]; then
echo "remove.sh" >> "$MANIFEST_FILE" cp "$STAGE_DIR/deploy/remove.sh" .mam_deploy/remove.sh
chmod 0755 .mam_deploy/remove.sh
if ! grep -Fqx ".mam_deploy/remove.sh" "$MANIFEST_FILE" 2>/dev/null; then
echo ".mam_deploy/remove.sh" >> "$MANIFEST_FILE"
fi
fi fi
if [ -f "$STAGE_DIR/deploy/update.sh" ] && [ ! -e "update.sh" ]; then if [ -f "$STAGE_DIR/deploy/update.sh" ]; then
cp "$STAGE_DIR/deploy/update.sh" update.sh || { echo "❌ Error: Failed to copy update.sh" >&2; exit 1; } cp "$STAGE_DIR/deploy/update.sh" .mam_deploy/update.sh
chmod +x update.sh chmod 0755 .mam_deploy/update.sh
echo "update.sh" >> "$MANIFEST_FILE" if ! grep -Fqx ".mam_deploy/update.sh" "$MANIFEST_FILE" 2>/dev/null; then
echo ".mam_deploy/update.sh" >> "$MANIFEST_FILE"
fi
fi fi
if [ -f "$STAGE_DIR/.env.example" ] && [ ! -e ".env.example" ]; then if [ -f "$STAGE_DIR/.mam.env.example" ] && [ ! -e ".mam.env.example" ]; then
cp "$STAGE_DIR/.env.example" . || { echo "❌ Error: Failed to copy .env.example" >&2; exit 1; } cp "$STAGE_DIR/.mam.env.example" .
echo ".env.example" >> "$MANIFEST_FILE" echo ".mam.env.example" >> "$MANIFEST_FILE"
fi fi
# Ship the user manual into the target's .agents/ (consistent with install_mam.sh) if [ -f "$STAGE_DIR/deploy/INSTALL.md" ] && [ ! -e ".agents/INSTALL.md" ]; then
if [ -f "$STAGE_DIR/deploy/INSTALL.md" ]; then
mkdir -p .agents mkdir -p .agents
if [ ! -e ".agents/INSTALL.md" ]; then cp "$STAGE_DIR/deploy/INSTALL.md" .agents/INSTALL.md
cp "$STAGE_DIR/deploy/INSTALL.md" .agents/INSTALL.md || { echo "❌ Error: Failed to copy INSTALL.md" >&2; exit 1; }
echo ".agents/INSTALL.md" >> "$MANIFEST_FILE" echo ".agents/INSTALL.md" >> "$MANIFEST_FILE"
fi fi
# Record version stamp
STAGE_COMMIT="unknown"
if [ -d "$STAGE_DIR/.git" ]; then
STAGE_COMMIT=$(git -C "$STAGE_DIR" rev-parse HEAD 2>/dev/null || echo "unknown")
fi fi
cat <<EOF > .mam/version.txt
source=$REPO_URL
commit=$STAGE_COMMIT
fetched_at=$TS
method=$FETCH_METHOD
EOF
rm -rf "$STAGE_DIR" rm -rf "$STAGE_DIR"
trap - EXIT trap - EXIT
echo "✅ Skills staged into workspace (user documents and custom configs preserved)." echo "✅ Skills staged into workspace (user documents and custom configs preserved)."
fi fi
# Sanity check: verify all core files, not just a single one — an empty or
# incomplete layout would yield a silently broken install.
if ! check_assets_present "."; then if ! check_assets_present "."; then
echo "❌ Error: Core runtime assets missing after setup. Target layout might be invalid." >&2 echo "❌ Error: Core runtime assets missing after setup." >&2
exit 1 exit 1
fi fi
echo "✅ Orchestration skills present." echo "✅ Orchestration skills present."
@@ -226,20 +362,80 @@ echo "✅ Orchestration skills present."
echo "📂 Ensuring metadata directory structure (.mam/)..." echo "📂 Ensuring metadata directory structure (.mam/)..."
mkdir -p .mam/jobs .mam/delegate_job_logs mkdir -p .mam/jobs .mam/delegate_job_logs
# File permission lockdown on database directory (if owned by the current user to prevent multi-user system issues)
if [ -O .mam ]; then if [ -O .mam ]; then
chmod 0700 .mam chmod 0700 .mam
fi fi
# R-4: Manage .gitignore block (never put .gitignore in manifest)
MAM_GI_START="# >>> MAM managed block (managed by install.sh — do not edit) >>>"
MAM_GI_END="# <<< MAM managed block <<<"
if [ "${MAM_SKIP_GITIGNORE:-0}" != "1" ]; then
GI_CREATED=0
[ -e .gitignore ] || { touch .gitignore; GI_CREATED=1; }
if ! grep -q '^gitignore_created=' .mam/install_state 2>/dev/null; then
echo "gitignore_created=$GI_CREATED" >> .mam/install_state
fi
python3 - .gitignore "$MAM_GI_START" "$MAM_GI_END" <<'PY'
import sys, os
gi_path, start_marker, end_marker = sys.argv[1], sys.argv[2], sys.argv[3]
block_lines = [
start_marker + "\n",
"/.venv/\n",
"/.mam/\n",
"/.mam_deploy/\n",
"/.mam.env\n",
"/.mam.env.*\n",
"!/.mam.env.example\n",
"/.cache/multi-agent-mux-monitor/\n",
"/.mam-skill-backup.*/\n",
"CURRENT_JOB.md\n",
end_marker + "\n"
]
lines = []
if os.path.exists(gi_path):
with open(gi_path, "r") as f:
lines = f.readlines()
new_lines = []
in_block = False
block_inserted = False
for line in lines:
if line.strip() == start_marker:
in_block = True
if not block_inserted:
new_lines.extend(block_lines)
block_inserted = True
continue
if line.strip() == end_marker:
in_block = False
continue
if not in_block:
new_lines.append(line)
if not block_inserted:
if new_lines and not new_lines[-1].endswith("\n"):
new_lines[-1] += "\n"
new_lines.extend(block_lines)
with open(gi_path, "w") as f:
f.writelines(new_lines)
PY
fi
# --- 3. Check Network File System (NFS) Warnings --- # --- 3. Check Network File System (NFS) Warnings ---
echo "💾 Detecting file system mount type..." echo "💾 Detecting file system mount type..."
if command -v df &>/dev/null && command -v mount &>/dev/null; then if command -v df &>/dev/null && command -v mount &>/dev/null; then
MOUNTPOINT="$(df --output=target . 2>/dev/null | tail -1 || echo "")" MOUNTPOINT="$(df --output=target . 2>/dev/null | tail -1 || echo "")"
if [ -n "$MOUNTPOINT" ]; then if [ -n "$MOUNTPOINT" ]; then
if mount | grep -q "$MOUNTPOINT.*nfs\|$MOUNTPOINT.*cifs\|$MOUNTPOINT.*fuse.sshfs"; then if mount | grep -q "$MOUNTPOINT.*nfs\|$MOUNTPOINT.*cifs\|$MOUNTPOINT.*fuse.sshfs"; then
echo "⚠️ WARNING: Target directory is on a network filesystem (NFS/CIFS/SSHFS)." echo "⚠️ WARNING: Target directory is on a network filesystem."
echo " SQLite WAL journaling and file locks are UNRELIABLE on network storage."
echo " The sqlite3 registry will fall back to 'DELETE' journaling instead of WAL."
else else
echo "✅ File system supports WAL (Local storage detected)." echo "✅ File system supports WAL (Local storage detected)."
fi fi
@@ -247,6 +443,7 @@ if command -v df &>/dev/null && command -v mount &>/dev/null; then
fi fi
# --- 4. Python Virtual Environment Setup --- # --- 4. Python Virtual Environment Setup ---
if [ "${MAM_SKIP_VENV:-0}" != "1" ]; then
echo "🐍 Bootstrapping Python virtual environment (.venv)..." echo "🐍 Bootstrapping Python virtual environment (.venv)..."
if [ ! -d "$VENV_NAME" ]; then if [ ! -d "$VENV_NAME" ]; then
python3 -m venv "$VENV_NAME" python3 -m venv "$VENV_NAME"
@@ -255,29 +452,59 @@ else
echo "️ Virtual environment (.venv) already exists. Skipping creation." echo "️ Virtual environment (.venv) already exists. Skipping creation."
fi fi
# Activate virtual environment
# shellcheck disable=SC1091
source "$VENV_NAME"/bin/activate source "$VENV_NAME"/bin/activate
# Upgrade pip
pip install --upgrade pip pip install --upgrade pip
# Install requirements
REQ_FILE=".agents/skills/multi-agent-mux-delegate-job/requirements.txt" REQ_FILE=".agents/skills/multi-agent-mux-delegate-job/requirements.txt"
if [ -f "$REQ_FILE" ]; then if [ -f "$REQ_FILE" ]; then
echo "📦 Installing backplane dependencies from $REQ_FILE..." echo "📦 Installing backplane dependencies from $REQ_FILE..."
pip install -r "$REQ_FILE" pip install -r "$REQ_FILE"
echo "✅ Dependencies installed successfully." echo "✅ Dependencies installed successfully."
else else
echo "⚠️ WARNING: Could not find requirements file: $REQ_FILE"
echo " Installing default packages (paho-mqtt, pyyaml) manually..."
pip install "paho-mqtt>=2.0.0" pyyaml pip install "paho-mqtt>=2.0.0" pyyaml
fi fi
fi
# --- 5. Generate Environment Template --- # --- 5. Generate Environment Template ---
ENV_FILE=".env" ENV_FILE=".mam.env"
ENV_EXAMPLE=".env.example" ENV_EXAMPLE=".mam.env.example"
if [ ! -f "$ENV_FILE" ]; then
migrate_legacy_env() {
local manifest=".mam/install_manifest.txt"
[ -f ".mam.env" ] && return 0
[ -f ".env" ] || return 0
if [ "${MAM_LEGACY_ENV_OWNED:-0}" = "1" ] || { [ -f "$manifest" ] && grep -Fqx ".env" "$manifest" 2>/dev/null; }; then
mv -f ".env" ".mam.env"
chmod 0600 ".mam.env" 2>/dev/null || true
if [ -f "$manifest" ]; then
if grep -Fqx ".env" "$manifest" 2>/dev/null; then
python3 -c '
import sys
path = sys.argv[1]
with open(path, "r") as f:
lines = f.readlines()
with open(path, "w") as f:
for line in lines:
if line.strip() == ".env":
f.write(".mam.env\n")
else:
f.write(line)
' "$manifest" 2>/dev/null || true
else
echo ".mam.env" >> "$manifest"
fi
fi
echo "️ Legacy MAM config migrated: .env -> .mam.env"
else
echo "️ Existing .env left untouched (ownership unproven)."
echo " MAM will read it via the deprecated fallback."
echo " To migrate explicitly: deploy/generate-env.sh --migrate-legacy"
fi
}
migrate_legacy_env
if [ ! -f "$ENV_FILE" ] && [ ! -f ".env" ] && [ ! -f ".env.update-tmp" ]; then
if [ -f "$ENV_EXAMPLE" ]; then if [ -f "$ENV_EXAMPLE" ]; then
echo "📝 Creating configuration from $ENV_EXAMPLE..." echo "📝 Creating configuration from $ENV_EXAMPLE..."
cp "$ENV_EXAMPLE" "$ENV_FILE" cp "$ENV_EXAMPLE" "$ENV_FILE"
@@ -286,7 +513,6 @@ if [ ! -f "$ENV_FILE" ]; then
touch "$ENV_FILE" touch "$ENV_FILE"
fi fi
# Always append the active defaults to ensure they are set and not commented out
cat <<EOF >> "$ENV_FILE" cat <<EOF >> "$ENV_FILE"
# === Installer-applied active defaults === # === Installer-applied active defaults ===
@@ -297,14 +523,17 @@ MQTT_CLIENT_ID_PREFIX=mam-agent
HERDR_SERVER_NAME=default HERDR_SERVER_NAME=default
EOF EOF
chmod 0600 "$ENV_FILE" chmod 0600 "$ENV_FILE"
echo "✅ Config file .env initialized with chmod 0600." echo "✅ Config file .mam.env initialized with chmod 0600."
# Record the newly created .env in the manifest
mkdir -p .mam mkdir -p .mam
touch .mam/install_manifest.txt touch .mam/install_manifest.txt
echo "$ENV_FILE" >> .mam/install_manifest.txt echo "$ENV_FILE" >> .mam/install_manifest.txt
else else
if [ -f "$ENV_FILE" ]; then
echo "$ENV_FILE already exists. Skipping config override." echo "$ENV_FILE already exists. Skipping config override."
else
echo "️ Legacy environment detected. Preserved without shadowing."
fi
fi fi
echo "====================================================================" echo "===================================================================="
+69 -27
View File
@@ -114,15 +114,27 @@ log_info "Deploying orchestration rules & skills (.agents/)..."
mkdir -p "$TARGET_DIR/.agents" mkdir -p "$TARGET_DIR/.agents"
# Sync rules and skills, avoiding copying temporary or system files # Sync rules and skills, avoiding copying temporary or system files
# Exclude git histories, reports, logs or internal runtime cache if any # Exclude git histories, reports, references, logs or internal runtime cache
rsync -a --exclude='.git/' --exclude='/reports/' --exclude='*.log' --exclude='__pycache__/' --exclude='*.pyc' "$SRC_DIR/.agents/" "$TARGET_DIR/.agents/" rsync -a --exclude='.git/' --exclude='/reports/' --exclude='/references/' --exclude='*.log' --exclude='*.tmp' --exclude='__pycache__/' --exclude='*.pyc' "$SRC_DIR/.agents/" "$TARGET_DIR/.agents/"
log_ok "Deployed Rules and Skills under target's .agents/" log_ok "Deployed Rules and Skills under target's .agents/"
# Copy config templates and generate scripts (M-2) # Copy config templates and generate scripts (M-2)
if [ -f "$SRC_DIR/.env.example" ]; then if [ -f "$SRC_DIR/.mam.env.example" ]; then
cp "$SRC_DIR/.env.example" "$TARGET_DIR/.env.example" cp "$SRC_DIR/.mam.env.example" "$TARGET_DIR/.mam.env.example"
log_ok "Copied .env.example configuration template" log_ok "Copied .mam.env.example configuration template"
fi fi
# Deploy remove.sh and update.sh into .mam_deploy/ (R-3)
mkdir -p "$TARGET_DIR/.mam_deploy"
if [ -f "$SRC_DIR/deploy/remove.sh" ]; then
cp "$SRC_DIR/deploy/remove.sh" "$TARGET_DIR/.mam_deploy/remove.sh"
chmod 0755 "$TARGET_DIR/.mam_deploy/remove.sh"
fi
if [ -f "$SRC_DIR/deploy/update.sh" ]; then
cp "$SRC_DIR/deploy/update.sh" "$TARGET_DIR/.mam_deploy/update.sh"
chmod 0755 "$TARGET_DIR/.mam_deploy/update.sh"
fi
if [ -f "$SRC_DIR/deploy/generate-env.sh" ]; then if [ -f "$SRC_DIR/deploy/generate-env.sh" ]; then
mkdir -p "$TARGET_DIR/scripts" mkdir -p "$TARGET_DIR/scripts"
cp "$SRC_DIR/deploy/generate-env.sh" "$TARGET_DIR/scripts/generate-env.sh" cp "$SRC_DIR/deploy/generate-env.sh" "$TARGET_DIR/scripts/generate-env.sh"
@@ -160,32 +172,62 @@ else
log_ok "Guidelines AGENTS.md copied to project root." log_ok "Guidelines AGENTS.md copied to project root."
fi fi
# 4. Gitignore adjustments # 4. Gitignore adjustments (R-4: managed block)
log_info "Registering runtime isolation blocks in .gitignore..." log_info "Registering runtime isolation blocks in .gitignore..."
GITIGNORE="$TARGET_DIR/.gitignore" GITIGNORE="$TARGET_DIR/.gitignore"
MAM_PATTERN="/.mam/" MAM_GI_START="# >>> MAM managed block (managed by install.sh — do not edit) >>>"
VENV_PATTERN="/.venv/" MAM_GI_END="# <<< MAM managed block <<<"
if [ -f "$GITIGNORE" ]; then python3 - "$GITIGNORE" "$MAM_GI_START" "$MAM_GI_END" <<'PY'
# Register .mam/ if absent import sys, os
if grep -Eq '^/?\.mam/?$' "$GITIGNORE"; then
log_ok ".mam/ already registered in target's .gitignore."
else
echo -e "\n# Multi-Agent Mux (MAM) runtime databases and isolation cache\n$MAM_PATTERN" >> "$GITIGNORE"
log_ok "Appended /.mam/ registration to .gitignore."
fi
# Register .venv/ if absent gi_path, start_marker, end_marker = sys.argv[1], sys.argv[2], sys.argv[3]
if grep -Eq '^/?\.venv/?$' "$GITIGNORE"; then
log_ok ".venv/ already registered in target's .gitignore." block_lines = [
else start_marker + "\n",
echo -e "\n# Python virtual environment\n$VENV_PATTERN" >> "$GITIGNORE" "/.venv/\n",
log_ok "Appended /.venv/ registration to .gitignore." "/.mam/\n",
fi "/.mam_deploy/\n",
else "/.mam.env\n",
echo -e "# Multi-Agent Mux (MAM) runtime databases and isolation cache\n$MAM_PATTERN\n\n# Python virtual environment\n$VENV_PATTERN" > "$GITIGNORE" "/.mam.env.*\n",
log_ok "Created .gitignore with MAM and .venv exclusions." "!/.mam.env.example\n",
fi "/.cache/multi-agent-mux-monitor/\n",
"/.mam-skill-backup.*/\n",
"CURRENT_JOB.md\n",
end_marker + "\n"
]
lines = []
if os.path.exists(gi_path):
with open(gi_path, "r") as f:
lines = f.readlines()
new_lines = []
in_block = False
block_inserted = False
for line in lines:
if line.strip() == start_marker:
in_block = True
if not block_inserted:
new_lines.extend(block_lines)
block_inserted = True
continue
if line.strip() == end_marker:
in_block = False
continue
if not in_block:
new_lines.append(line)
if not block_inserted:
if new_lines and not new_lines[-1].endswith("\n"):
new_lines[-1] += "\n"
new_lines.extend(block_lines)
with open(gi_path, "w") as f:
f.writelines(new_lines)
PY
log_ok "Registered MAM managed block in .gitignore."
# 5. Python Virtual Environment Setup (F-1) # 5. Python Virtual Environment Setup (F-1)
log_info "Bootstrapping Python virtual environment (.venv) in target..." log_info "Bootstrapping Python virtual environment (.venv) in target..."
+143 -42
View File
@@ -11,6 +11,7 @@ set -euo pipefail
TARGET_DIR="" TARGET_DIR=""
FORCE=0 FORCE=0
PURGE_ENV=0 PURGE_ENV=0
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Parse arguments # Parse arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -31,8 +32,16 @@ while [[ $# -gt 0 ]]; do
done done
if [ -z "$TARGET_DIR" ]; then if [ -z "$TARGET_DIR" ]; then
if [ "$(basename "$SCRIPT_DIR")" = ".mam_deploy" ]; then
TARGET_DIR="$(dirname "$SCRIPT_DIR")"
else
TARGET_DIR="$(pwd)" TARGET_DIR="$(pwd)"
fi fi
else
if [ "$(basename "$TARGET_DIR")" = ".mam_deploy" ]; then
TARGET_DIR="$(dirname "$TARGET_DIR")"
fi
fi
echo "====================================================================" echo "===================================================================="
echo "⚡ Starting Multi-Agent Mux (MAM) Uninstallation" echo "⚡ Starting Multi-Agent Mux (MAM) Uninstallation"
@@ -46,6 +55,11 @@ fi
cd "$TARGET_DIR" cd "$TARGET_DIR"
GI_CREATED=0
if [ -f ".mam/install_state" ]; then
GI_CREATED=$(grep '^gitignore_created=' .mam/install_state 2>/dev/null | cut -d= -f2 || echo 0)
fi
# 1. Non-interactive input safety guard (set -e read crash prevention) # 1. Non-interactive input safety guard (set -e read crash prevention)
if [ ! -t 0 ] && [ $FORCE -eq 0 ]; then if [ ! -t 0 ] && [ $FORCE -eq 0 ]; then
echo "❌ Error: Non-interactive terminal detected. Please run with -y/--yes/--force." >&2 echo "❌ Error: Non-interactive terminal detected. Please run with -y/--yes/--force." >&2
@@ -66,7 +80,6 @@ if [ -f "$MANIFEST_FILE" ]; then
fi fi
done < "$MANIFEST_FILE" done < "$MANIFEST_FILE"
else else
# Fallback to the core MAM directories to check if any exist
fallback_assets=( fallback_assets=(
".agents/skills/lib.sh" ".agents/skills/lib.sh"
".agents/skills/multi-agent-mux-create" ".agents/skills/multi-agent-mux-create"
@@ -77,6 +90,7 @@ else
".agents/skills/multi-agent-mux-stop" ".agents/skills/multi-agent-mux-stop"
".venv" ".venv"
".mam" ".mam"
".mam_deploy"
) )
for asset in "${fallback_assets[@]}"; do for asset in "${fallback_assets[@]}"; do
if [ -e "$asset" ] || [ -h "$asset" ]; then if [ -e "$asset" ] || [ -h "$asset" ]; then
@@ -95,7 +109,6 @@ fi
if [ $FORCE -eq 0 ]; then if [ $FORCE -eq 0 ]; then
echo "⚠️ WARNING: This will permanently remove the MAM orchestration skills, " echo "⚠️ WARNING: This will permanently remove the MAM orchestration skills, "
echo " virtual environment (.venv), local metadata (.mam), and docs." echo " virtual environment (.venv), local metadata (.mam), and docs."
echo " (Your own custom files inside .agents/ will NOT be touched)."
if ! read -p "❓ Are you sure you want to proceed? [y/N]: " -r response; then if ! read -p "❓ Are you sure you want to proceed? [y/N]: " -r response; then
response="n" response="n"
@@ -114,18 +127,51 @@ delete_asset() {
fi fi
} }
# Check for modified skills before deleting
TS=$(date -u +%Y%m%dT%H%M%SZ)
python3 - ".mam/asset_hashes.txt" "$TS" <<'PY' 2>/dev/null || true
import sys, os, hashlib, shutil
hash_db_path = sys.argv[1]
ts = sys.argv[2]
if not os.path.exists(hash_db_path):
sys.exit(0)
modified = []
with open(hash_db_path, "r") as f:
for line in f:
parts = line.strip().split(None, 1)
if len(parts) == 2:
expected_hash, path = parts[0], parts[1]
if os.path.exists(path):
h = hashlib.sha256()
with open(path, "rb") as pf:
while chunk := pf.read(65536):
h.update(chunk)
if h.hexdigest() != expected_hash:
modified.append(path)
if modified:
backup_dir = f".mam-skill-backup.{ts}"
os.makedirs(backup_dir, exist_ok=True)
for p in modified:
dest = os.path.join(backup_dir, p)
os.makedirs(os.path.dirname(dest), exist_ok=True)
shutil.copy2(p, dest)
print(f"💾 Preserved {len(modified)} modified skill file(s) under {backup_dir}")
PY
# 2. Uninstall files using the manifest if present # 2. Uninstall files using the manifest if present
if [ ${#manifest_files[@]} -gt 0 ]; then if [ ${#manifest_files[@]} -gt 0 ]; then
echo "📜 Manifest found. Reversing installer-created files..." echo "📜 Manifest found. Reversing installer-created files..."
for f in ${manifest_files[@]+"${manifest_files[@]}"}; do for f in ${manifest_files[@]+"${manifest_files[@]}"}; do
# Skip .env and remove.sh for now, they are handled separately if [ "$f" = ".env" ] || [ "$f" = ".mam.env" ] || [ "$f" = "remove.sh" ] || [ "$f" = ".mam_deploy/remove.sh" ]; then
if [ "$f" = ".env" ] || [ "$f" = "remove.sh" ]; then
continue continue
fi fi
delete_asset "$f" delete_asset "$f"
done done
else else
# Fallback: Delete MAM skills manually (only if manifest is missing)
echo "⚠️ No manifest found. Deleting standard MAM skills..." echo "⚠️ No manifest found. Deleting standard MAM skills..."
delete_asset ".agents/skills/lib.sh" delete_asset ".agents/skills/lib.sh"
delete_asset ".agents/skills/multi-agent-mux-create" delete_asset ".agents/skills/multi-agent-mux-create"
@@ -136,75 +182,130 @@ if [ ${#manifest_files[@]} -gt 0 ]; then
delete_asset ".agents/skills/multi-agent-mux-stop" delete_asset ".agents/skills/multi-agent-mux-stop"
fi fi
# 3. Clean up empty parent directories under .agents recursively to avoid littering
if [ -d ".agents" ]; then if [ -d ".agents" ]; then
find .agents -depth -type d -exec rmdir {} + 2>/dev/null || true find .agents -depth -type d -exec rmdir {} + 2>/dev/null || true
fi fi
# 4. Remove virtual environment, monitor cache, and metadata database # Clean up .gitignore managed block (C8)
MAM_GI_START="# >>> MAM managed block (managed by install.sh — do not edit) >>>"
MAM_GI_END="# <<< MAM managed block <<<"
if [ -f .gitignore ]; then
python3 - .gitignore "$MAM_GI_START" "$MAM_GI_END" "$GI_CREATED" <<'PY'
import sys, os
gi_path, start_marker, end_marker, gi_created = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] == "1"
if not os.path.exists(gi_path):
sys.exit(0)
with open(gi_path, "r") as f:
lines = f.readlines()
new_lines = []
in_block = False
block_found = False
for line in lines:
if line.strip() == start_marker:
in_block = True
block_found = True
continue
if line.strip() == end_marker:
in_block = False
continue
if not in_block:
new_lines.append(line)
if block_found:
content = "".join(new_lines).strip()
if gi_created and not content:
os.remove(gi_path)
else:
with open(gi_path, "w") as f:
f.writelines(new_lines)
PY
fi
delete_asset ".venv" delete_asset ".venv"
delete_asset ".cache/multi-agent-mux-monitor" delete_asset ".cache/multi-agent-mux-monitor"
delete_asset ".mam" # Deletes manifest file too delete_asset ".mam"
for env_name in ".mam.env" ".env"; do
[ -f "$env_name" ] || continue
# 5. Clean up .env file (Only if created by installer, or forced with --purge-env)
# If .env is in manifest, it means MAM created it.
env_created_by_mam=0 env_created_by_mam=0
for f in ${manifest_files[@]+"${manifest_files[@]}"}; do for f in ${manifest_files[@]+"${manifest_files[@]}"}; do
if [ "$f" = ".env" ]; then if [ "$f" = "$env_name" ]; then
env_created_by_mam=1 env_created_by_mam=1
break break
fi fi
done done
if [ -f ".env" ]; then
should_delete_env=0 should_delete_env=0
if [ $PURGE_ENV -eq 1 ]; then if [ $PURGE_ENV -eq 1 ]; then
should_delete_env=1 should_delete_env=1
elif [ $env_created_by_mam -eq 1 ]; then elif [ $env_created_by_mam -eq 1 ] && [ $FORCE -eq 0 ]; then
# Even if MAM created it, ask or rename to backup to prevent loss of custom secrets if ! read -p " MAM-created '$env_name' found. Delete it? (Saying No preserves it) [y/N]: " -r env_response; then
if [ $FORCE -eq 1 ]; then
should_delete_env=1
else
if ! read -p "❓ MAM-created '.env' found. Delete it? (Saying No preserves it) [y/N]: " -r env_response; then
env_response="n" env_response="n"
fi fi
if [[ "$env_response" =~ ^[yY](es)?$ ]]; then if [[ "$env_response" =~ ^[yY](es)?$ ]]; then
should_delete_env=1 should_delete_env=1
fi fi
fi fi
fi
if [ $should_delete_env -eq 1 ]; then if [ $should_delete_env -eq 1 ]; then
delete_asset ".env" delete_asset "$env_name"
else else
if [ $env_created_by_mam -eq 1 ]; then if [ $env_created_by_mam -eq 1 ]; then
backup_name=".env.mam-backup" already_preserved=0
if [ -e "$backup_name" ]; then for existing in "${env_name}.mam-backup" "${env_name}".mam-backup.*; do
backup_name=".env.mam-backup.$(date +%Y%m%d%H%M%S)" [ -f "$existing" ] || continue
fi if cmp -s "$env_name" "$existing" 2>/dev/null; then
echo "💾 Backing up .env configuration to $backup_name..." already_preserved=1
mv ".env" "$backup_name" rm -f "$env_name"
else echo "️ '$env_name' is already preserved in $existing (no duplicate created)."
echo "️ Preserving user-owned .env configuration."
fi
fi
fi
# 6. Remove uninstaller file itself (if we are in the target root)
# Simple check: only delete remove.sh if it is recorded in the manifest
remove_in_manifest=0
for f in ${manifest_files[@]+"${manifest_files[@]}"}; do
if [ "$f" = "remove.sh" ]; then
remove_in_manifest=1
break break
fi fi
done done
if [ -f "remove.sh" ] && [ $remove_in_manifest -eq 1 ]; then if [ $already_preserved -eq 0 ]; then
echo "🗑️ Removing uninstaller: remove.sh" slot="${env_name}.mam-backup"
# Self-delete is the final action if [ -e "$slot" ]; then
rm -f "remove.sh" slot="${env_name}.mam-backup.$(date +%Y%m%d%H%M%S)"
n=1
while [ -e "$slot" ]; do
slot="${env_name}.mam-backup.$(date +%Y%m%d%H%M%S)-$n"
n=$((n + 1))
done
fi fi
mv "$env_name" "$slot"
echo "💾 Backed up $env_name -> $slot"
fi
else
echo "️ Preserving user-owned $env_name configuration."
fi
fi
done
# Remove uninstaller file(s)
for self in ".mam_deploy/remove.sh" "remove.sh"; do
[ -f "$self" ] || continue
in_manifest=0
for f in ${manifest_files[@]+"${manifest_files[@]}"}; do
if [ "$f" = "$self" ]; then
in_manifest=1
break
fi
done
if [ $in_manifest -eq 1 ] || [ $FORCE -eq 1 ]; then
echo "🗑️ Removing uninstaller: $self"
rm -f "$self"
fi
done
delete_asset ".mam_deploy/update.sh"
rmdir .mam_deploy 2>/dev/null || true
echo "====================================================================" echo "===================================================================="
echo "🎉 Uninstallation complete!" echo "🎉 Uninstallation complete!"
+76 -11
View File
@@ -9,6 +9,7 @@ set -euo pipefail
TARGET_DIR="" TARGET_DIR=""
FORCE=0 FORCE=0
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Parse arguments # Parse arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -25,8 +26,15 @@ while [[ $# -gt 0 ]]; do
done done
if [ -z "$TARGET_DIR" ]; then if [ -z "$TARGET_DIR" ]; then
TARGET_DIR="$(pwd)" if [ "$(basename "$SCRIPT_DIR")" = ".mam_deploy" ]; then
TARGET_DIR="$(dirname "$SCRIPT_DIR")"
else else
TARGET_DIR="$(pwd)"
fi
else
if [ "$(basename "$TARGET_DIR")" = ".mam_deploy" ]; then
TARGET_DIR="$(dirname "$TARGET_DIR")"
fi
if [ ! -d "$TARGET_DIR" ]; then if [ ! -d "$TARGET_DIR" ]; then
echo "❌ Error: Target directory '$TARGET_DIR' does not exist." >&2 echo "❌ Error: Target directory '$TARGET_DIR' does not exist." >&2
exit 1 exit 1
@@ -41,8 +49,16 @@ echo "===================================================================="
cd "$TARGET_DIR" cd "$TARGET_DIR"
# 1. Verification of existing install # 1. Verification of existing install (B-2: dual resolution)
if [ ! -f "remove.sh" ]; then REMOVER=""
for cand in ".mam_deploy/remove.sh" "remove.sh"; do
if [ -f "$cand" ]; then
REMOVER="$cand"
break
fi
done
if [ -z "$REMOVER" ]; then
echo "❌ Error: No MAM installation (remove.sh) found in '$TARGET_DIR'." >&2 echo "❌ Error: No MAM installation (remove.sh) found in '$TARGET_DIR'." >&2
echo " Please run install.sh first to set up the workspace." >&2 echo " Please run install.sh first to set up the workspace." >&2
exit 1 exit 1
@@ -66,12 +82,29 @@ if [ $FORCE -eq 0 ]; then
fi fi
fi fi
# 1.5 Pre-capture legacy ownership before remove.sh deletes the manifest (M-4)
MAM_LEGACY_ENV_OWNED=0
if [ -f ".mam/install_manifest.txt" ] && grep -Fqx ".env" ".mam/install_manifest.txt" 2>/dev/null; then
MAM_LEGACY_ENV_OWNED=1
fi
export MAM_LEGACY_ENV_OWNED
# 2. Stage backups of user configurations and metadata to prevent deletion # 2. Stage backups of user configurations and metadata to prevent deletion
echo "💾 Backing up configuration and database..." echo "💾 Backing up configuration and database..."
HAS_ENV=0 HAS_ENV=0
if [ -f ".env" ]; then ENV_BACKUP_SRC=""
ENV_BACKUP_TMP=""
if [ -f ".mam.env" ]; then
HAS_ENV=1 HAS_ENV=1
mv ".env" ".env.update-tmp" ENV_BACKUP_SRC=".mam.env"
ENV_BACKUP_TMP=".mam.env.update-tmp"
mv ".mam.env" "$ENV_BACKUP_TMP"
elif [ -f ".env" ]; then
HAS_ENV=1
ENV_BACKUP_SRC=".env"
ENV_BACKUP_TMP=".env.update-tmp"
mv ".env" "$ENV_BACKUP_TMP"
fi fi
HAS_MAM=0 HAS_MAM=0
@@ -100,13 +133,23 @@ if [ -d ".mam" ]; then
if [ -f ".mam/install_manifest.txt" ]; then if [ -f ".mam/install_manifest.txt" ]; then
cp -f .mam/install_manifest.txt .mam.update-tmp/ cp -f .mam/install_manifest.txt .mam.update-tmp/
fi fi
# C12: Copy MAM state files across update cycle
for st in install_state asset_hashes.txt version.txt; do
if [ -f ".mam/$st" ]; then
cp -f ".mam/$st" .mam.update-tmp/
fi
done
if [ -d ".mam/skill-backups" ]; then
mkdir -p .mam.update-tmp/skill-backups
cp -rf .mam/skill-backups/* .mam.update-tmp/skill-backups/ 2>/dev/null || true
fi
fi fi
# Define trap to restore backup files on failure # Define trap to restore backup files on failure
restore_on_failure() { restore_on_failure() {
echo "❌ Update failed. Reverting configuration and database to previous state..." echo "❌ Update failed. Reverting configuration and database to previous state..."
if [ $HAS_ENV -eq 1 ] && [ -f ".env.update-tmp" ]; then if [ $HAS_ENV -eq 1 ] && [ -n "$ENV_BACKUP_TMP" ] && [ -f "$ENV_BACKUP_TMP" ]; then
mv -f ".env.update-tmp" ".env" 2>/dev/null || true mv -f "$ENV_BACKUP_TMP" "$ENV_BACKUP_SRC" 2>/dev/null || true
fi fi
if [ $HAS_MAM -eq 1 ] && [ -d ".mam.update-tmp" ]; then if [ $HAS_MAM -eq 1 ] && [ -d ".mam.update-tmp" ]; then
# Revert to old database/jobs backup by restoring .mam directory # Revert to old database/jobs backup by restoring .mam directory
@@ -122,6 +165,15 @@ restore_on_failure() {
if [ -f ".mam.update-tmp/install_manifest.txt" ]; then if [ -f ".mam.update-tmp/install_manifest.txt" ]; then
cp -f .mam.update-tmp/install_manifest.txt .mam/ 2>/dev/null || true cp -f .mam.update-tmp/install_manifest.txt .mam/ 2>/dev/null || true
fi fi
for st in install_state asset_hashes.txt version.txt; do
if [ -f ".mam.update-tmp/$st" ]; then
cp -f ".mam.update-tmp/$st" .mam/ 2>/dev/null || true
fi
done
if [ -d ".mam.update-tmp/skill-backups" ]; then
mkdir -p .mam/skill-backups
cp -rf .mam.update-tmp/skill-backups/* .mam/skill-backups/ 2>/dev/null || true
fi
rm -rf .mam.update-tmp 2>/dev/null || true rm -rf .mam.update-tmp 2>/dev/null || true
fi fi
} }
@@ -131,7 +183,7 @@ trap restore_on_failure EXIT
echo "🗑️ Removing existing installation..." echo "🗑️ Removing existing installation..."
# remove.sh will run in manifest mode because .mam/install_manifest.txt is still present. # remove.sh will run in manifest mode because .mam/install_manifest.txt is still present.
# It will delete .agents/, documents, scripts, .venv, and .mam folder. # It will delete .agents/, documents, scripts, .venv, and .mam folder.
bash remove.sh --force bash "$REMOVER" --force "$TARGET_DIR"
# 4. Fetch and run the latest installer from Gitea # 4. Fetch and run the latest installer from Gitea
echo "📥 Fetching and running the latest installer..." echo "📥 Fetching and running the latest installer..."
@@ -151,9 +203,13 @@ trap - EXIT
# 5. Restore backups of configuration and database # 5. Restore backups of configuration and database
echo "🔄 Restoring configuration and database..." echo "🔄 Restoring configuration and database..."
if [ $HAS_ENV -eq 1 ]; then if [ $HAS_ENV -eq 1 ] && [ -n "$ENV_BACKUP_TMP" ] && [ -f "$ENV_BACKUP_TMP" ]; then
# Overwrite the default .env created by installer (if any) with the user's backup if [ "$MAM_LEGACY_ENV_OWNED" = "1" ] && [ "$ENV_BACKUP_SRC" = ".env" ] && [ ! -f ".mam.env" ]; then
mv -f ".env.update-tmp" ".env" mv -f "$ENV_BACKUP_TMP" ".mam.env"
chmod 0600 ".mam.env" 2>/dev/null || true
else
mv -f "$ENV_BACKUP_TMP" "$ENV_BACKUP_SRC"
fi
fi fi
if [ $HAS_MAM -eq 1 ]; then if [ $HAS_MAM -eq 1 ]; then
@@ -173,6 +229,15 @@ if [ $HAS_MAM -eq 1 ]; then
mkdir -p .mam/delegate_job_logs mkdir -p .mam/delegate_job_logs
cp -rf .mam.update-tmp/delegate_job_logs/* .mam/delegate_job_logs/ cp -rf .mam.update-tmp/delegate_job_logs/* .mam/delegate_job_logs/
fi fi
for st in install_state asset_hashes.txt version.txt; do
if [ -f ".mam.update-tmp/$st" ]; then
cp -f ".mam.update-tmp/$st" .mam/
fi
done
if [ -d ".mam.update-tmp/skill-backups" ]; then
mkdir -p .mam/skill-backups
cp -rf .mam.update-tmp/skill-backups/* .mam/skill-backups/ 2>/dev/null || true
fi
rm -rf ".mam.update-tmp" rm -rf ".mam.update-tmp"
fi fi
fi fi
-77
View File
@@ -1,77 +0,0 @@
# [보고서] MAM 위임 도구의 역할(Role) 지정 옵션 누락 이슈 분석
본 문서는 멀티 에이전트 오케스트레이션 프레임워크(`multi-agent-mux`)의 핵심 CLI 도구인 `multi-agent-mux-delegate-job`에서 세션의 역할(Role)을 지정할 수 있는 옵션이 누락되어 발생하는 정합성 충돌 문제와 이에 대한 원인 분석 및 해결 방안을 정의합니다.
---
## 1. 문제가 발생한 정확한 상황 (Context)
프로젝트 개발을 오케스트레이션하는 과정에서 아래와 같은 에이전트 간 역할 분담을 적용하고자 했습니다.
* **개발 팀장 (Antigravity)**: 실제 저장소의 문서 수정 및 구현 진행 (**Worker/Implementer**)
* **리뷰 에이전트 (Claude)**: 문서 구조의 설계 및 계획안 수립 (**Planner**)
이 분담에 따라 Claude 세션(`canary-projects-grpccanary-creator-claude`)에 "문서 모듈화 계획 및 체크리스트 작성" 작업을 위임하기 위해 `multi-agent-mux-delegate-job` 도구로 비동기 작업을 요청했습니다.
그러나 자동 생성된 잡 지시서인 `.mam/jobs/<job_id>/brief.md` 파일의 메타데이터에 다음과 같이 **구현자의 역할이 `Worker`로 강제 지정**되어 나가는 상황이 발생했습니다:
```markdown
# 📋 Brief: Job ed31b5fb Delegation
- **Job ID**: ed31b5fb
- **Target Agent**: claude (session: tmux:canary-projects-grpccanary-creator-claude)
- **Role**: Worker <-- [이슈 발생 지점: Planner가 아닌 Worker로 강제 지정됨]
- **Timeout**: 3600 s (Idle: 120 s)
```
이는 프로젝트 협업 규칙(`.agents/MULTI_AGENT_RULES.ko.md`)에 명시된 **"에이전트 역할 범위 준수 원칙(Role Suitability Check)"**에 위배되며, `claude`가 문서 작성이 아닌 파일 직접 수정을 시도할 위험이 있는 정합성 모순을 유발합니다.
---
## 2. 문제 사유 (Root Cause)
이 문제의 근본적인 기술적 원인은 **CLI 인수 파싱 로직 및 지시서(Brief) 생성 템플릿의 하드코딩**에 있습니다.
1. **CLI 옵션 설계 누락**:
* `multi-agent-mux-delegate-job submit` 명령어의 헬프 스펙을 확인한 결과, `--agent`, `--agent-session`, `--prompt` 등의 인수는 정의되어 있으나, 작업의 논리적 성격을 조율하는 **`--role <role_name>` 파라미터가 구현되어 있지 않습니다**.
2. **템플릿 내부의 상수 고정**:
* API를 통해 비동기 잡이 수임될 때 생성되는 `brief.md` 파일과 잡 레지스트리 JSON의 생성기 로직 내부에 `Role` 값이 **`Worker` 문자열 상수로 하드코딩**되어 동작하고 있습니다. 이로 인해 어떤 에이전트에 어떤 종류의 명령을 위임하더라도 메타데이터상으로는 항상 `Worker`로 바인딩됩니다.
---
## 3. 문제 해결 방법 (Remediation & Workarounds)
### 3.1 단기적 우회 방법 (Workaround)
프레임워크 CLI 소스코드를 수정하기 어려운 제한적 상황에서는 **프롬프트 페이로드(Prompt Payload) 하드닝** 기법을 사용하여 에이전트의 오작동을 차단합니다.
* **해결 원리**: brief.md의 메타데이터상 `Role: Worker` 지정을 덮어쓸 수 있도록, 프롬프트 문맥 내부에 **"너의 역할은 실제 문서를 수정하지 않고 계획만 수립하는 Planner이다. 절대 문서를 직접 수정하지 말라"**는 강력한 지시 제약(System-level Rule Override)을 포함하여 송신합니다.
* **효과**: AI 에이전트는 메타데이터보다 프롬프트 지시어의 행위 제약을 우선 순위로 받아들이므로, 의도한 대로 설계서 및 계획안만 수립하는 Planner 동작을 정상 수행하게 됩니다.
### 3.2 근본적인 해결 방법 (Remediation)
프레임워크의 CLI 래퍼인 `multi-agent-mux-delegate-job` 파일의 파싱 로직 및 brief.md 빌더 로직을 다음과 같이 수정합니다.
#### 1단계: CLI 인수 파서 수정 (`submit` 옵션 추가)
스크립트의 인수 파싱 영역에 `--role` 파라미터를 식별할 수 있는 변수 및 분기 로직을 선언합니다.
```bash
# 옵션 분석 루프 예시
while [[ $# -gt 0 ]]; do
case $1 in
--role)
DELEGATE_ROLE="$2"
shift 2
;;
# ... 기존 옵션 파싱 ...
esac
done
# 기본값 정의
DELEGATE_ROLE="${DELEGATE_ROLE:-Worker}"
```
#### 2단계: `brief.md` 생성 템플릿 연동
잡 디렉토리 내에 `brief.md`를 기입하여 내보내는 빌더 영역(Python 혹은 쉘 스크립트 에코 영역)을 다음과 같이 동적 변수와 연결합니다.
```diff
- echo "- **Role**: Worker" >> "$BRIEF_PATH"
+ echo "- **Role**: ${DELEGATE_ROLE}" >> "$BRIEF_PATH"
```
#### 3단계: 잡 레지스트리 JSON 메타데이터 갱신
동일하게 생성되는 `.mam/jobs/<job_id>.json` 파일 등의 메타데이터 생성 객체 내에 `role: DELEGATE_ROLE` 매핑 키를 추가하여, 타 모니터링 도구(예: `reconcile.sh``status.sh`)에서도 해당 에이전트의 잡 실행 역할을 정확하게 대시보드에 모니터링할 수 있도록 보완합니다.
+148
View File
@@ -0,0 +1,148 @@
# ==============================================================================
# test_deploy_layout.py — Deploy Script Layout, Refresh & Gitignore Test Suite
# ==============================================================================
import os
import shutil
import subprocess
import tempfile
import unittest
class TestDeployLayout(unittest.TestCase):
def setUp(self):
self.repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
self.temp_dir = tempfile.mkdtemp(prefix="mam_deploy_layout_test_")
self.test_dir = os.path.join(self.temp_dir, "workspace")
os.makedirs(self.test_dir, exist_ok=True)
self.env = os.environ.copy()
self.env["MAM_REPO_URL"] = self.repo_root
self.env["MAM_SKIP_VENV"] = "1"
def tearDown(self):
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_td1_td2_td3_essential_markdowns_only(self):
"""T-D1~D3: Verify essential markdowns are shipped and internal reports/references are excluded."""
res = subprocess.run(
["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir],
env=self.env,
capture_output=True,
text=True
)
self.assertEqual(res.returncode, 0, f"install.sh failed: {res.stderr}")
# T-D1: No internal reports in .agents/reports
reports_dir = os.path.join(self.test_dir, ".agents", "reports")
if os.path.exists(reports_dir):
reports_files = [f for root, _, files in os.walk(reports_dir) for f in files if f.endswith(".md")]
self.assertEqual(len(reports_files), 0, f"Internal reports found: {reports_files}")
# T-D2: Non-essential root docs omitted by default
self.assertFalse(os.path.exists(os.path.join(self.test_dir, "MESSAGING.md")))
self.assertFalse(os.path.exists(os.path.join(self.test_dir, "BOOTSTRAP.md")))
# T-D3: Core essential docs present (B-3 gate)
self.assertTrue(os.path.exists(os.path.join(self.test_dir, "AGENTS.md")))
self.assertTrue(os.path.exists(os.path.join(self.test_dir, ".agents", "MULTI_AGENT_RULES.md")))
self.assertTrue(os.path.exists(os.path.join(self.test_dir, ".agents", "INSTALL.md")))
self.assertTrue(os.path.exists(os.path.join(self.test_dir, ".agents", "skills", "lib.sh")))
def test_td6_td7_td8_mam_deploy_layout_and_removal(self):
"""T-D6~D8: Verify remove.sh/update.sh installed in .mam_deploy and work properly."""
subprocess.run(
["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir],
env=self.env,
check=True
)
# T-D6: Installed in .mam_deploy and executable
remover = os.path.join(self.test_dir, ".mam_deploy", "remove.sh")
updater = os.path.join(self.test_dir, ".mam_deploy", "update.sh")
self.assertTrue(os.path.exists(remover))
self.assertTrue(os.path.exists(updater))
self.assertTrue(os.access(remover, os.X_OK))
# T-D8: Execution from inside .mam_deploy directory targets parent workspace
res = subprocess.run(
["bash", "./remove.sh", "--force"],
cwd=os.path.join(self.test_dir, ".mam_deploy"),
capture_output=True,
text=True
)
self.assertEqual(res.returncode, 0, f"remove.sh failed: {res.stderr}")
self.assertFalse(os.path.exists(os.path.join(self.test_dir, ".agents", "skills")))
self.assertFalse(os.path.exists(os.path.join(self.test_dir, ".mam_deploy")))
def test_td12_td13_td14_gitignore_managed_block(self):
"""T-D12~D14: Verify .gitignore managed block injection and manifest exclusion (B-1 gate)."""
subprocess.run(
["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir],
env=self.env,
check=True
)
gi_path = os.path.join(self.test_dir, ".gitignore")
self.assertTrue(os.path.exists(gi_path))
with open(gi_path) as f:
content = f.read()
self.assertIn("# >>> MAM managed block", content)
self.assertIn("/.mam.env", content)
self.assertIn("/.mam_deploy/", content)
# T-D14 [MERGE BLOCKER B-1]: .gitignore MUST NOT be in manifest
manifest_path = os.path.join(self.test_dir, ".mam", "install_manifest.txt")
with open(manifest_path) as f:
manifest = f.read()
self.assertNotIn(".gitignore", manifest)
def test_td21_td22_td23_safe_refresh_custom_skills(self):
"""T-D21~D23: Verify custom modifications to skills are preserved on refresh (B-4 gate)."""
subprocess.run(
["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir],
env=self.env,
check=True
)
# Modify a framework skill file
custom_skill = os.path.join(self.test_dir, ".agents", "skills", "lib.sh")
with open(custom_skill, "a") as f:
f.write("\n# CUSTOM_USER_MODIFICATION\n")
# Run install.sh again (REFRESH=1)
res = subprocess.run(
["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir],
env=self.env,
capture_output=True,
text=True
)
self.assertEqual(res.returncode, 0)
self.assertIn("Local modification detected", res.stderr)
# Verify modification preserved
with open(custom_skill) as f:
self.assertIn("CUSTOM_USER_MODIFICATION", f.read())
# Verify backup created in .mam/skill-backups
backups_dir = os.path.join(self.test_dir, ".mam", "skill-backups")
self.assertTrue(os.path.exists(backups_dir))
def test_td27_td28_update_preserves_mam_state(self):
"""T-D27~D28: Verify update.sh preserves .mam state files across cycles (B-5 gate)."""
subprocess.run(
["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir],
env=self.env,
check=True
)
# Run update.sh
updater = os.path.join(self.test_dir, ".mam_deploy", "update.sh")
res = subprocess.run(["bash", updater, "--force"], cwd=self.test_dir, env=self.env, capture_output=True, text=True)
self.assertEqual(res.returncode, 0, f"update.sh failed: {res.stderr}")
# Verify asset_hashes.txt and version.txt still exist
self.assertTrue(os.path.exists(os.path.join(self.test_dir, ".mam", "asset_hashes.txt")))
self.assertTrue(os.path.exists(os.path.join(self.test_dir, ".mam", "version.txt")))
if __name__ == "__main__":
unittest.main()
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env python3
"""Full regression test suite for .mam.env migration (T-1 through T-17)."""
import os
import shutil
import tempfile
import unittest
import subprocess
import logging
from unittest.mock import patch
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.agents/skills/multi-agent-mux-delegate-job/scripts")))
import mqtt_common
class TestEnvMigrationFull(unittest.TestCase):
def setUp(self):
self.test_dir = tempfile.mkdtemp(prefix="mam_env_full_test_")
self.old_cwd = os.getcwd()
os.chdir(self.test_dir)
self.clean_os_env()
def tearDown(self):
os.chdir(self.old_cwd)
shutil.rmtree(self.test_dir, ignore_errors=True)
self.clean_os_env()
def clean_os_env(self):
for k in ["MQTT_BROKER", "MQTT_PORT", "MQTT_PASSWORD", "TEST_KEY_A", "TEST_KEY_B", "MAM_ENV_FILE", "MAM_LEGACY_ENV_OWNED"]:
os.environ.pop(k, None)
mqtt_common._warned_deprecated_env = False
mqtt_common._warned_coexistence_env = False
def test_t1_mam_env_only(self):
"""T-1: Loads .mam.env when present."""
mam_env = os.path.join(self.test_dir, ".mam.env")
with open(mam_env, "w") as f:
f.write("TEST_KEY_A=val_mam_env\n")
mqtt_common._load_dotenv(self.test_dir)
self.assertEqual(os.environ.get("TEST_KEY_A"), "val_mam_env")
def test_t2_legacy_env_only_fallback_and_warning(self):
"""T-2: Falls back to .env when .mam.env absent, logs deprecation warning."""
legacy_env = os.path.join(self.test_dir, ".env")
with open(legacy_env, "w") as f:
f.write("TEST_KEY_A=val_legacy_env\n")
with self.assertLogs("delegate_job.mqtt_common", level="WARNING") as cm:
mqtt_common._load_dotenv(self.test_dir)
self.assertEqual(os.environ.get("TEST_KEY_A"), "val_legacy_env")
self.assertTrue(any("deprecated" in log for log in cm.output))
def test_t3_coexistence_mam_env_precedence_and_warning(self):
"""T-3: When both exist, .mam.env wins and coexistence warning logs."""
mam_env = os.path.join(self.test_dir, ".mam.env")
legacy_env = os.path.join(self.test_dir, ".env")
with open(mam_env, "w") as f:
f.write("TEST_KEY_A=val_mam\n")
with open(legacy_env, "w") as f:
f.write("TEST_KEY_A=val_legacy\n")
with self.assertLogs("delegate_job.mqtt_common", level="WARNING") as cm:
mqtt_common._load_dotenv(self.test_dir)
self.assertEqual(os.environ.get("TEST_KEY_A"), "val_mam")
self.assertTrue(any("Both" in log for log in cm.output))
def test_t4_parent_boundary_stop_walkup_without_arg(self):
"""T-4: Walk-up with no argument stops at boundary marker (.agents/.git)."""
parent_dir = os.path.join(self.test_dir, "parent")
child_repo = os.path.join(parent_dir, "child_repo")
nested_script_dir = os.path.join(child_repo, ".agents", "skills", "test", "scripts")
os.makedirs(nested_script_dir, exist_ok=True)
with open(os.path.join(parent_dir, ".env"), "w") as f:
f.write("TEST_KEY_A=parent_leaked_secret\n")
dummy_file = os.path.join(nested_script_dir, "mqtt_common.py")
with patch.object(mqtt_common, "__file__", dummy_file):
mqtt_common._load_dotenv()
self.assertIsNone(os.environ.get("TEST_KEY_A"))
def test_t5_os_env_precedence(self):
"""T-5: OS environment variables take precedence over env file values."""
os.environ["TEST_KEY_A"] = "os_val"
mam_env = os.path.join(self.test_dir, ".mam.env")
with open(mam_env, "w") as f:
f.write("TEST_KEY_A=file_val\n")
mqtt_common._load_dotenv(self.test_dir)
self.assertEqual(os.environ.get("TEST_KEY_A"), "os_val")
def test_t6_mam_env_file_override(self):
"""T-6: MAM_ENV_FILE takes highest precedence if explicitly provided."""
custom_env = os.path.join(self.test_dir, "custom.env")
with open(custom_env, "w") as f:
f.write("TEST_KEY_A=custom_val\n")
os.environ["MAM_ENV_FILE"] = custom_env
mam_env = os.path.join(self.test_dir, ".mam.env")
with open(mam_env, "w") as f:
f.write("TEST_KEY_A=mam_val\n")
mqtt_common._load_dotenv(self.test_dir)
self.assertTrue(os.path.exists(custom_env))
self.assertEqual(os.environ.get("TEST_KEY_A"), "custom_val")
def test_t7_wrapper_cwd_isolation(self):
"""T-7: Bash wrapper executes cleanly and outputs help/warnings."""
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
wrapper_path = os.path.join(repo_root, ".agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job")
res = subprocess.run([wrapper_path, "--help"], capture_output=True, text=True)
self.assertEqual(res.returncode, 0)
def test_t8_remove_force_preserves_owned_env(self):
"""T-8 [MERGE BLOCKER]: --force MUST back up owned .env, NEVER delete without backup."""
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
shutil.copy(os.path.join(repo_root, "deploy/remove.sh"), self.test_dir)
os.makedirs(".mam", exist_ok=True)
with open(".mam/install_manifest.txt", "w") as f:
f.write(".env\nremove.sh\n")
with open(".env", "w") as f:
f.write("SECRET_KEY=user_secret_data\n")
res = subprocess.run(["bash", "remove.sh", "--force"], capture_output=True, text=True)
self.assertEqual(res.returncode, 0, f"remove.sh failed: {res.stderr}")
self.assertTrue(os.path.exists(".env.mam-backup"), "MAM-owned .env MUST be backed up under --force")
with open(".env.mam-backup") as f:
self.assertIn("user_secret_data", f.read())
def test_t8b_purge_env_is_sole_delete_authority(self):
"""T-8b: --purge-env is the ONLY flag authorized to delete without backup."""
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
shutil.copy(os.path.join(repo_root, "deploy/remove.sh"), self.test_dir)
os.makedirs(".mam", exist_ok=True)
with open(".mam/install_manifest.txt", "w") as f:
f.write(".env\nremove.sh\n")
with open(".env", "w") as f:
f.write("SECRET_KEY=user_secret_data\n")
res = subprocess.run(["bash", "remove.sh", "--force", "--purge-env"], capture_output=True, text=True)
self.assertEqual(res.returncode, 0)
self.assertFalse(os.path.exists(".env"))
self.assertFalse(os.path.exists(".env.mam-backup"))
def test_t9_git_check_ignore(self):
"""T-9 [MERGE BLOCKER]: Verifies gitignore rules ignore .mam.env variants but track .mam.env.example."""
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
check_cmd = ["git", "check-ignore", "-v", ".mam.env", ".mam.env.bak", ".mam.env.update-tmp", ".mam.env.mam-backup"]
res = subprocess.run(check_cmd, cwd=repo_root, capture_output=True, text=True)
self.assertEqual(res.returncode, 0, f"git check-ignore failed: {res.stderr}")
ex_cmd = ["git", "check-ignore", ".mam.env.example"]
res_ex = subprocess.run(ex_cmd, cwd=repo_root, capture_output=True, text=True)
self.assertNotEqual(res_ex.returncode, 0, ".mam.env.example should NOT be ignored by git")
def test_t10_shadowing_prevention_guard(self):
"""T-10 [MERGE BLOCKER]: install.sh does NOT create new .mam.env if .env.update-tmp exists."""
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
shutil.copy(os.path.join(repo_root, "deploy/install.sh"), self.test_dir)
os.makedirs("deploy", exist_ok=True)
shutil.copy(os.path.join(repo_root, "deploy/install.sh"), "deploy/install.sh")
if os.path.exists(os.path.join(repo_root, ".mam.env.example")):
shutil.copy(os.path.join(repo_root, ".mam.env.example"), ".mam.env.example")
with open(".env.update-tmp", "w") as f:
f.write("MQTT_PASSWORD=s3cr3t_user_pass\n")
res = subprocess.run(["bash", "deploy/install.sh", self.test_dir], capture_output=True, text=True)
self.assertEqual(res.returncode, 0, f"install.sh failed: {res.stderr}")
self.assertIn("Preserved without shadowing", res.stdout)
self.assertFalse(os.path.exists(".mam.env"), ".mam.env MUST NOT be created when .env.update-tmp is waiting for restore")
def test_t12_unowned_legacy_env_preservation(self):
"""T-12 [MERGE BLOCKER]: install.sh leaves unowned .env untouched without heuristic hijacking."""
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
os.makedirs("deploy", exist_ok=True)
shutil.copy(os.path.join(repo_root, "deploy/install.sh"), "deploy/install.sh")
if os.path.exists(os.path.join(repo_root, ".mam.env.example")):
shutil.copy(os.path.join(repo_root, ".mam.env.example"), ".mam.env.example")
with open(".env", "w") as f:
f.write("MQTT_BROKER=my-private-iot-broker.com\nSTRIPE_SECRET=sk_live_123\n")
res = subprocess.run(["bash", "deploy/install.sh", self.test_dir], capture_output=True, text=True)
self.assertEqual(res.returncode, 0)
self.assertTrue(os.path.exists(".env"), ".env should remain untouched")
self.assertFalse(os.path.exists(".mam.env"), ".mam.env should NOT hijack unowned .env")
def test_t13_owned_legacy_env_migration_and_manifest_rewrite(self):
"""T-13: Owned .env is migrated to .mam.env and manifest is updated."""
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
os.makedirs("deploy", exist_ok=True)
shutil.copy(os.path.join(repo_root, "deploy/install.sh"), "deploy/install.sh")
os.makedirs(".mam", exist_ok=True)
with open(".mam/install_manifest.txt", "w") as f:
f.write(".env\n")
with open(".env", "w") as f:
f.write("MQTT_BROKER=owned-broker.internal\n")
res = subprocess.run(["bash", "deploy/install.sh", self.test_dir], capture_output=True, text=True)
self.assertEqual(res.returncode, 0)
self.assertTrue(os.path.exists(".mam.env"))
self.assertFalse(os.path.exists(".env"))
with open(".mam/install_manifest.txt", "r") as f:
manifest_lines = [line.strip() for line in f]
self.assertIn(".mam.env", manifest_lines)
self.assertNotIn(".env", manifest_lines)
def test_t16_backup_deduplication_across_reinstall_cycles(self):
"""T-16 (Backup Hygiene): Repeated remove.sh -y -> install.sh cycles do not multiply identical backups."""
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
os.makedirs("deploy", exist_ok=True)
shutil.copy(os.path.join(repo_root, "deploy/install.sh"), "deploy/install.sh")
shutil.copy(os.path.join(repo_root, "deploy/remove.sh"), "deploy/remove.sh")
if os.path.exists(os.path.join(repo_root, ".mam.env.example")):
shutil.copy(os.path.join(repo_root, ".mam.env.example"), ".mam.env.example")
# Initial setup: MAM-owned .mam.env with user secret
os.makedirs(".mam", exist_ok=True)
with open(".mam/install_manifest.txt", "w") as f:
f.write(".mam.env\nremove.sh\nupdate.sh\n.mam.env.example\n")
with open(".mam.env", "w") as f:
f.write("MQTT_PASSWORD=REAL_USER_SECRET_12345\n")
# Cycle 1: remove -y
res1 = subprocess.run(["bash", "deploy/remove.sh", "--force"], capture_output=True, text=True)
self.assertEqual(res1.returncode, 0)
self.assertTrue(os.path.exists(".mam.env.mam-backup"))
# Cycle 1: reinstall
res_inst1 = subprocess.run(["bash", "deploy/install.sh", self.test_dir], capture_output=True, text=True)
self.assertEqual(res_inst1.returncode, 0)
# Cycle 2: remove -y & reinstall
subprocess.run(["bash", "deploy/remove.sh", "--force"], check=True)
subprocess.run(["bash", "deploy/install.sh", self.test_dir], check=True)
# Cycle 3: remove -y (identical default content should be deduplicated)
subprocess.run(["bash", "deploy/remove.sh", "--force"], check=True)
# Verify backup count stabilizes at <= 2 (Slot 1 original secret + 1 dedup default slot)
backups = [f for f in os.listdir(self.test_dir) if f.startswith(".mam.env.mam-backup")]
self.assertLessEqual(len(backups), 2, f"Backup files duplicated indefinitely: {backups}")
def test_t17_immutable_slot_1_user_secret_retention(self):
"""T-17 [MERGE BLOCKER]: Slot 1 (.mam.env.mam-backup) retains original user secret after repeated cycles."""
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
os.makedirs("deploy", exist_ok=True)
shutil.copy(os.path.join(repo_root, "deploy/install.sh"), "deploy/install.sh")
shutil.copy(os.path.join(repo_root, "deploy/remove.sh"), "deploy/remove.sh")
if os.path.exists(os.path.join(repo_root, ".mam.env.example")):
shutil.copy(os.path.join(repo_root, ".mam.env.example"), ".mam.env.example")
os.makedirs(".mam", exist_ok=True)
with open(".mam/install_manifest.txt", "w") as f:
f.write(".mam.env\nremove.sh\n")
with open(".mam.env", "w") as f:
f.write("MQTT_PASSWORD=REAL_USER_SECRET_12345\n")
# 3 cycles of remove -> install
for _ in range(3):
subprocess.run(["bash", "deploy/remove.sh", "--force"], check=True)
subprocess.run(["bash", "deploy/install.sh", self.test_dir], check=True)
with open(".mam.env.mam-backup", "r") as f:
slot1_content = f.read()
self.assertIn("REAL_USER_SECRET_12345", slot1_content, "Slot 1 MUST retain original user secret permanently")
if __name__ == "__main__":
unittest.main()