# herdr-agent-orchestration (다중 에이전트 Herdr 오케스트레이션 및 메시징 백플레인) [herdr](https://herdr.dev)와 MQTT 위에 만든 **다중 에이전트 오케스트레이션·메시징 백플레인**입니다. 장시간 실행되는 코딩 에이전트(`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`)를 격리된 herdr pane 에서 돌리고, 작업을 추적되는 잡으로 위임하며, 모든 리뷰어가 통과시킬 때까지 planner / creator / reviewer 루프를 돌립니다. [![Version: v4.9.4](https://img.shields.io/badge/version-v4.9.4-blue.svg)](VERSIONS.md) --- ## 🚀 개요 긴 에이전트 작업은 정해진 방식으로 깨집니다. 터미널과 함께 세션이 죽고, 한 pane 에 잡 두 개가 들어가고, 오케스트레이터는 바쁜 에이전트와 죽은 에이전트를 구분하지 못하고, 리뷰는 읽은 뒤에 바뀐 코드를 승인합니다. Multi-Agent Mux(MAM)는 이를 다음으로 막습니다. 1. **격리되고 재개 가능한 에이전트 세션.** 에이전트마다 자기 herdr pane 에서 돌고, 보통 워크스페이스 전용 herdr 서버를 씁니다. 세션은 대화 ID 와 함께 `.mam/agent-sessions.yaml`(SQLite 백업)에 기록되므로, 멈춘 에이전트를 나중에 같은 대화로 재개할 수 있습니다. 2. **화면이 아니라 이벤트로 판정하는 잡.** 위임 잡은 레지스트리 레코드와 MQTT 이벤트 흐름(`started`, `progress`, `completed`, `error`)입니다. 에이전트는 정해진 시간 안에 프롬프트 수신을 알리고, 일하는 동안 heartbeat 를 보내야 합니다. 잡을 닫는 것은 프로토콜 이벤트나 시간 초과뿐이며, 터미널을 읽어 판정하지 않습니다. 3. **세션당 한 번에 잡 하나.** 프롬프트를 넣기 전에 디스패처가 레지스트리로 그 세션의 앞선 잡(온보딩 포함)이 끝나기를 기다립니다. pane 배치는 herdr 서버마다 직렬화되어, 동시에 실행한 `create` 가 같은 pane 을 차지하지 않습니다. 4. **근거를 고정한 리뷰 루프.** `multi-agent-mux-loop` 는 변경을 선택한 리뷰어에게 보내고, 그 전원의 `PASS` 를 요구합니다. 리뷰어는 `--reviewer` 로 지정하거나 `--all-reviewer` 로 실행 중인 리뷰어 전원을 고르며, 둘 다 없으면 creator 가 스스로 리뷰합니다. `--worktree`/`--base` 를 쓰면 리뷰 라운드마다 소스 스냅샷에 묶이며, 라운드 중 소스가 바뀌면 그 승인은 버리고 다시 리뷰합니다. --- ## 📦 설치 및 설정 아무 워크스페이스 디렉터리에서 MAM 을 설치합니다: ```bash curl -fsSL https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh | bash ``` 로컬 클론에서는: ```bash bash deploy/install.sh ``` `install.sh` 는 여러 번 실행해도 안전합니다. 하는 일은 다음과 같습니다. - herdr 와 Python 3.9+ 가 설치돼 있는지, 시스템 `python3` 에 PyYAML 이 있는지 확인합니다. - `.agents/` 와 `.mam.env.example` 을 워크스페이스에 복사합니다. - `.venv` 를 만들고 `paho-mqtt`·PyYAML 을 설치합니다. - `MQTT_BROKER=localhost` 로 된 초기 `.mam.env` 를 씁니다. 이미 있는 `.mam.env` 는 그대로 둡니다. 예전 `.env` 만 있으면 그것을 `.mam.env` 로 복사하고 원본은 남깁니다. - `.venv`, `.mam/`, `.mam.env` 를 `.gitignore` 에 추가합니다. `.mam.env` 의 `MQTT_BROKER` 를 직접 운영하는 브로커로 바꾸십시오(사설 브로커는 [MESSAGING.md](MESSAGING.md) 참고). `MQTT_BROKER` 가 아예 설정되지 않으면 코드는 공개 브로커 `broker.hivemq.com` 을 씁니다. 시험용으로는 괜찮지만 실제 작업용은 아닙니다. `deploy/generate-env.sh` 는 `.mam.env` 가 없을 때 `.mam.env.example` 로 만듭니다(`--force` 는 `.mam.env.bak` 을 남기고 덮어씁니다). 업데이트와 제거는 `deploy/update.sh`, `deploy/remove.sh` 입니다. `deploy/install_mam.sh` 는 로컬 클론에서 다른 디렉터리로 복사해 설치하는 별도 설치기입니다. --- ## 🛠️ 핵심 스킬 모든 스킬은 `.agents/skills/` 아래에 있고, 각 `SKILL.md` 가 옵션과 종료 코드를 설명합니다. | 스킬 | 하는 일 | |---|---| | **`multi-agent-mux-create`** | 에이전트를 herdr pane 에서 시작하고 세션을 등록합니다. `--onboard` 는 온보딩 잡이 끝날 때까지 기다립니다(실패하면 종료 코드 44, 시간 안에 끝나지 않으면 45; 세션은 남김). `scripts/worktree_prepare.sh` 는 creator 용 이슈 워크트리를 준비합니다. | | **`multi-agent-mux-stop`** | 세션을 정상 종료합니다. 대화 ID 를 행에 저장합니다(`status: stopped`, `resumable: true`). `--purge-conversation --yes` 는 대신 대화를 지웁니다. `scripts/worktree_remove.sh` 는 서브모듈까지 깨끗한지 확인한 뒤에만 이슈 워크트리를 지웁니다. | | **`multi-agent-mux-resume`** | 멈춘 세션을 저장된 대화로 되살립니다(`claude -r`, `agy --conversation`, `hermes --resume`, `grok --resume`, `opencode --session`, `codex resume`). 세션이 원래 돌던 디렉터리에서만 재개합니다. | | **`multi-agent-mux-status`** | 모든 세션의 읽기 전용 스냅샷: 레지스트리 상태, herdr 생존, pane 명령과 cwd, 재개 ID, 불일치(drift). | | **`multi-agent-mux-monitor`** | 장기 실행 조정자(`reconcile.sh --subscribe`). 잡 이벤트에 반응하고, 브로커가 죽으면 폴링으로 돌아가며, 레지스트리 불일치를 고칩니다. 예를 들어 죽은 세션을 `terminated` 로 표시하고, 레지스트리가 모르는 살아 있는 세션을 등록합니다. | | **`multi-agent-mux-delegate-job`** | 세션(`herdr:`)이나 다른 호스트의 러너(`remote:`)에 잡을 위임하고 결과를 기다립니다. [위임 계약](#-위임-계약)을 보십시오. | | **`multi-agent-mux-loop`** | 계획·구현·리뷰: 선택적인 planner 와 creator, 그다음 선택한 리뷰어(`--reviewer A,B`, 또는 `--all-reviewer` 로 creator 를 뺀 실행 중인 리뷰어 전원)가 모두 `PASS` 를 낼 때까지(또는 `--max-loop` 까지). 둘 다 없으면 creator 가 스스로 리뷰합니다. 루프 밖에서 끝난 작업은 `--review-only` 로 리뷰합니다. 통과한 리포트는 `.agents/reports//` 으로 옮겨 보관합니다. | | **`multi-agent-mux-orc-onboard`** | 오케스트레이터 자신의 세션 ID 를 등록해, 세션 탐색이 그것을 하위 에이전트로 착각하지 않게 합니다. | --- ## 📐 아키텍처 MAM 은 각자 레지스트리를 가진 두 계층으로 이루어집니다. 1. **세션 (herdr)**: `lib.sh` 와 create/stop/resume/status/monitor 스크립트가 에이전트를 돌리고, `.mam/agent-sessions.yaml`(+ `.mam/agent-sessions.db`)을 기준 기록으로 유지합니다. 2. **잡 (위임)**: `lib_py/delegation/` 이 `.mam/jobs/.json` 에 잡을 등록하고, 브리프를 주입하고, MQTT 로 잡의 이벤트를 따라갑니다. 모든 이벤트는 잡의 감사 로그 `.mam/delegate_job_logs//` 에도 덧붙여집니다. ```text +--------------+ register / pick +----------------------+ | orchestrator | -----------------> | .mam/jobs/.json | live record | (wrapper) | +----------+-----------+ +------+-------+ | every transition | inject brief v v +----------------------+ +--------------+ publish_event | .mam/delegate_job_ | append-only | agent pane | --------+ | logs// | audit log +--------------+ | +----------------------+ v +-------------+ events + retained state | MQTT broker | -------------------------> job_subscriber (orchestrator) +-------------+ -------------------------> monitor (reconcile.sh) | v .mam/agent-sessions.yaml / .db ``` ### 🔒 Herdr 서버 격리 - **워크스페이스 전용 서버.** `create` 는 기본으로 워크스페이스 이름(slug)을 딴 herdr 서버에서 에이전트를 돌립니다. 이 서버는 기본 herdr 와 분리된 자기 소켓을 씁니다. `--herdr-session ` 또는 `HERDR_SESSION_NAME` 으로 다른 이름을 고를 수 있습니다. 서버 이름은 세션 행에 기록되고, 이후 모든 명령이 그 서버로 갑니다. - **shim.** `lib.sh` 를 source 하면 `/.mam/shim/herdr` 를 쓰고 `PATH` 맨 앞에 둡니다. shim 은 스크립트가 쓰는 tmux 식 명령(`new-session`, `has-session` …)을 herdr 의 에이전트 API 위에 제공합니다. 이름 있는 서버면 호출마다 `--session ` 를 붙이고, 그 서버가 돌고 있지 않으면 헤드리스로 시작합니다. 단 `MAM_NO_AUTOSTART=1` 이면 시작하지 않습니다. `status` 와 `monitor` 가 이 값을 설정하므로, 조회만으로 서버가 뜨는 일은 없습니다. `default` 서버 호출은 바꾸지 않고 사용자의 herdr 로 그대로 갑니다. - **예상치 못한 재실행 없음.** MAM 이 시작한 서버는 보통 사용자의 herdr 설정에 `resume_agents_on_restore = false` 를 더해 씁니다. 그래서 herdr 가 재시작돼도 저장된 에이전트를 권한 플래그 없이 다시 띄우지 않습니다. `tomllib` 이 없는 Python(macOS 기본 3.9 등)에서는 이 파일에 그 키만 들어가므로, restore 는 꺼지지만 사용자의 다른 herdr 설정은 반영되지 않습니다. 이때 부트스트랩이 경고합니다. 설정 파일을 아예 쓰지 못한 경우에만 서버가 restore 가 켜진 채 시작하며, 이때도 경고를 남깁니다. - **pane 배치는 한 번에 하나.** pane 배치는 서버별 잠금(`lib_py/pane_lock.py`, macOS·Linux 에서 `flock`)을 잡고 합니다. 한 폴더에서 동시에 실행한 `create` 도 각자 자기 pane 을 받습니다. ### 🛡️ 레지스트리 쓰기와 동시성 - **직렬화된 쓰기.** `agent-sessions.yaml` 의 모든 변경은 `lib.sh::atomic_dump_yaml` 을 거칩니다. `.mam/agent-sessions.db` 의 배타적 SQLite 트랜잭션(`BEGIN IMMEDIATE`) 안에서 실행되고, 검증한 뒤에 YAML 을 원자적으로 다시 씁니다. 잡 레코드는 별도의 레지스트리 잠금을 씁니다. - **Python 인터프리터.** 레지스트리 쓰기와 위임 헬퍼는 이 순서로 Python 을 고릅니다: `AGENT_PYTHON_BIN`, 활성화된 가상환경, 스킬 위쪽에서 가장 가까운 `.venv`, 마지막으로 `PATH` 의 `python3`. shim 과 `create` 의 일부 같은 셸 경로는 `PATH` 의 `python3` 를 직접 부릅니다. - **네트워크 파일시스템.** `.mam/` 이 NFS/CIFS/SSHFS 에 있으면 `lib.sh` 가 경고하고, SQLite 저널 모드를 `WAL` 에서 `DELETE` 로 바꿉니다. - **체크아웃당 루프 하나.** 루프 잠금이 같은 제어 평면에서 두 번째 `run_loop.sh` 를 거부합니다. --- ## 📨 위임 계약 전체 명세는 [MESSAGING.md §2.5](MESSAGING.md)에 있습니다. - **전달**: 프롬프트를 넘긴 뒤 **T** 초(기본 180) 안에 에이전트가 `started` 를 발행해야 합니다. 그러지 않으면 잡은 실패로 닫히고(`agent_blocked`, 종료 코드 1), 프롬프트를 두 번 넣지 않습니다. - **생존**: `started --heartbeat N` 이 자동 heartbeat 를 시작합니다(기본 30 초마다). **K×N** 초(기본 120 초) 동안 아무 이벤트가 없으면 무응답으로 봅니다. herdr 상태와 pane 텍스트는 진단용으로만 로그에 남깁니다. - **예산**: 잡마다 벽시계 예산이 있습니다(`--timeout` / `MAM_JOB_TIMEOUT_S`, 기본 3600 초). 예산을 다 쓴 잡은 `wall_clock_timeout` 으로 닫힙니다. - **바쁜 세션**: 래퍼는 그 세션의 앞선 열린 잡이 끝나기를 `MAM_DISPATCH_WAIT_S` 만큼 기다립니다. 끝나지 않으면 `cancelled`(`session_busy`, 종료 코드 5)로 끝나고, 프롬프트는 전달되지 않습니다. - **원격 에이전트**: `remote:` 은 브리프를 서명된 retained 브로커 메시지로 다른 호스트의 `remote.py run` 러너에 보냅니다. 사설 브로커에서만 쓰십시오. | 종료 코드 | 의미 | |:---:|---| | `0` | `completed` | | `1` | 실패, 또는 T 안에 `started` 없음 | | `2` | K×N 동안 이벤트 없음(`deadline_timeout`) 또는 예산 소진(`wall_clock_timeout`) | | `3` | 브로커를 쓸 수 없고, 디스크에도 종결 상태가 없음 | | `5` | 이번 시도는 프롬프트를 전달하지 않음: 세션이 바쁘거나 대상을 찾지 못함(잡은 `cancelled`), 또는 기다리는 동안 다른 시도가 레코드를 가져감(레코드는 새 소유자에게 그대로 둠) | --- ## 🔁 리뷰 루프 ```mermaid sequenceDiagram autonumber actor User participant L as run_loop.sh participant P as Planner (optional) participant C as Creator participant R as Reviewers User->>L: --task, --creator, --reviewer / --all-reviewer opt --plan L->>P: write the plan (Creator challenges it for --plan-talk turns) end L->>C: implement and commit (skipped with --review-only) loop up to --max-loop L->>R: review the snapshot diff (base..head) alt every reviewer PASS and the source unchanged R-->>L: [VERDICT: PASS] else NOT PASS R-->>L: [VERDICT: NOT PASS] + findings L->>C: fix (or [REBUT:] up to --max-rebut) end end L->>User: reports promoted to .agents/reports// ``` - **엄격한 판정**: 판정 토큰은 리포트 마지막 줄에 단독으로 있어야 합니다. 판정이 없으면 `NOT PASS` 로 봅니다. - **리뷰 identity** (`--worktree` / `--base`): 라운드마다 커밋하지 않은 파일까지 담은 스냅샷을 리뷰합니다. 라운드 중 소스가 바뀌면 그 승인은 버립니다. 보관되는 리포트 옆에는 base·head·tree·리뷰어를 적은 `report-.identity.json` 이 붙습니다. - **하위 잡**: creator 가 작업 일부를 위임하면(`--parent-job`), 루프는 리뷰 전에 그 잡들을 기다리고 상태를 리뷰어에게 보여 줍니다. --- ## 🔐 보안 - **서명된 이벤트**: 토큰을 직접 넘기거나 브로커가 TLS 또는 사용자 이름을 쓰면, 잡은 고유 토큰(`auth_token`)을 받습니다. 그때 모든 이벤트와 retained 상태 메시지에 그 토큰으로 만든 HMAC-SHA256 서명이 붙고, 구독자는 검증에 실패한 메시지를 버립니다. 이벤트와 상태 메시지에는 토큰이 아니라 서명만 실립니다. 토큰을 넘기지 않았고 브로커에 TLS 도 사용자 이름도 없으면(PoC 구성), 잡에 토큰이 없어 서명을 검사하지 않습니다. - **원격 디스패치에는 토큰이 실린다**: `remote:` 디스패치 메시지에는 잡의 토큰이 들어갑니다(`MAM_DISPATCH_SECRET` 으로 서명될 뿐 암호화되지는 않음). 접근이 통제된 사설 브로커에서만 쓰십시오. - **재전송과 순서**: 이벤트마다 증가하는 `seq` 가 붙습니다. 구독자는 지금까지 본 것보다 큰 번호만 받고, 빈 번호는 `seq_gap` 으로 기록합니다. 타임스탬프는 참고용입니다. - **자격 증명**: 브로커 자격 증명은 `.mam.env` 에만 있고, 잡 레코드에는 없습니다. --- ## 🧪 테스트 ```bash .venv/bin/python -m pytest -q -p no:cacheprovider tests ``` - 테스트는 약 1,900 개이고, 전체 실행에 한 시간 넘게 걸립니다. - 프로세스 안에서 자체 MQTT 브로커를 띄우므로 `.mam.env` 가 필요 없습니다. 테스트용으로 `.mam.env` 를 export 하지 마십시오. - 모든 테스트는 임시 잡 레지스트리와 감사 로그 디렉터리를 씁니다. 약 3분의 1 은 임시 HOME 을 설정하는 샌드박스 픽스처도 쓰고, 그보다 적은 수는 mock herdr 도 씁니다. - 실제 herdr 를 다루는 테스트는 일회용 herdr 서버를 직접 띄웁니다. - 실행이 끝나면 세션 단위 가드가 실제 제어 평면과 사용자의 에이전트 홈(`~/.claude/projects`, `~/.codex`, `~/.gemini` …)을 확인하고, 테스트가 남긴 항목이 있으면 실행을 실패시킵니다. - 일부 테스트는 플랫폼에 따라 다릅니다. Python 3.11+(`tomllib`), Linux(`/proc`), `nats-docker` 서브모듈이 필요한 테스트는 그 밖의 환경에서 건너뜁니다. --- ## 📁 저장소 구조 ```text . ├── .agents/ │ ├── MULTI_AGENT_RULES.md / .ko.md # 역할, 워크플로우, 협업 규칙 │ ├── reports/ # 보관된 리뷰 리포트 (+ identity 사이드카) │ └── skills/ │ ├── lib.sh # 공용 셸 라이브러리 (herdr shim, 레지스트리 쓰기) │ ├── lib_py/ # Python 코어 │ │ ├── agents/ # 에이전트별 CLI 어댑터 (spawn/resume/discover) │ │ ├── delegation/ # 잡 레지스트리, 래퍼, MQTT 발행/구독, 원격 러너 │ │ ├── loop/ # 리뷰 루프 런타임 │ │ └── *.py # pane_lock, stall_watch, worktree, herdr_config, ... │ └── multi-agent-mux-*/ # 스킬마다 디렉터리 하나 (SKILL.md + scripts/) ├── .mam/ # 런타임 상태 (git 제외): 세션, 잡, 감사 로그, shim ├── deploy/ # install.sh, install_mam.sh, update.sh, remove.sh, generate-env.sh ├── docs/ # contracts/, design/, 에이전트 통합·브리지 가이드 ├── nats-docker/ # 사설 NATS 브로커 배포 (서브모듈) ├── tests/ # pytest 스위트 ├── AGENTS.md # 에이전트 행동 지침 ├── BOOTSTRAP.md / BOOTSTRAP.ko.md # 단계별 설치·검증 가이드 ├── IMPROVEMENTS.md # 백로그 ├── MESSAGING.md # MQTT 와이어 프로토콜과 위임 계약 ├── README.md # 영문 README └── VERSIONS.md # 릴리스 노트 (락스텝 버전 원장) ``` --- ## 🚦 빠른 시작 전체 과정은 [BOOTSTRAP.ko.md](BOOTSTRAP.ko.md)를 보십시오. 요약하면: ```bash # 1. 워크스페이스에 설치한 뒤 .mam.env 에 브로커를 설정 bash deploy/install.sh # 2. 전용 herdr 서버에서 creator 와 reviewer 시작 bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \ --workspace "$PWD" --agent claude --role developer --session creator-claude-01 \ --herdr-session my-project --onboard bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \ --workspace "$PWD" --agent codex --role reviewer --session reviewer-codex-01 \ --herdr-session my-project --onboard # 3. 세션 확인 bash .agents/skills/multi-agent-mux-status/scripts/status.sh # 4. 루프로 작업 실행 bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ --creator creator-claude-01 --all-reviewer --task "Fix the typo in docs/guide.md" ``` --- ## 📝 프로젝트에 합류하는 에이전트에게 1. 역할과 규칙은 [AGENTS.md](AGENTS.md)와 [MULTI_AGENT_RULES.ko.md](.agents/MULTI_AGENT_RULES.ko.md)를 읽으십시오. 2. 긴 출력이 pane 스크롤백 밖으로 사라지지 않도록 `MULTI_AGENT_RULES.ko.md` §4 의 스냅샷 규칙을 따르십시오. 3. 브리프가 지시하는 대로 잡 이벤트를 발행하십시오(`started --heartbeat`, 그다음 `completed` 또는 `error`). 리뷰받은 diff 없이 핵심 로직을 바꾸지 마십시오.