From 9e4f286b43af953c0e1593c6f57fe94e48f7ade8 Mon Sep 17 00:00:00 2001 From: Godopu Date: Sun, 12 Jul 2026 23:20:48 +0900 Subject: [PATCH] refactor: remove legacy multi-agent-mux skills and infrastructure, and add Mattermost notification script and collaboration documentation --- .agents/AGENT.ko.md | 183 ---- .agents/AGENT.md | 183 ---- .agents/skills/lib.sh | 829 ------------------ .../skills/multi-agent-mux-create/SKILL.md | 220 ----- .../scripts/create_session.sh | 316 ------- .../multi-agent-mux-delegate-job/README.md | 11 - .../multi-agent-mux-delegate-job/SKILL.md | 94 -- .../job-protocol.md | 114 --- .../mqtt-broker-setup.md | 176 ---- .../multi-agent-mux-delegate-job | 440 ---------- .../multi-agent-mux-delegate-job/registry.md | 183 ---- .../requirements.txt | 2 - .../scripts/job_subscriber.py | 253 ------ .../scripts/mqtt_common.py | 616 ------------- .../scripts/publish_event.py | 229 ----- .../scripts/registry.py | 420 --------- .../skills/multi-agent-mux-monitor/SKILL.md | 236 ----- .../scripts/reconcile.sh | 644 -------------- .../skills/multi-agent-mux-resume/SKILL.md | 151 ---- .../scripts/resolve_session_id.sh | 40 - .../scripts/update_yaml_resumed.sh | 164 ---- .../skills/multi-agent-mux-status/SKILL.md | 124 --- .../multi-agent-mux-status/scripts/status.sh | 140 --- .agents/skills/multi-agent-mux-stop/SKILL.md | 136 --- .../scripts/stop_session.sh | 353 -------- .mam/agent-sessions.yaml | 88 -- .mam/install_manifest.txt | 34 - BOOTSTRAP.ko.md | 185 ---- BOOTSTRAP.md | 186 ---- DASHBOARD.canvas | 9 - INSTRUCTION.md | 67 -- MESSAGING.md | 364 -------- docs/collaboration_guide.md | 106 +++ gRPC_Based_Interface/DESIGN.md | 20 + gRPC_Based_Interface/IMPL_DESIGN.md | 153 +++- .../docs/agent_card_template.json | 97 ++ paper_draft/DRAFT_PLAN.md | 136 ++- paper_draft/REVIEW.md | 2 +- reviewer_prompt.txt | 13 - scripts/notify_mattermost.py | 138 +++ 40 files changed, 634 insertions(+), 7221 deletions(-) delete mode 100644 .agents/AGENT.ko.md delete mode 100644 .agents/AGENT.md delete mode 100644 .agents/skills/lib.sh delete mode 100644 .agents/skills/multi-agent-mux-create/SKILL.md delete mode 100755 .agents/skills/multi-agent-mux-create/scripts/create_session.sh delete mode 100644 .agents/skills/multi-agent-mux-delegate-job/README.md delete mode 100644 .agents/skills/multi-agent-mux-delegate-job/SKILL.md delete mode 100644 .agents/skills/multi-agent-mux-delegate-job/job-protocol.md delete mode 100644 .agents/skills/multi-agent-mux-delegate-job/mqtt-broker-setup.md delete mode 100755 .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job delete mode 100644 .agents/skills/multi-agent-mux-delegate-job/registry.md delete mode 100644 .agents/skills/multi-agent-mux-delegate-job/requirements.txt delete mode 100755 .agents/skills/multi-agent-mux-delegate-job/scripts/job_subscriber.py delete mode 100644 .agents/skills/multi-agent-mux-delegate-job/scripts/mqtt_common.py delete mode 100755 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py delete mode 100755 .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py delete mode 100644 .agents/skills/multi-agent-mux-monitor/SKILL.md delete mode 100755 .agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh delete mode 100644 .agents/skills/multi-agent-mux-resume/SKILL.md delete mode 100755 .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh delete mode 100755 .agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh delete mode 100644 .agents/skills/multi-agent-mux-status/SKILL.md delete mode 100755 .agents/skills/multi-agent-mux-status/scripts/status.sh delete mode 100644 .agents/skills/multi-agent-mux-stop/SKILL.md delete mode 100755 .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh delete mode 100644 .mam/agent-sessions.yaml delete mode 100644 .mam/install_manifest.txt delete mode 100644 BOOTSTRAP.ko.md delete mode 100644 BOOTSTRAP.md delete mode 100644 DASHBOARD.canvas delete mode 100644 INSTRUCTION.md delete mode 100644 MESSAGING.md create mode 100644 docs/collaboration_guide.md create mode 100644 gRPC_Based_Interface/docs/agent_card_template.json delete mode 100644 reviewer_prompt.txt create mode 100755 scripts/notify_mattermost.py diff --git a/.agents/AGENT.ko.md b/.agents/AGENT.ko.md deleted file mode 100644 index 02804d4..0000000 --- a/.agents/AGENT.ko.md +++ /dev/null @@ -1,183 +0,0 @@ -# AGENT.md - -본 문서는 새로운 프로젝트에 **MQTT 메시징 백플레인 및 Tmux 기반 멀티 에이전트 오케스트레이션 워크플로우**를 도입하고, 협업하는 에이전트들이 일관된 규칙과 아키텍처에 따라 안전하고 견고하게 작업을 수행할 수 있도록 정의한 공통 지침 및 규약입니다. - -새로운 프로젝트에서 작업하는 모든 에이전트는 작업을 시작하기 전 이 문서를 반드시 정독하고 규약을 준수해야 합니다. - ---- - -## 1. 에이전트의 역할 정의 (Agent Roles) - -역할군 간의 책임 및 권한을 명확히 분리하여 병목을 줄이고 작업의 완성도를 높입니다. - -### 👑 General Manager (총괄 매니저) -- **주요 책무**: 사용자와 직접 소통하여 요구사항 접수, 상세 작업 계획 수립, 팀장 에이전트 할당 및 작업 위임, 전체 워크플로우 통제 및 최종 완료 보고. -- **모호성 제거**: 사용자의 요구사항에 모호한 부분이 있다면 작업을 추측하여 진행하지 말고, 즉시 사용자에게 질문하여 명확히 해야 합니다 (`/grill-me` 슬래시 명령어 권장). - -### 👥 Team Leaders (팀장) -새롭게 생성되는 에이전트(`antigravity`, `claude`, `cline`, `hermes` 등)는 각 팀의 **팀장** 역할을 수행합니다. 총괄 매니저로부터 작업을 위임받아 개발 또는 리뷰 워크플로우를 주도합니다. -- **Developer Team Leader (개발 팀장)**: - - 총괄 매니저로부터 작업을 위임받습니다. - - **작업 분석 및 계획**: 주어진 작업을 철저히 분석하고, 작은 단위로 문제를 나누어 세부 계획을 수립합니다. - - **내부 병렬 처리**: 내부적으로 subagent를 활용해 위임받은 작업을 병렬적으로 처리할 수 있습니다. - - **리뷰 타당성 검증 및 거부**: 리뷰어가 지적한 피드백을 면밀히 검토합니다. 타당한 제안은 수렴하여 코드를 수정하지만, 타당하지 않다고 판단되는 안건은 반영하지 않고 **그 명확한 이유를 작성하여 리뷰어에게 되돌려 보냅니다**. - - **완료 신호 송신**: 모든 리뷰어들로부터 `PASS`를 획득하고 변경 사항이 검증되면, 최초 작업을 위임받았던 개발 팀장이 총괄 매니저에게 최종 작업 완료 신호를 송신합니다. -- **Reviewer Team Leader (리뷰어 팀장)**: - - 개발 팀장으로부터 리뷰 요청을 접수합니다. - - **문제 제시에 대한 이유와 개선 방향 포함**: 단순한 반려(`NOT PASS`) 통보는 금지됩니다. 이슈를 제기할 때는 **반드시 해당 문제가 발생하는 구체적인 이유와 확실한 개선 방향(코드 대안 포함)을 함께 작성**해야 합니다. - - **합의 루프**: 모든 지적 사항이 해결되고 최종 `PASS`를 발행할 때까지 리뷰 루프에 동참합니다. - -### 🛡️ 역할 범위 준수 원칙 (Role Suitability Check) -- 모든 에이전트는 자신에게 부여된 역할에 부합하는 작업만을 수행해야 합니다. (예: 개발 팀장은 최종 PASS 여부를 결정하지 않으며, 리뷰어 팀장은 직접 프로젝트 소스코드를 작성하지 않습니다.) -- **자신의 역할에 맞지 않는 작업이 지시된 경우**, 에이전트는 반드시: - 1. 해당 작업을 수행하기에 가장 적합한 에이전트 세션을 추천하여 위임을 유도하거나, - 2. 프로젝트 연속성을 위해 극히 필요한 경우 직접 작업을 수행합니다. - ---- - -## 2. 메시징 백플레인 & 레지스트리 규약 - -에이전트 간의 비동기 소통과 상태 관리는 분산 이벤트 채널 및 파일/DB 레지스트리를 통해 제어됩니다. - -### 📡 MQTT 백플레인 (MQTT Backplane) -- **이벤트 라이프사이클**: - - `started` (작업 개시) ➡️ `progress`/`permission_required` (진행 상황 공유) ➡️ `completed` (성공 종료) 또는 `error` (실패 종료) - - `completed` 및 `error`는 단 한 번만 발행되는 단말(Terminal) 이벤트입니다. -- **메시지 발행/구독 규칙**: - - MQTT는 영속 큐를 보장하지 않으므로, 에이전트 구동 전 **반드시 구독자(`job_subscriber.py`)가 먼저 백그라운드에서 대기**해야 합니다 (Subscribe-before-Publish 원칙). - - 단말 이벤트 발행 시 브로커에 `retain=True`로 영속화하여 늦게 합류한 구독자도 최종 상태를 읽을 수 있도록 조치합니다. - - 전송 데이터에는 비밀번호, 개인키 등의 중요 비밀 정보나 절대 경로가 포함되지 않도록 보편화(Generalised)해야 합니다. - -### 🗃️ 레지스트리 및 상태 관리 -- 본 아키텍처는 목적에 따라 두 가지 레지스트리를 분리하여 운영합니다: - - **잡 레지스트리 (Job Registry)**: 각 비동기 잡의 메타데이터와 생명주기는 개별 JSON 파일(`.mam/jobs/.json`)로 기록되며, 다중 세션 간의 동시 청구(claiming) 경합은 파일 단위의 `fcntl` advisory lock(`registry_lock` via `registry.py`)을 통해 방어합니다. - - **세션 레지스트리 (Session Registry)**: TMUX 모니터링 상태 및 에이전트 구동 정보는 SQLite WAL 데이터베이스(`.mam/agent-sessions.db`)를 통해 단일 호스트 내에서 안정적인 동시 트랜잭션으로 일관되게 제어합니다. 단, SQLite WAL 모드는 NFS(네트워크 파일 시스템) 환경에서는 완전한 파일 락이 보장되지 않으므로 로컬 파일 시스템 사용을 권장합니다. - -### 🛡️ 보안 프로토콜 (HMAC-SHA256) -- **무인증 PoC 모드**: 잡 레지스트리 생성 시 `auth_token`이 `null`로 지정된 경우(PoC 기본 모드), 별도의 서명 검증을 생략하고 모든 이벤트를 수용합니다 (`verify_hmac`이 항상 `True`를 반환). -- **인증 Production 모드**: 실배포 환경이나 인증이 필요한 연동 단계에서는 각 잡마다 고유 암호화 토큰(`auth_token`)을 발급합니다. 퍼블리셔는 이 토큰을 키로 삼아 `hmac_sig` 서명을 페이로드에 동반해야 하며, 수신단(`verify_hmac`)에서 서명이 없거나 일치하지 않는 메시지는 즉시 드랍하여 다운그레이드 공격을 원천 차단합니다. -- **롤아웃 전략**: 보안 스킴 갱신 시 송수신 노드 간 불일치로 인한 이벤트 드랍을 피하기 위해, 과도기적 하이브리드 포맷 전송(평문 유출 위험 있음)을 배제하고 **모든 노드를 일제히 업데이트하는 "동시 롤아웃(Simultaneous Rollout)"**을 채택해야 합니다. - ---- - -## 3. 협업 워크플로우 실행 절차 (Workflow Loop) - -```mermaid -sequenceDiagram - autonumber - actor User as 사용자 - participant GM as General Manager - participant DTL as Developer Team Leader - participant RTL as Reviewer Team Leaders - participant M as MQTT Backplane - - User->>GM: 요구사항 전달 - GM->>DTL: 작업 위임 (예: 랜딩 페이지 제작) - Note over DTL: 작업 분석, 세분화 및 subagent 병렬 구동 - DTL->>M: 'started' 이벤트 발행 - Note over DTL: 코드 변경 및 구현 - DTL->>M: 'completed' 발행 - DTL->>RTL: 리뷰 요청 (랜딩 페이지를 제작했습니다. 리뷰를 진행해주세요) - Note over RTL: 교차 분석 & 검증 - alt 결함 발견 (리뷰어 피드백) - RTL->>DTL: NOT PASS / 피드백 (반드시 이유와 확실한 개선 방향 포함) - Note over DTL: DTL이 피드백의 타당성 검증 - alt 타당한 피드백 - Note over DTL: DTL이 수용하여 코드 수정 - else 타당하지 않은 피드백 - DTL->>RTL: 반론 및 거부 이유 전달 (부적절한 항목 미반영) - end - DTL->>RTL: 재리뷰 요청 (리뷰 안건 수정 완료) - else 검증 통과 - RTL->>DTL: PASS - end - DTL->>GM: 최종 완료 신호 송신 - GM->>User: 사용자에게 작업 완료 통보 -``` - -1. **계획 수립 및 할당**: 총괄 매니저는 개발 팀장에게 작업을 인가합니다. -2. **분석 및 내부 실행**: 개발 팀장은 작업을 분석하고 세분화하여 계획을 세운 뒤 내부 subagent를 가동하여 구현을 완료합니다. 이후 `started`를 거쳐 `completed` 이벤트를 발행하고 리뷰어에게 검수를 요청합니다. -3. **이의 제기 및 정제 루프**: - - 리뷰어 팀장은 상세 피드백 시 반드시 이유와 보완 방향을 제시해야 합니다. - - 개발 팀장은 의견을 검토해 타당하면 수정하고, 타당하지 않으면 반론과 근거를 회신합니다. - - 리뷰어 전원이 `PASS`를 인가할 때까지 이 과정이 반복됩니다. -4. **최종 보고**: 개발 팀장이 총괄 매니저에게 완료 신호를 보내면 총괄 매니저가 사용자에게 완료를 알립니다. - -### 3.1. 에이전트 전담 워크플로우 루프 (Planner-Developer-Reviewer Loop) - -프로젝트 내부의 특정 작업을 수행할 때 다음과 같은 역할 전담 루프를 엄격히 준수합니다: -1. **Planner Agent (기획/설계)**: 세션명 `canary-projects-multi-agent-paper-creator-claude-planner`는 변경 요구사항을 분석하여 `implementation_plan.md`를 포함한 기획 및 설계 문서를 작성/조정하고 계획 수립을 담당합니다. -2. **Developer Agent (개발/수정 - Master Agent)**: Antigravity(Master Agent)는 Planner가 수립한 계획(`implementation_plan.md` 및 `task.md`)에 따라 실제 파일 수정, 테스트 등 구현 작업을 진행합니다. -3. **Reviewer Agents (교차 검증/리뷰)**: 세션명 `canary-projects-multi-agent-paper-creator-claude` 및 `canary-projects-multi-agent-paper-creator-cline`은 수정된 내역에 대해 학술적 정합성과 팩트 검증을 교차 수행하며 피드백 및 `PASS`/`NOT PASS` 판정을 내립니다. -4. **피드백 루프**: 리뷰어가 반려하거나 개선 피드백을 주면, **Planner Agent**에게 피드백을 전달하여 수정 계획을 다시 수립하고 ➡️ **Developer Agent**가 수정 작업을 수행한 뒤 ➡️ **Reviewer Agents**의 최종 `PASS` 승인을 얻을 때까지 반복합니다. - ---- - -## 4. 분석 인프라 패턴 & 실무 가이드 (Infra Patterns) - -장기 실행 에이전트 분석 중 발생하는 유실 및 인프라적 장애를 예방하기 위한 중요 지침입니다. - -### 📸 TUI 뷰포트 절단 방지 (Pane Snapshotting 3대 규칙) -TMUX 환경에서 실행되는 에이전트가 화면 스크롤 한계로 인해 이전 출력이나 장문의 디버깅 로그를 잃지 않도록 아래의 **스냅샷 패턴을 의무적으로 수행**합니다. -1. **Pre-brief Capture**: 작업 지침(Brief)을 전송한 직후, 즉시 해당 세션의 pane을 캡처(`capture-pane -S -200`)해두어 입력 기록의 시작점을 백업합니다. -2. **Loop Snapshot**: 장기 실행(5분 이상) 중인 에이전트 세션의 경우, 주기적으로(예: 30초마다) 뷰포트를 스캔하여 증분 데이터를 `/tmp/pane-snap.txt`에 계속 누적(append) 기록합니다. -3. **Post-job Capture**: 잡 완료/에러 반환 즉시 전체 pane 상태를 마지막으로 캡처하여 전체 작업 궤적을 보존합니다. - -### 📄 장문 브리핑 전달 방식 -- TMUX `send-keys`나 입력 버퍼를 통해 수백 줄의 장문 지시나 프롬프트를 직렬로 입력하면, 에이전트의 TUI가 이를 모두 온전히 소화하지 못하고 일부 문자나 문단이 탈취/누락될 수 있습니다. -- **해결 지침**: 지시 사항이 긴 경우, 반드시 `/tmp/brief-.md` 등의 파일 경로로 지시문을 별도 작성해 전달하고, 에이전트에는 `"Read /tmp/brief-... and execute"` 라는 단순화된 실행 명령만 전달하십시오. - -### ⏱️ 타임아웃 구성 및 정렬 규칙 -- **잡 실행 제한 (`timeout_sec` & `idle_timeout_sec`)**: 각 잡은 전체 실행 만료 시간(`timeout_sec`, 기본 3600s)과 메세지 미수신 유휴 시간(`idle_timeout_sec`, 기본 120s)을 독립적으로 가집니다. -- **모니터 유휴 대기 (`SUB_IDLE_TIMEOUT`)**: 모니터 스크립트(`reconcile.sh`)의 유휴 대기 시간(`SUB_IDLE_TIMEOUT`) 기본값은 잡 최대 예산에 맞춰 `3600s`(1시간) 이상으로 항상 넉넉히 설정해야 합니다. 모니터가 작업 완료 전에 유휴 감지로 조기 자동 종료되어 백그라운드 태스크 관리를 소실하는 문제를 방지하기 위함입니다. - ---- - -## 5. 새 프로젝트 적용 체크리스트 (Setup Checklist) - -새 프로젝트에 이 에이전트 오케스트레이션 모델을 구축할 때의 체크리스트입니다. - -- [ ] **가상환경 의존성**: `pyyaml`, `paho-mqtt` 등 필요한 Python 패키지가 `.venv` 또는 `requirements.txt`에 포함되었는가? -- [ ] **환경 설정 파일**: MQTT 브로커 주소 및 보안 Credential이 `.env` 파일에 안전하게 로드되고 공유되는가? -- [ ] **디렉토리 규약**: 레지스트리 경로(`.mam/jobs/`) 및 로깅 경로(`.mam/delegate_job_logs/`)가 `.gitignore`에 등록되었는가? -- [ ] **스크립트 구비**: `mqtt_common.py`, `publish_event.py`, `job_subscriber.py`, `registry.py` 등의 핵심 모듈이 배치되었는가? -- [ ] **HMAC 활성화**: 새로운 레지스트리 잡 발급 시 난수 기반의 `auth_token`이 정상적으로 주입되고, 서명 기반의 상호 인증이 활성화되는가? -- [ ] **운영 헌장 배치**: 본 규약 파일(`AGENT.md`)이 새 프로젝트의 **.agents/ 디렉터리**에 배치되었는가? (프로젝트 루트를 깔끔하게 유지하면서도 온보딩하는 에이전트들이 규칙을 이해할 수 있도록 `.agents/` 경로 배치가 권장됩니다.) - ---- - -*본 가이드는 협업 효율성과 코드 보안의 엄격한 균형을 유지하기 위한 규범입니다. 변경 사항이 필요한 경우 총괄 매니저 및 전체 팀장의 합의를 거쳐 본 문서를 업데이트해야 합니다.* - ---- - -## 6. AIoT 멀티 에이전트 인터페이스 모듈 설계 지침 (A2A 및 Agent Card) - -향후 연구 및 개발 단계에서 구현할 통합 인터페이스 모듈(Unified Interface Module)은 이종 에이전트 간의 자율 협업 및 상호운용성 확보를 위해 **A2A(Agent-to-Agent)** 표준을 포함하며, 다음의 설계 지침을 철저히 준수해야 합니다. - -### 📇 1. 에이전트 카드(Agent Card)의 표준 지원 -- **규격화된 JSON 스키마**: 인터페이스 모듈은 에이전트의 메타데이터 및 입출력 인터페이스를 정의한 에이전트 카드를 JSON 형태로 자동 생성 및 서빙해야 합니다. -- **필수 명세 항목**: - - 에이전트의 명칭 및 페르소나/역할 (Name, Persona, Role) - - 호출 가능한 엔드포인트 정보 (Endpoint URI 및 프로토콜 규격) - - 입력 데이터 스키마 (Tool Schema 및 입출력 제약사항) - - 보안 인증 및 인가 사양 (Authentication & Security Credentials) -- **배포 및 탐색 규격**: 시스템 내의 다른 이종 에이전트들이 실시간으로 탐색할 수 있도록 `/.well-known/agent-card.json` 표준 경로를 통한 에이전트 카드 서빙 인프라를 내장해야 합니다. - -### 📡 2. A2A 및 ACP 프로토콜 커넥터 설계 -- **프로토콜 상호운용성**: IBM ACP(Agent Communication Protocol)와 Linux Foundation A2A 표준의 병합 사양에 기반하여, 에이전트 간 비동기 위임(Task Delegation), 제어권 이관, 상태 동기화가 가능한 프로토콜 커넥터를 인터페이스 모듈 내에 구현해야 합니다. -- **이벤트 기반 자율 제어**: 상시 센서 모니터링은 경량 물리 노드 및 모니터 프로그램이 수행하도록 분리하고, 감지된 물리 이벤트는 A2A 프로토콜 메시지를 통해 비동기적으로 특정 AI 에이전트에 작업을 자율 위임하고 처리 후 즉시 생명주기 및 제어권을 회수하는 비동기 인터페이스를 탑재해야 합니다. - -### 🛠️ 3. 워크플로우 정합성 및 이슈 추적 -- **실행 일관성 보장**: 이종 에이전트가 동일한 작업을 재수행할 때 동일한 결과를 얻을 수 있도록 워크플로우 실행을 원자적(Atomic)으로 통제해야 합니다. -- **표준 이슈 트래킹**: 실행 예외나 네트워크 단절 등의 장애가 발생한 경우, 복구 및 추적이 용이하도록 표준 규격화된 이슈 추적 인터페이스(Issue Tracking Interface)를 연동 및 마련해야 합니다. - -### 🛡️ 4. 멱등성(Idempotency) 보장 스키마 -- **물리 제어 명령의 멱등성 분류**: 인터페이스 모듈은 전달되는 제어 명령을 멱등성 보장 여부에 따라 분류해야 합니다. 정보 조회·상태 설정 등 멱등성이 자연 보장되는 명령과, 농약 투포·급수·밸브 개폐 등 부작용이 누적되어 재실행 시 치명적 안전 사고로 이어질 수 있는 비멱등성(non-idempotent) 물리 제어 명령을 명시적으로 구분해야 합니다. -- **중복 발행 억제 메커니즘**: 비동기 재시도 루프에서 동일한 제어 의도가 새로운 작업 ID로 중복 발행되는 것을 방지하기 위해, 제어 의도 식별자(Control Intent Key) 기반의 중복 억제(Deduplication) 스키마를 인터페이스 계층에 내장해야 합니다. 재시도 시 신규 작업 ID가 아닌 원본 제어 의도에 바인딩된 멱등성 키를 재사용하여 동일 제어의 재실행 가능성을 시스템 차원에서 통제해야 합니다. -- **물리 장치측 사전 검증**: 중복 제어가 물리 장치에 도달하기 전에 인터페이스 모듈이 사전 검증(pre-flight check)을 수행하여, 이미 실행된 제어 의도에 대한 재실행 요청을 사전 차단하거나 장치측 상태와 대조 후 승인하는 안전망을 제공해야 합니다. - -### ✍️ 5. 학술 용어 및 일관성 통일 지침 (Terminology & Alignment Guidelines) -- **학술적 명확성 확보**: 논문 또는 설계 문서를 작성하거나 편집할 때, 직관적인 구어체 표현(예: '값을 나르는 계층', '행동을 시키고 확인하는 계층')의 사용을 지양하고 정식 학술 용어인 **'센싱 계층(Sensing Layer)'**과 **'제어 계층(Control Layer)'**을 사용해야 합니다. -- **센싱 계층의 정의**: 센싱 계층은 단순히 데이터를 업로드하는 단방향 전송에 그치지 않고, "온도·습도 등의 센서 값을 특정하여 edge 또는 cloud로 전송"하는 명확한 동작으로 정의하여 데이터 흐름을 정합적으로 기술합니다. -- **제어 계층의 정의**: 제어 계층은 "농약 살포"와 같이 물리 계층에 제어 명령을 내리고 그 결과를 검증/확인하는 능동적 행위 계층으로 정의합니다. -- **용어 일관성 유지**: 모든 문서(서론, 배경기술, 설계, 실험 등) 전반에 걸쳐 해당 명칭을 통일되게 사용하여 독자의 이해를 돕고 인지적 혼선을 방지해야 합니다. \ No newline at end of file diff --git a/.agents/AGENT.md b/.agents/AGENT.md deleted file mode 100644 index bba3be5..0000000 --- a/.agents/AGENT.md +++ /dev/null @@ -1,183 +0,0 @@ -# AGENT.md - -This document serves as the common guidelines and protocol for introducing the **MQTT messaging backplane and Tmux-based multi-agent orchestration workflow** to a new project. It defines the rules and architecture to ensure collaborating agents perform tasks safely, robustly, and consistently. - -All agents working on a new project must read this document thoroughly and comply with the defined protocols before starting any tasks. - ---- - -## 1. Agent Roles Definition (Agent Roles) - -We clearly separate responsibilities and permissions between roles to reduce bottlenecks and enhance the quality of execution. - -### 👑 General Manager (Orchestrator) -- **Core Responsibility**: Interact directly with the user, receive high-level requirements, establish task plans, delegate tasks to Team Leaders, control the overall workflow, and report completion back to the user. -- **Ambiguity Resolution**: If a user's requirements contain ambiguous details, do not guess. Immediately ask the user for clarification (we recommend using the `/grill-me` slash command). - -### 👥 Team Leaders (팀장) -Newly spawned agents (e.g., `antigravity`, `claude`, `cline`, `hermes`) act as **Team Leaders** of their respective groups. They receive delegated tasks from the General Manager and manage implementation or review workflows. -- **Developer Team Leader (개발 팀장)**: - - Receives tasks from the General Manager. - - **Task Breakdown & Planning**: Thoroughly analyzes the task, breaks it down into small units, and creates a plan. - - **Internal Parallelism**: Can run subagents in parallel internally to handle the delegated work. - - **Review Integrity & Refusal**: Thoroughly reviews feedback from Reviewers. Adopts/implements recommendations if valid. If any recommendation is judged invalid, the Developer Team Leader must **not** implement it, but instead return the refutation along with detailed reasons to the Reviewer. - - **Completion Signal**: Once all reviewers yield a `PASS` and changes are verified, the Developer Team Leader who first received the task sends a completion signal back to the General Manager. -- **Reviewer Team Leader (리뷰어 팀장)**: - - Receives review requests from the Developer Team Leader. - - **Detailed Feedback with Directions**: Simply rejecting changes (`NOT PASS`) is forbidden. Reviewers **must** specify the exact reason for the issue and provide a concrete, stable, and verified alternative direction for improvement. - - **Consensus Loop**: Engages in the review cycle until all objections are resolved and a final `PASS` is issued. - -### 🛡️ Role Suitability Check Principle (자신의 역할 범위 수행 원칙) -- Every agent must only perform tasks suitable for its designated role (e.g., Developer Team Leaders do not issue final reviews, and Reviewer Team Leaders do not write project code). -- **If an agent receives a task that does not fit its role**, it must either: - 1. Recommend the optimal agent session to delegate the task to, or - 2. Perform the task directly if strictly necessary for project continuity. - ---- - -## 2. Messaging Backplane & Registry Protocol - -Asynchronous communication and state management between agents are controlled via distributed event channels and file/DB registries. - -### 📡 MQTT Backplane -- **Event Lifecycle**: - - `started` (Job execution starts) ➡️ `progress`/`permission_required` (Share intermediate progress) ➡️ `completed` (Successful termination) or `error` (Failed termination) - - `completed` and `error` are terminal events that are published exactly once. -- **Publish/Subscribe Rules**: - - Since MQTT does not guarantee persistent queues, the subscriber (`job_subscriber.py`) **must be running in the background before the agent starts** (the Subscribe-before-Publish principle). - - When publishing terminal events, publish with `retain=True` on the broker so that subscribers joining late can still read the final state. - - Generalize all transmitted data to ensure that sensitive secrets like passwords, private keys, or absolute system paths are not included. - -### 🗃️ Registry & State Management -- This architecture maintains two distinct registries based on their purpose: - - **Job Registry**: The metadata and lifecycle of each asynchronous job are recorded in individual JSON files (`.mam/jobs/.json`). Concurrency conflicts (claiming races) across multiple sessions are prevented via file-based `fcntl` advisory locks (`registry_lock` via `registry.py`). - - **Session Registry**: TMUX monitoring states and running agent metadata are consistently controlled using a SQLite WAL database (`.mam/agent-sessions.db`) to support reliable concurrent transactions on a single host. However, since SQLite WAL mode does not guarantee complete file locking in Network File System (NFS) environments, we recommend using a local file system. - -### 🛡️ Security Protocol (HMAC-SHA256) -- **Unauthenticated PoC Mode**: If the `auth_token` in the job registry is set to `null` (the default PoC mode), signature verification is skipped and all events are accepted (`verify_hmac` always returns `True`). -- **Authenticated Production Mode**: In production environments or integrations requiring authentication, a unique cryptographic token (`auth_token`) is issued for each job. The publisher must include an `hmac_sig` signature in the payload keyed by this token, and the receiving end (`verify_hmac`) will immediately drop messages that lack a signature or have mismatching signatures to prevent downgrade attacks. -- **Rollout Strategy**: To avoid event drops caused by inconsistencies between publishing and receiving nodes when updating security schemes, hybrid transition formats (which risk leaking plaintext tokens) must not be used. Instead, adopt a **"Simultaneous Rollout"** where all nodes are updated at once. - ---- - -## 3. Collaborative Workflow Execution Loop (Workflow Loop) - -```mermaid -sequenceDiagram - autonumber - actor User as User - participant GM as General Manager - participant DTL as Developer Team Leader - participant RTL as Reviewer Team Leaders - participant M as MQTT Backplane - - User->>GM: Hand over requirements - GM->>DTL: Delegate task (e.g., create landing page) - Note over DTL: Analyze, breakdown & spawn parallel subagents - DTL->>M: Publish 'started' event - Note over DTL: Modify code & implement - DTL->>M: Publish 'completed' - DTL->>RTL: Request review (I created landing page. Please review it) - Note over RTL: Cross-analysis & verification - alt Defect Found (Reviewer feedback) - RTL->>DTL: NOT PASS / Feedback (Must include reason & improvement direction) - Note over DTL: DTL checks validity of suggestions - alt Valid feedback - Note over DTL: DTL adopts and modifies code - else Invalid feedback - DTL->>RTL: Send refutation & reasons (Did not reflect inappropriate parts) - end - DTL->>RTL: Request review again (Modified review items) - else Verification Pass - RTL->>DTL: PASS - end - DTL->>GM: Send completion signal - GM->>User: Notify task completion -``` - -1. **Planning and Allocation**: The General Manager delegates the task to the Developer Team Leader. -2. **Analysis and Internal Execution**: The Developer Team Leader analyzes the task, breaks it down, plans execution, and optionally spawns parallel subagents. It publishes `started`, completes the task, and requests review from the Reviewer Team Leader. -3. **Objection & Refinement Loop**: - - The Reviewer Team Leader must provide clear reasons and improvement directions for any issues. - - The Developer Team Leader validates the feedback. Valid suggestions are implemented; invalid ones are refuted with reasons and returned to the reviewer. - - This cycle repeats until all reviewers issue a `PASS`. -4. **Completion and Report**: The Developer Team Leader sends the final completion signal to the General Manager, who notifies the user. - -### 3.1. Designated Agent Workflow Loop (Planner-Developer-Reviewer Loop) - -When performing specific tasks within the project, the following role-based loop must be strictly followed: -1. **Planner Agent (Planning & Design)**: Session name `canary-projects-multi-agent-paper-creator-claude-planner` analyzes requirements, drafts or updates `implementation_plan.md` (and design files), and coordinates the planning phase. -2. **Developer Agent (Development & Implementation - Master Agent)**: Antigravity (Master Agent) carries out the actual file edits, formatting, testing, and implementation tasks based on the plan approved by the Planner (`implementation_plan.md` and `task.md`). -3. **Reviewer Agents (Cross-Verification & Review)**: Session names `canary-projects-multi-agent-paper-creator-claude` and `canary-projects-multi-agent-paper-creator-cline` cross-examine the implemented changes for technical correctness, terminology compliance, and issue PASS/NOT PASS verdicts. -4. **Feedback Loop**: When reviewers raise issues or feed nits, **Planner Agent** processes the feedback to update the plan ➡️ **Developer Agent** implements the updated plan ➡️ **Reviewer Agents** re-verify until a final `PASS` is achieved. - ---- - -## 4. Analysis Infrastructure Patterns & Practical Guide (Infra Patterns) - -These are critical instructions for preventing data loss and infrastructure-level failures during long-running agent analyses. - -### 📸 Preventing TUI Viewport Truncation (The 3 Pane Snapshotting Rules) -To ensure that agents running in TMUX environments do not lose debug logs or previous outputs due to screen scrollback limits, the following **snapshotting pattern must be enforced**: -1. **Pre-brief Capture**: Capture the pane (`capture-pane -S -200`) immediately after sending the task instruction (Brief) to back up the starting point of the input history. -2. **Loop Snapshot**: For long-running agent sessions (5 minutes or more), periodically (e.g., every 30 seconds) scan the viewport and append the incremental data to `/tmp/pane-snap.txt`. -3. **Post-job Capture**: Capture the complete pane state one final time immediately after a job completes or returns an error to preserve the entire execution trajectory. - -### 📄 Handling Long Briefing Instructions -- Sending long instructions or prompts (hundreds of lines) sequentially via TMUX `send-keys` or input buffers can overwhelm the agent's TUI, leading to lost characters or truncated paragraphs. -- **Resolution**: If instructions are long, write them separately to a file path (e.g., `/tmp/brief-.md`) and send a simplified execution command to the agent: `"Read /tmp/brief-... and execute"`. - -### ⏱️ Timeout Configuration & Alignment Rules -- **Job Execution Limits (`timeout_sec` & `idle_timeout_sec`)**: Each job independently manages its overall execution timeout (`timeout_sec`, default 3600s) and idle timeout without receiving messages (`idle_timeout_sec`, default 120s). -- **Monitor Idle Waiting (`SUB_IDLE_TIMEOUT`)**: The idle timeout for the monitor script (`reconcile.sh`), `SUB_IDLE_TIMEOUT`, must always be set generously to `3600s` (1 hour) or more to align with the maximum job budget. This prevents the monitor from terminating early due to idle detection, which would lose control over background tasks before they finish. - ---- - -## 5. Setup Checklist for New Projects (Setup Checklist) - -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`? -- [ ] **Configuration File**: Are the MQTT broker address and security credentials safely loaded and shared via the `.env` file? -- [ ] **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? -- [ ] **HMAC Enablement**: When a new registry job is created, is a random `auth_token` correctly injected, and is signature-based mutual authentication active? -- [ ] **Charter Placement**: Is this protocol file (`AGENT.md`) placed in the **.agents/ directory** of the new project? (Placing it in `.agents/` is essential to keep the project root clean while allowing onboarding agents to align on the rules.) - ---- - -*This guide balances collaboration efficiency with strict code security. Any required changes must be discussed and agreed upon by the General Manager and all Team Leaders before updating this document.* - ---- - -## 6. AIoT Multi-Agent Interface Module Design Guidelines (A2A and Agent Card) - -The Unified Interface Module to be implemented in future research and development stages must incorporate the **A2A (Agent-to-Agent)** standard to achieve autonomous collaboration and interoperability among heterogeneous agents, and must comply with the following design guidelines: - -### 📇 1. Standard Support for Agent Cards -- **Structured JSON Schema**: The interface module must automatically generate and serve Agent Cards defining the agent's metadata and input/output interfaces in JSON format. -- **Mandatory Specifications**: - - Agent Name and Persona/Role (Name, Persona, Role) - - Callable Endpoint Information (Endpoint URI and Protocol Specifications) - - Input Data Schema (Tool Schema and I/O constraints) - - Authentication and Authorization Specifications (Authentication & Security Credentials) -- **Deployment and Discovery Specification**: An Agent Card serving infrastructure must be built-in to serve the metadata via the standard path `/.well-known/agent-card.json`, allowing other heterogeneous agents in the system to discover it in real-time. - -### 📡 2. A2A and ACP Protocol Connector Design -- **Protocol Interoperability**: Implement a protocol connector inside the interface module based on the merged specifications of IBM ACP (Agent Communication Protocol) and Linux Foundation A2A standards to handle task delegation, control transfer, and state synchronization. -- **Event-Driven Autonomous Control**: Decouple sensor monitoring to be executed by lightweight physical nodes and monitoring programs, and implement an asynchronous interface that delegates tasks to specific AI agents upon detecting physical events via A2A protocol messages, reclaiming lifecycles and control rights immediately upon task completion. - -### 🛠️ 3. Workflow Coherence and Issue Tracking -- **Execution Consistency**: Control workflow execution atomically to guarantee identical outputs when heterogeneous agents re-execute the same tasks. -- **Standardized Issue Tracking**: Provide a standardized Issue Tracking Interface to support debugging, tracking, and recovery in case of execution exceptions or network disconnections. - -### 🛡️ 4. Idempotency Assurance Schema -- **Idempotency Classification of Physical Control Commands**: The interface module must classify control commands by their idempotency guarantees. It must explicitly distinguish between commands where idempotency is naturally guaranteed (e.g., information queries, state settings) and non-idempotent physical control commands where side effects accumulate and re-execution may lead to critical safety incidents (e.g., pesticide spraying, irrigation, valve actuation). -- **Duplicate Dispatch Suppression Mechanism**: To prevent the same control intent from being dispatched under a new job ID during asynchronous retry loops, a deduplication schema based on a Control Intent Key must be embedded in the interface layer. Upon retry, the system must reuse the idempotency key bound to the original control intent rather than issuing a new job ID, thereby systemically controlling the possibility of duplicate execution of the same control. -- **Pre-flight Verification at the Interface Layer**: Before a duplicate control reaches the physical device, the interface module must perform a pre-flight check that pre-blocks re-execution requests for already-executed control intents, or approves them only after cross-checking against device-side state, providing a safety net against physical double actuation. - -### ✍️ 5. Terminology & Alignment Guidelines -- **Academic Precision**: When writing or editing papers and design documents, avoid colloquial descriptions (e.g., 'value-delivering layer', 'action-triggering layer') and strictly use formal academic terminology: **'Sensing Layer'** and **'Control Layer'**. -- **Refined Sensing Layer Definition**: The Sensing Layer must be defined not merely as a unidirectional data upload channel, but as a specific operation of "specifying sensor values such as temperature and humidity and transmitting them to the edge or cloud," describing the data flow accurately. -- **Actionable Control Layer Definition**: The Control Layer must be defined as an active layer that issues control commands to the physical layer (e.g., "pesticide spraying") and verifies/validates the execution results. -- **Cross-Section Terminology Alignment**: Ensure that these terms are used consistently across all sections of the document (Introduction, Background, Design, Experiments, etc.) to assist reader comprehension and prevent cognitive confusion. \ No newline at end of file diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh deleted file mode 100644 index f4fb789..0000000 --- a/.agents/skills/lib.sh +++ /dev/null @@ -1,829 +0,0 @@ -#!/usr/bin/env bash -# lib.sh — shared library for the multi-agent-mux-* skills. -# -# Single source of truth for the four things that were inconsistently -# re-implemented across create/resume/delete/monitor (REVIEW.md §4.1): -# - derive_session_name : the tmux session slug (P0-A) -# - atomic_dump_yaml : SQLite db transaction + temp+rename + .bak + validate (P0-B) -# - env_python : env-safe Python (no heredoc injection) (P0-B / P1-B) -# - find_workspace_uuid : workspace-SCOPED resume id lookup (P0-C) -# -# Source it from each script with a path computed from the script location: -# source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" -# -# HARD RULE: the agent-sessions.yaml file is only ever written through -# atomic_dump_yaml. Never `open(yaml_path, 'w')` anywhere else. - -SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WORKSPACE_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}" - -# Workspace-relative defaults with environment overrides (Phase Z) -HOME_DIR="${HOME_DIR:-$HOME}" -CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$HOME/.claude/projects}" -LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}" - -# --------------------------------------------------------------------------- -# Tmux Server Isolation support -# --------------------------------------------------------------------------- -# Paths to exclude when resolving the real tmux binary (shim/wrapper dirs). -_TMUX_SHIM_DIR_PATTERN="${_TMUX_SHIM_DIR_PATTERN:-/multi-agent-tmux-shim/}" -_TMUX_SKILLS_BIN_PATTERN="${_TMUX_SKILLS_BIN_PATTERN:-/.agents/skills/.bin}" - -TMUX_SERVER_NAME="${TMUX_SERVER_NAME:-default}" - -_resolve_real_tmux_path() { - if [ -z "${_REAL_TMUX_PATH:-}" ] || [[ "$_REAL_TMUX_PATH" == *"${_TMUX_SHIM_DIR_PATTERN}"* ]] || [[ "$_REAL_TMUX_PATH" == *"${_TMUX_SKILLS_BIN_PATTERN}"* ]]; then - local dir save_ifs="$IFS" - _REAL_TMUX_PATH="" - IFS=: - for dir in $PATH; do - if [[ "$dir" != *"${_TMUX_SHIM_DIR_PATTERN}"* ]] && [[ "$dir" != *"${_TMUX_SKILLS_BIN_PATTERN}"* ]] && [ -x "$dir/tmux" ]; then - _REAL_TMUX_PATH="$dir/tmux" - break - fi - done - IFS="$save_ifs" - if [ -z "$_REAL_TMUX_PATH" ]; then - _REAL_TMUX_PATH="tmux" - fi - export _REAL_TMUX_PATH - fi -} - -_init_tmux_isolation() { - _resolve_real_tmux_path - if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then - local wrapper_dir="${TMPDIR:-/tmp}${_TMUX_SHIM_DIR_PATTERN}${TMUX_SERVER_NAME}" - if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then - mkdir -p "$wrapper_dir" - cat < "$wrapper_dir/tmux" -#!/usr/bin/env bash -if [ -z "\${TMUX_SERVER_NAME:-}" ] || [ "\$TMUX_SERVER_NAME" = "default" ]; then - exec "$_REAL_TMUX_PATH" "\$@" -else - exec "$_REAL_TMUX_PATH" -L "\$TMUX_SERVER_NAME" "\$@" -fi -EOF - chmod +x "$wrapper_dir/tmux" - export PATH="$wrapper_dir:$PATH" - fi - else - # 격리 비활성화 시 shim 자동 cleanup (PATH에서 제거) - local new_path="" dir save_ifs="$IFS" - IFS=: - for dir in $PATH; do - if [[ "$dir" != *"${_TMUX_SHIM_DIR_PATTERN}"* ]] && [[ "$dir" != *"${_TMUX_SKILLS_BIN_PATTERN}"* ]]; then - if [ -z "$new_path" ]; then - new_path="$dir" - else - new_path="$new_path:$dir" - fi - fi - done - IFS="$save_ifs" - export PATH="$new_path" - fi -} - -_tmux() { - _init_tmux_isolation - if [ -z "${TMUX_SERVER_NAME:-}" ] || [ "$TMUX_SERVER_NAME" = "default" ]; then - "$_REAL_TMUX_PATH" "$@" - else - "$_REAL_TMUX_PATH" -L "$TMUX_SERVER_NAME" "$@" - fi -} - -tmux() { - _tmux "$@" -} - -# --------------------------------------------------------------------------- -# resolve_tmux_server -# -# Query agent-sessions.yaml to find the tmux_server associated with a session. -# Fallback to TMUX_SERVER_NAME or 'default' if not registered or field is missing. -# Prints the resolved server name on stdout. -# --------------------------------------------------------------------------- -resolve_tmux_server() { - local session_name="$1" - SESSION_NAME="$session_name" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF' -import os, sys, sqlite3, json, yaml -name = os.environ['SESSION_NAME'] -yaml_path = os.environ['YAML_PATH'] -db_path = os.path.splitext(yaml_path)[0] + '.db' -try: - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=10.0) - try: - row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone() - if row: - s = json.loads(row[0]) - server = s.get('tmux_server') - if server: - print(server) - sys.exit(0) - except sqlite3.OperationalError: - pass - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: - d = json.loads(row[0]) - for s in d.get('tmux_sessions', []): - if s.get('name') == name: - server = s.get('tmux_server') - if server: - print(server) - sys.exit(0) - conn.close() - elif os.path.exists(yaml_path): - with open(yaml_path) as f: - d = yaml.safe_load(f) or {} - for s in d.get('tmux_sessions', []): - if s.get('name') == name: - server = s.get('tmux_server') - if server: - print(server) - sys.exit(0) -except Exception: - pass -# Fallback -print(os.environ.get('TMUX_SERVER_NAME', 'default')) -PYEOF -} - -# --------------------------------------------------------------------------- -# derive_session_name -# -# THE single source of truth for the tmux session name. Rule: -# slug = the two trailing path components of the absolute workspace, -# '_' -> '-', lowercased, joined with '-' -# name = "-creator-" -# -# Workspace root 기준 상대 해석. 예: -# $WORKSPACE_ROOT/landing_page/refer_landing_page + claude -# -> landing-page-refer-landing-page-creator-claude -# -# Decision (REVIEW P0-A): the actual workspace basename (refer_landing_page) -# IS included. The hand-written historical entry that dropped it -# (lab-landing-page-creator-claude) was the bug, not the convention. -# Every script and SKILL.md must use exactly this rule. -# --------------------------------------------------------------------------- -derive_session_name() { - local workspace="$1" agent="$2" - local abs parent work slug - abs="$(cd "$workspace" 2>/dev/null && pwd)" || abs="$workspace" - parent="$(basename "$(dirname "$abs")" 2>/dev/null || echo "")" - work="$(basename "$abs" 2>/dev/null || echo "root")" - if [ -z "$parent" ] || [ "$parent" = "/" ] || [ "$parent" = "." ]; then - parent="workspace" - fi - if [ -z "$work" ] || [ "$work" = "/" ] || [ "$work" = "." ]; then - work="root" - fi - slug="$(printf '%s-%s' "$parent" "$work" | tr '[:upper:]' '[:lower:]' | tr '_' '-')" - slug="$(printf '%s' "$slug" | tr -cd 'a-zA-Z0-9-')" - printf '%s-creator-%s' "$slug" "$agent" -} - -# --------------------------------------------------------------------------- -# env_python [KEY=VALUE ...] (Python source read from stdin) -# -# Run python3 with the source supplied on stdin via a *quoted* heredoc, so the -# shell never interpolates the source. All values are passed through the -# environment (YAML_PATH plus any KEY=VALUE pairs). Untrusted data (workspace -# paths, capture-pane text) must travel as env vars and be read via os.environ -# inside the script — never spliced into the source. Read-only by convention; -# use atomic_dump_yaml when you need to write the YAML. -# --------------------------------------------------------------------------- -_validate_env_key() { - local key="$1" - if [[ ! "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then - echo "ERROR: Invalid environment variable name: $key" >&2 - return 1 - fi - case "$key" in - LD_PRELOAD|LD_LIBRARY_PATH|PYTHONPATH|PYTHONHOME|PYTHONINSPECT|PYTHONSTARTUP) - echo "ERROR: Blocked environment variable: $key" >&2 - return 1 - ;; - esac - return 0 -} - -env_python() { - local yaml_path="$1"; shift - local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME_DIR" "CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" "LOCAL_BIN=$LOCAL_BIN") - while [ $# -gt 0 ]; do - case "$1" in - *=*) - local key="${1%%=*}" - _validate_env_key "$key" || return 1 - envs+=("$1") - shift - ;; - *) - break - ;; - esac - done - env "${envs[@]}" python3 - "$@" -} - -# --------------------------------------------------------------------------- -# atomic_dump_yaml [KEY=VALUE ...] (mutation source from stdin) -# -# The ONLY sanctioned way to write agent-sessions.yaml. It: -# 1. takes an exclusive SQLite BEGIN IMMEDIATE transaction lock on -# agent-sessions.db (serialises all writers) -# 2. loads the current state into `d` (seeds from YAML if DB is empty) -# 3. exec()s the caller's mutation source (sees d, yaml, os, datetime, -# timezone, glob, subprocess; reads values via os.environ). The mutation -# may print and may `raise SystemExit(n)` to abort *without* writing. -# 4. validates the resulting schema -# 5. backs up to .bak, then writes YAML atomically (temp + os.replace) -# when a session transitions to a finished state. -# -# The mutation source is passed via env and exec()'d — it is never string -# spliced and untrusted data never lands in Python source (P0-B / P1-B). -# --------------------------------------------------------------------------- -# Check if the workspace is on NFS — locking behaves differently on NFS -_check_is_nfs() { - local f="$1" - local mountpoint - mountpoint="$(df --output=target "$f" 2>/dev/null | tail -1)" || return 1 - if mount | grep -q "$mountpoint.*nfs\|$mountpoint.*cifs\|$mountpoint.*fuse.sshfs"; then - return 0 # is NFS - fi - return 1 # not NFS -} - -atomic_dump_yaml() { - local yaml_path="$1"; shift - if [ -z "${MAM_IS_NFS:-}" ]; then - if _check_is_nfs "$(dirname "$yaml_path")"; then - export MAM_IS_NFS="true" - echo "WARNING: $(dirname "$yaml_path") appears to be a network filesystem (NFS/CIFS/SSHFS)." >&2 - echo "WARNING: SQLite journal_mode automatically falls back to DELETE." >&2 - else - export MAM_IS_NFS="false" - fi - fi - - local -a envs=("YAML_PATH=$yaml_path" "HOME_DIR=$HOME_DIR" "CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" "LOCAL_BIN=$LOCAL_BIN" "MAM_IS_NFS=$MAM_IS_NFS") - while [ $# -gt 0 ]; do - case "$1" in - *=*) - local key="${1%%=*}" - _validate_env_key "$key" || return 1 - envs+=("$1") - shift - ;; - *) - break - ;; - esac - done - local mutation; mutation="$(cat)" - env "${envs[@]}" AGENT_SESSIONS_MUTATION="$mutation" python3 - <<'PYEOF' -import os, sys, tempfile, shutil, glob, subprocess, json, sqlite3 -from datetime import datetime, timezone -import yaml - -yaml_path = os.environ['YAML_PATH'] -db_path = os.path.splitext(yaml_path)[0] + '.db' - -def _validate(d): - if not isinstance(d, dict): - raise SystemExit("VALIDATE: top-level is not a mapping") - sessions = d.get('tmux_sessions', []) - if not isinstance(sessions, list): - raise SystemExit("VALIDATE: tmux_sessions is not a list") - valid = {'running', 'terminated', 'archived', 'stopped'} - for i, s in enumerate(sessions): - if not isinstance(s, dict): - raise SystemExit(f"VALIDATE: tmux_sessions[{i}] not a mapping") - if not s.get('name') or not s.get('status'): - raise SystemExit(f"VALIDATE: tmux_sessions[{i}] missing name/status") - if s.get('role') is not None and (not isinstance(s['role'], str) or not s['role'].strip()): - raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} role must be a non-empty string") - if s['status'] not in valid: - raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} bad status {s['status']!r}") - if not isinstance(s.get('pane'), dict): - raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} missing pane") - -def get_terminal_set(d): - return {s.get('name'): s.get('status') for s in d.get('tmux_sessions', []) if s.get('status') in ('stopped', 'terminated', 'archived')} - -os.makedirs(os.path.dirname(db_path) or '.', exist_ok=True) -conn = sqlite3.connect(db_path, timeout=60.0) - -for f in [db_path, db_path + '-wal', db_path + '-shm']: - if os.path.exists(f): - try: - os.chmod(f, 0o600) - except Exception: - pass - -is_nfs = os.environ.get('MAM_IS_NFS') == 'true' -if is_nfs: - conn.execute('PRAGMA journal_mode=DELETE') -else: - conn.execute('PRAGMA journal_mode=WAL') - -try: - # Disable auto-commit by explicitly starting a transaction with BEGIN IMMEDIATE - # This prevents the read-modify-write lost update race condition. - conn.execute('BEGIN IMMEDIATE') - conn.execute('CREATE TABLE IF NOT EXISTS state (id INTEGER PRIMARY KEY, data TEXT)') - conn.execute('CREATE TABLE IF NOT EXISTS sessions (name TEXT PRIMARY KEY, status TEXT, pane_cwd TEXT, data JSON)') - conn.execute('CREATE INDEX IF NOT EXISTS idx_sessions_pane_cwd ON sessions(pane_cwd)') - - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: - d = json.loads(row[0]) - else: - # Seed from YAML - if os.path.exists(yaml_path): - with open(yaml_path) as f: - d = yaml.safe_load(f) or {} - else: - d = {} - - # Assemble d['tmux_sessions'] from sessions table if table contains data - db_sessions = [] - cursor = conn.execute('SELECT name, status, pane_cwd, data FROM sessions') - for s_row in cursor.fetchall(): - s_data = json.loads(s_row[3]) - s_data['name'] = s_row[0] - s_data['status'] = s_row[1] - if 'pane' not in s_data: - s_data['pane'] = {} - s_data['pane']['cwd'] = s_row[2] - db_sessions.append(s_data) - - if db_sessions: - d['tmux_sessions'] = db_sessions - elif 'tmux_sessions' not in d: - d['tmux_sessions'] = [] - - old_terminals = get_terminal_set(d) - old_roles = {s.get('name'): s.get('role') for s in db_sessions if s.get('role')} - - # --- caller mutation (module scope: sees d, yaml, os, glob, subprocess) --- - exec(compile(os.environ['AGENT_SESSIONS_MUTATION'], '', 'exec'), globals()) - - # Role immutability check - for s in d.get('tmux_sessions', []): - name = s.get('name') - if name in old_roles and s.get('role') != old_roles[name]: - raise SystemExit(f"VALIDATE: role of session {name!r} cannot be modified from {old_roles[name]!r} to {s.get('role')!r}") - - _validate(d) - - # Separate globals and sessions for normalization - d_state = {k: v for k, v in d.items() if k != 'tmux_sessions'} - conn.execute('REPLACE INTO state (id, data) VALUES (1, ?)', (json.dumps(d_state),)) - - current_names = [] - for s in d.get('tmux_sessions', []): - name = s.get('name') - status = s.get('status') - pane_cwd = (s.get('pane') or {}).get('cwd', '') - conn.execute('REPLACE INTO sessions (name, status, pane_cwd, data) VALUES (?, ?, ?, ?)', - (name, status, pane_cwd, json.dumps(s))) - current_names.append(name) - - if current_names: - placeholders = ','.join('?' for _ in current_names) - conn.execute(f'DELETE FROM sessions WHERE name NOT IN ({placeholders})', current_names) - else: - conn.execute('DELETE FROM sessions') - - new_terminals = get_terminal_set(d) - - conn.commit() - - # Write to YAML ONLY when a session transitions to a finished state - # (Moved after conn.commit() per Claude's feedback) - if new_terminals != old_terminals: - if os.path.exists(yaml_path): - try: - shutil.copy2(yaml_path, yaml_path + '.bak') - except Exception: - pass - dir_ = os.path.dirname(yaml_path) or '.' - fd, tmp = tempfile.mkstemp(dir=dir_, prefix='.agent-sessions.', suffix='.tmp') - try: - with os.fdopen(fd, 'w') as f: - yaml.safe_dump(d, f, default_flow_style=False, sort_keys=False, - allow_unicode=True, width=4096) - os.replace(tmp, yaml_path) - except Exception: - if os.path.exists(tmp): - os.remove(tmp) - raise - - try: - conn.execute('PRAGMA wal_checkpoint(TRUNCATE)') - except Exception: - pass -except Exception: - conn.rollback() - raise -finally: - conn.close() - - # H3: Re-apply chmod 0600 after close to cover newly created -wal / -shm files - try: - os.chmod(db_path, 0o600) - wal = db_path + '-wal' - if os.path.exists(wal): os.chmod(wal, 0o600) - shm = db_path + '-shm' - if os.path.exists(shm): os.chmod(shm, 0o600) - except Exception: - pass -PYEOF -} - - -# --------------------------------------------------------------------------- -# find_workspace_uuid -# -# Workspace-SCOPED resolution of the resume UUID (P0-C). It NEVER returns a -# global agent_identities id unless that id's project_cwd matches THIS -# workspace. Resolution order: -# 1) tmux_sessions[] row whose pane.cwd == this workspace -> per-row own id -# (claude_session_id_own / agy_conversation_id_own) -# 2) on-disk scan scoped to this workspace -# (claude: ~/.claude/projects//*.jsonl ; agy: last_conversations.json[cwd]) -# 3) agent_identities cache, ONLY when its project_cwd == this workspace -# Prints the UUID on stdout (empty line if none). Always exits 0. -# --------------------------------------------------------------------------- -find_workspace_uuid() { - local workspace="$1" agent="$2" - local abs; abs="$(cd "$workspace" 2>/dev/null && pwd)" || abs="$workspace" - WS_ABS="$abs" AGENT="$agent" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF' -import os, json, glob, sqlite3 -import yaml - -ws = os.environ['WS_ABS'] -agent = os.environ['AGENT'] -home = os.environ['HOME_DIR'] -yaml_path = os.environ['YAML_PATH'] -db_path = os.path.splitext(yaml_path)[0] + '.db' -claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") - -def jsonl_exists(uuid): - key = ws.replace('/', '-').replace('_', '-') - return os.path.exists(f"{claude_project_dir}/{key}/{uuid}.jsonl") - - -def db_exists(uuid): - return os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{uuid}.db") - - -def hermes_exists(uuid): - hdb = f"{home}/.hermes/state.db" - if not os.path.exists(hdb): - return False - try: - conn = sqlite3.connect(hdb) - r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (uuid,)).fetchone() - conn.close() - return r is not None - except Exception: - return False - - -def cline_exists(uuid): - return os.path.exists(f"{home}/.cline/data/sessions/{uuid}/{uuid}.json") - - -def emit(u): - print(u) - raise SystemExit(0) - - -# 1) per-row own id for THIS workspace (optimized with direct sqlite query if db exists) -sessions = [] -try: - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=10.0) - has_sessions_table = False - try: - cursor = conn.execute('SELECT data FROM sessions WHERE pane_cwd=?', (ws,)) - for row in cursor.fetchall(): - sessions.append(json.loads(row[0])) - has_sessions_table = True - except sqlite3.OperationalError: - pass - if not has_sessions_table or not sessions: - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: - d = json.loads(row[0]) - for s in d.get('tmux_sessions', []): - if isinstance(s, dict) and (s.get('pane') or {}).get('cwd') == ws: - sessions.append(s) - conn.close() - elif os.path.exists(yaml_path): - with open(yaml_path) as f: - d = yaml.safe_load(f) or {} - for s in d.get('tmux_sessions', []): - if isinstance(s, dict) and (s.get('pane') or {}).get('cwd') == ws: - sessions.append(s) -except Exception: - pass - -for s in sessions: - name = s.get('name', '') - if agent == 'claude' and name.endswith('-creator-claude'): - cand = s.get('claude_session_id_own') - if cand and jsonl_exists(cand): - emit(cand) - if agent == 'agy' and name.endswith('-creator-agy'): - cand = s.get('agy_conversation_id_own') - if cand and db_exists(cand): - emit(cand) - if agent == 'hermes' and name.endswith('-creator-hermes'): - cand = s.get('hermes_conversation_id_own') - if cand and hermes_exists(cand): - emit(cand) - if agent == 'cline' and name.endswith('-creator-cline'): - cand = s.get('cline_conversation_id_own') - if cand and cline_exists(cand): - emit(cand) - -# 2) disk scan scoped to THIS workspace -if agent == 'claude': - key = ws.replace('/', '-').replace('_', '-') - proj = f"{claude_project_dir}/{key}" - if os.path.isdir(proj): - for j in sorted(glob.glob(f"{proj}/*.jsonl"), key=os.path.getmtime, reverse=True): - sid = None - try: - with open(j) as f: - first = f.readline().strip() - if first: - sid = json.loads(first).get('sessionId') - except Exception: - sid = None - cand = sid or os.path.basename(j)[:-6] - if cand and jsonl_exists(cand): - emit(cand) -elif agent == 'agy': - lc = f"{home}/.gemini/antigravity-cli/cache/last_conversations.json" - if os.path.exists(lc): - cand = None - try: - cand = json.load(open(lc)).get(ws) - except Exception: - cand = None - if cand and db_exists(cand): - emit(cand) -elif agent == 'hermes': - hdb = f"{home}/.hermes/state.db" - if os.path.exists(hdb): - cand = None - try: - conn = sqlite3.connect(hdb) - r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (ws,)).fetchone() - conn.close() - if r: - cand = r[0] - except Exception: - cand = None - if cand: - emit(cand) -elif agent == 'cline': - sessions_dir = f"{home}/.cline/data/sessions" - if os.path.isdir(sessions_dir): - candidates = [] - for session_folder in glob.glob(f"{sessions_dir}/*"): - if os.path.isdir(session_folder): - folder_name = os.path.basename(session_folder) - json_file = f"{session_folder}/{folder_name}.json" - if os.path.exists(json_file): - candidates.append(json_file) - candidates.sort(key=os.path.getmtime, reverse=True) - for j in candidates: - try: - with open(j) as f: - sdata = json.load(f) - if sdata.get('cwd') == ws or sdata.get('workspace_root') == ws: - sid = sdata.get('session_id') - if sid: - emit(sid) - except Exception: - pass - -# 3) agent_identities cache, ONLY when its project_cwd == this workspace -ai = {} -try: - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=10.0) - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: - ai = json.loads(row[0]).get('agent_identities', {}) - conn.close() - elif os.path.exists(yaml_path): - with open(yaml_path) as f: - d = yaml.safe_load(f) or {} - ai = d.get('agent_identities', {}) -except Exception: - pass - -ai_agent = ai.get(agent) or {} -if ai_agent.get('project_cwd') == ws: - if agent == 'claude': - cand = ai_agent.get('session_id') - if cand and jsonl_exists(cand): - emit(cand) - elif agent == 'agy': - cand = ai.get('conversation_id') - if cand and db_exists(cand): - emit(cand) - elif agent == 'hermes': - cand = ai_agent.get('session_id') or ai.get('conversation_id') - if cand and hermes_exists(cand): - emit(cand) - elif agent == 'cline': - cand = ai_agent.get('session_id') or ai.get('conversation_id') - if cand and cline_exists(cand): - emit(cand) - -print('') -PYEOF -} - -# --------------------------------------------------------------------------- -# capture_conversation_id -# -# Thin wrapper over find_workspace_uuid: resolves THIS workspace's conversation -# id (claude jsonl sessionId / agy db uuid) and prints it on stdout (empty line -# if none). find_workspace_uuid is already a workspace-scoped, 3-tier, race-free -# resolver (per-row own id -> workspace-scoped disk scan -> cwd-matched cache), -# so recording its result into the row before kill guarantees tier-1 on the next -# resume. Always exits 0. -# --------------------------------------------------------------------------- -capture_conversation_id() { - local agent="$1" workdir="$2" - find_workspace_uuid "$workdir" "$agent" -} - -# --------------------------------------------------------------------------- -# is_already_stopped -# -# Exits 0 if the row's status is 'stopped' (printing "stopped_at=" on -# stdout), 1 otherwise (including not-found). Used for idempotency: a second -# stop on an already-stopped session is a no-op. -# --------------------------------------------------------------------------- -is_already_stopped() { - local session_name="$1" - SESSION_NAME="$session_name" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF' -import os, yaml, sqlite3, json -name = os.environ['SESSION_NAME'] -yaml_path = os.environ['YAML_PATH'] -db_path = os.path.splitext(yaml_path)[0] + '.db' -try: - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=10.0) - has_sessions_table = False - try: - row = conn.execute('SELECT status, data FROM sessions WHERE name=?', (name,)).fetchone() - if row: - status, s_data_str = row[0], row[1] - if status == 'stopped': - s = json.loads(s_data_str) - print(f"stopped_at={s.get('stopped_at', '?')}") - raise SystemExit(0) - has_sessions_table = True - except sqlite3.OperationalError: - pass - if not has_sessions_table: - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: - d = json.loads(row[0]) - for s in d.get('tmux_sessions', []): - if s.get('name') == name and s.get('status') == 'stopped': - print(f"stopped_at={s.get('stopped_at', '?')}") - raise SystemExit(0) - conn.close() - raise SystemExit(1) - elif os.path.exists(yaml_path): - with open(yaml_path) as f: - d = yaml.safe_load(f) or {} - for s in d.get('tmux_sessions', []): - if s.get('name') == name and s.get('status') == 'stopped': - print(f"stopped_at={s.get('stopped_at', '?')}") - raise SystemExit(0) -except Exception: - pass -raise SystemExit(1) -PYEOF -} - -# --------------------------------------------------------------------------- -# multi-agent-mux-delegate-job integration helpers -# -# All paths are resolved relative to lib.sh's own location (BASH_SOURCE), so the -# skill tree is relocatable — no hardcoded absolute paths (review item 6). -# --------------------------------------------------------------------------- - -# _delegate_py_bin — echo the virtualenv python (walk up from .agents/skills/), else python3. -_delegate_py_bin() { - # Return cached result if available (shell variable, not exported — avoids cross-workspace pollution) - if [ -n "${AGENT_PYTHON_BIN:-}" ] && [ -x "$AGENT_PYTHON_BIN" ]; then - printf '%s\n' "$AGENT_PYTHON_BIN"; return 0 - fi - local d - d="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - while [ "$d" != "/" ] && [ -n "$d" ]; do - if [ -x "$d/.venv/bin/python" ]; then - AGENT_PYTHON_BIN="$d/.venv/bin/python" - printf '%s\n' "$AGENT_PYTHON_BIN"; return 0 - fi - d="$(dirname "$d")" - done - AGENT_PYTHON_BIN="$(command -v python3 || echo python3)" - printf '%s\n' "$AGENT_PYTHON_BIN" -} - -# _delegate_script — echo the path to a multi-agent-mux-delegate-job script, resolved -# relative to .agents/skills/ (lib.sh dir). Empty if not found. -_delegate_script() { - local name="$1" skill_dir cand - skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - cand="$skill_dir/multi-agent-mux-delegate-job/scripts/$name" - if [ -f "$cand" ]; then printf '%s\n' "$cand"; return 0; fi - printf '%s\n' "$(find "$skill_dir" -name "$name" 2>/dev/null | head -n 1 || true)" -} - -# delegate_submit_job -# -# Register a job in the multi-agent-mux-delegate-job registry. Prints the new JID on stdout. -delegate_submit_job() { - local prompt="$1" agent="$2" session="$3" - local py_bin registry_py - py_bin="$(_delegate_py_bin)" - registry_py="$(_delegate_script registry.py)" - if [ -z "$registry_py" ] || [ ! -f "$registry_py" ]; then - echo "ERROR: multi-agent-mux-delegate-job registry.py not found under .agents/skills/" >&2 - return 1 - fi - "$py_bin" "$registry_py" register \ - --prompt "$prompt" \ - --agent "$agent" \ - --agent-session "$session" -} - -# delegate_publish_event [detail] -# -# Publish a lifecycle event to the multi-agent-mux-delegate-job registry. Consolidates the -# inline .venv-walk + publish_event.py blocks that were duplicated across -# create/delete/resume (review item 7). Non-fatal by contract: an empty job id, -# a missing script, or a broker failure never aborts the caller. -delegate_publish_event() { - local job_id="$1" event="$2" detail="${3:-}" - [ -n "$job_id" ] || return 0 - local py_bin pub - py_bin="$(_delegate_py_bin)" - pub="$(_delegate_script publish_event.py)" - [ -n "$pub" ] && [ -f "$pub" ] || return 0 - "$py_bin" "$pub" --job "$job_id" --event "$event" --detail "$detail" || true -} - -# start_watchdog [workdir] -# Spawns a watchdog process to monitor a delegate-job JOB in the background. -# The watchdog re-spawns the subscriber every 2 minutes (or whatever hard -# limit we set) and exits automatically when the JOB reaches terminal state. -# Returns the watchdog PID via stdout. -start_watchdog() { - local job_id="$1" - local workdir="${2:-$PWD}" - local monitor_script="$workdir/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh" - local log_file="$workdir/.mam/multi-agent-mux-monitor.log" - - if [ ! -f "$monitor_script" ]; then - echo "ERROR: monitor script not found: $monitor_script" >&2 - return 1 - fi - - # Check if reconcile.sh --subscribe is already running on this workspace - local pid - pid=$(pgrep -f "bash $monitor_script --subscribe" || true) - - if [ -z "$pid" ]; then - # Start the wildcard monitor subscriber daemon with --idle-timeout 0 (never idle out) - # and ensure it runs with $workdir as cwd to anchor relative log paths. - local orig_pwd="$PWD" - cd "$workdir" - nohup bash "$monitor_script" --subscribe --idle-timeout 0 >> "$log_file" 2>&1 & - pid=$! - cd "$orig_pwd" - fi - - echo "$pid" -} - - diff --git a/.agents/skills/multi-agent-mux-create/SKILL.md b/.agents/skills/multi-agent-mux-create/SKILL.md deleted file mode 100644 index 76cacfb..0000000 --- a/.agents/skills/multi-agent-mux-create/SKILL.md +++ /dev/null @@ -1,220 +0,0 @@ ---- -name: multi-agent-mux-create -description: "Create a new agent session (claude, antigravity/agy) in a dedicated tmux session for context-preserving long-running work. Always creates a tmux session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace." -version: 1.0.0 -author: godopu -license: MIT -platforms: [linux, macos] -environments: [terminal, tmux] -metadata: - hermes: - tags: [agent, tmux, claude, antigravity, agy, multi-agent, context, session] - related_skills: [multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-monitor, claude-code] - prereq_skills: [claude-code] ---- - -# Multi-Agent Create — Start a Fresh Agent in a tmux Session - -> **Companion skills**: `multi-agent-mux-resume` (resume an existing UUID), `multi-agent-mux-stop` (terminate), `multi-agent-mux-monitor` (live status). -> **Single source of truth**: `./.mam/agent-sessions.yaml` (this skill writes to it; never read it ad-hoc — go through this skill). - -## What this skill does - -Spawn a new agent (`claude` or `agy`/antigravity-cli) in a **dedicated tmux session** for context-preserving long-running work. The tmux session is the *container*; the agent's session ID is *data* inside the container. **This skill creates the container + starts the agent — but does not resume an old conversation** (use `multi-agent-mux-resume` for that). - -For all agents: the tmux session name is produced by **`lib.sh::derive_session_name`** — the single source of truth shared by create/resume/stop/status/monitor (P0-A). The rule (verbatim from the function): - -> slug = the **two trailing path components** of the absolute workspace, `_`→`-`, lowercased, joined with `-`; name = `-creator-`. - -So `$WORKSPACE_ROOT/landing_page/refer_landing_page` + `claude` → `landing-page-refer-landing-page-creator-claude`. The workspace basename (`refer_landing_page`) **is** included; the hand-written historical entry that dropped it (`lab-landing-page-creator-claude`) was the bug, not the convention. - -## Pre-flight checks - -Before doing anything, verify the environment: - -```bash -# 1) tmux available and isolated server status -command -v tmux || { echo "ERROR: tmux not installed"; exit 1; } -echo "Tmux server name: ${TMUX_SERVER_NAME:-default}" - -# 2) claude / agy available -command -v claude # required for --agent claude -command -v agy # required for --agent agy - -# 3) claude auth (if --agent claude) -claude auth status 2>&1 | python3 -c "import json,sys; d=json.load(sys.stdin); assert d.get('loggedIn'), 'claude not logged in'" - -# 4) target workspace exists -test -d "$WORKSPACE" || { echo "ERROR: workspace $WORKSPACE not a directory"; exit 1; } -``` - -If any check fails → `kanban_block(reason="...")` (worker path) or report to user (interactive path). Do not proceed with a half-broken setup. - -## Standard names - -- **tmux session name**: `derive_session_name ` (lib.sh) - - `` = `basename $(dirname $WORKSPACE)` `-` `basename $WORKSPACE` (lowercase, `_`→`-`) - - examples: `landing-page-refer-landing-page-creator-claude`, `paper-pdf2md-creator-agy` - - never re-derive this by hand — source lib.sh and call the function -- **wrapper script** (claude only): `~/.local/bin/-creator-claude` - - contents: tmux new-session with `claude` inside, auto-handles trust/bypass dialogs - - see `/agent_sessions.md` for the canonical wrapper template - -## Tmux Server Isolation (격리 서버) - -When running multiple agent sessions alongside other workflows (e.g., cmux, Kanban workers, manual tmux sessions), sharing the default tmux server can lead to session name conflicts, monitoring clutter, and accidental destruction of user sessions via global commands. - -To prevent this, you can run this skill inside an **isolated tmux server** using the `TMUX_SERVER_NAME` environment variable or the `--tmux-server ` flag (opt-in). - -### How to use -1. **Via Environment Variable**: - ```bash - export TMUX_SERVER_NAME=multi-agent-canary - # All subsequent commands (create, status, stop, etc.) will run in the isolated 'multi-agent-canary' tmux server. - ``` -2. **Via Option Flag**: - ```bash - bash scripts/create_session.sh --workspace /path/to/project --agent claude --role developer --tmux-server multi-agent-canary - ``` -3. **Submit Job Integration**: - You can automatically register a delegated job with a prompt when creating a session: - ```bash - bash scripts/create_session.sh --workspace /path/to/project --agent claude --role developer --submit-job "Task prompt here" - ``` - -### Recommended Alias -You can set an alias in your shell to easily query sessions on the isolated server: -```bash -alias tmc='tmux -L multi-agent-canary' -tmc ls # Lists only your multi-agent sessions -``` - -### Safety Rules (Pitfall 29 Summary) -- Never use global server termination commands like `tmux kill-server` or `tmux kill-session -a` as they will destroy all sessions on that server (including your own workspace sessions if they share the server). -- By using an isolated server via `TMUX_SERVER_NAME`, your agent sessions are completely separated from your default user workspace, ensuring 0% interference. - -## Workflow - -```bash -WORKSPACE=/path/to/project -AGENT=claude # or agy -source .agents/skills/lib.sh -SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")" - -# 1. If session already alive, fail fast -tmux has-session -t "$SESSION_NAME" 2>/dev/null && { - echo "ERROR: tmux session '$SESSION_NAME' already exists. Use multi-agent-mux-resume to attach or multi-agent-mux-stop first." - exit 1 -} - -# 2. Spawn the tmux session with the agent inside -case "$AGENT" in - claude) - # Use the wrapper if it exists, else inline tmux new-session - # Use the wrapper if it exists (LOCAL_BIN env var overrides default $HOME/.local/bin) - local_bin="${LOCAL_BIN:-$HOME/.local/bin}" - if [ -x "$local_bin/$SESSION_NAME" ]; then - nohup "$local_bin/$SESSION_NAME" >/dev/null 2>&1 & - else - tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "claude" - fi - ;; - agy) - tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions" - ;; - *) echo "ERROR: --agent must be claude or agy, got: $AGENT"; exit 2 ;; -esac - -# 3. Wait for agent TUI to be ready (varies: claude ~5s, agy ~3s) -sleep 6 - -# 4. Capture pane metadata -PANE_PID=$(tmux list-panes -t "$SESSION_NAME" -F '#{pane_pid}') -PANE_CWD=$(tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_path}') -PANE_CMD=$(tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_command}') -TMUX_EPOCH=$(tmux list-sessions -F '#{session_created}' -t "$SESSION_NAME" 2>/dev/null | head -1) -``` - -## Registering the session in agent-sessions.yaml - -After spawn, append a new `tmux_sessions[]` entry to `.mam/agent-sessions.yaml`: - -```yaml -- name: - status: running - tmux_session_created_at: 2026-06-17T...Z # ISO 8601 UTC - tmux_session_epoch: - tmux_server: # Isolated server name (default: 'default') - pane: - index: 0 - pid: - cmd: # 'claude' or 'agy' - cmd_full: - cwd: - tui: # only for claude - model: - provider: - plan: - account: - version: - start_command: - attach_command: "tmux attach -t " - kill_command: "tmux kill-session -t " -``` - -`cmd_full` per agent (this is the actual command line in the pane, not the resume command): - -| agent | cmd_full | -|---|---| -| claude (interactive) | `claude` | -| agy (interactive) | `agy --dangerously-skip-permissions` | - -Use the `agent-sessions-yaml-edit` script in `scripts/` to safely append (preserves comments + format): - -```bash -bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \ - --workspace "$WORKSPACE" --agent "$AGENT" --role "$ROLE" --session "$SESSION_NAME" -``` - -The script handles the YAML append, pane capture, and the `last_visible_status` placeholder. - -## Pitfalls - -- **Don't use `nohup`/`disown`/`setsid` for the agent itself** — those background the agent outside tmux. The whole point of this skill is *the tmux session is the supervisor*. `nohup` is OK only for *launching the wrapper* (which itself creates the tmux session via `tmux new-session -d`). -- **Don't trust `--session-id ` flags blindly** — claude/agy may not accept a fixed session id on first spawn. The session id is *assigned* on first user message; you can read it back from `~/.claude/projects/.../session.jsonl` headers or `~/.gemini/.../cache/last_conversations.json` AFTER the first message. -- **Wrapper script MUST NOT be created via `hermes profile alias`** — that command writes a `hermes -p ` wrapper that destroys the tmux behavior. Create wrappers manually (see `lab-landing-page-creator-claude` template). -- **Always use the workspace-relative path** in tmux `cwd` — relative paths break when tmux respawns in a different shell context. -- **The first `claude` message generates the session id** — `multi-agent-mux-create` only sets up the *container*. If you need a known session id for later resume, send a placeholder message (e.g. "init") and read it back, then call `multi-agent-mux-resume` later. - -## Verification - -After spawn + YAML append: - -```bash -# 1. tmux session is alive -tmux has-session -t "$SESSION_NAME" && echo OK || echo MISSING - -# 2. pane has the expected cmd + cwd -tmux list-panes -t "$SESSION_NAME" -F 'cmd=#{pane_current_command} cwd=#{pane_current_path}' - -# 3. agent-sessions.yaml has the new entry -python3 -c " -import yaml -d = yaml.safe_load(open('.mam/agent-sessions.yaml')) -names = [s['name'] for s in d['tmux_sessions']] -assert '$SESSION_NAME' in names, 'session not registered' -print('OK:', names) -" - -# 4. Optional: send a probe via tmux send-keys and capture-pane -tmux send-keys -t "$SESSION_NAME" "" Enter -sleep 2 -tmux capture-pane -t "$SESSION_NAME" -p -S -20 -``` - -## When NOT to use this skill - -- **Resuming an old conversation** → `multi-agent-mux-resume` -- **Killing an existing session** → `multi-agent-mux-stop` -- **Just attaching to an existing session** → `tmux attach -t ` (no skill needed) -- **One-shot print mode (claude -p "...")** → no tmux needed; use `claude-code` skill's print mode diff --git a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh deleted file mode 100755 index f8a2261..0000000 --- a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/env bash -# create_session.sh — multi-agent-mux-create 의 부속 스크립트 -# Usage: -# bash create_session.sh --workspace --agent --role [--session ] [--wrapper] -# -# 동작: -# 1) preflight: tmux/claude/agy 가용성, workspace 존재 -# 2) tmux 세션 이름 결정 (--session 없으면 자동) -# 3) tmux 세션 시작 (claude 는 wrapper 우선, agy 는 인라인) -# 4) pane 메타 캡처 (pid, cmd, cwd) -# 5) agent-sessions.yaml 에 tmux_sessions[] 엔트리 append -# 6) 검증 출력 -# -# Exit codes: -# 0 = success -# 1 = preflight failure -# 2 = invalid args -# 3 = tmux session already exists (use multi-agent-mux-resume or delete first) -# 4 = agent-sessions.yaml append failure -set -euo pipefail - -source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" - -usage() { - cat < --agent --role [options] - -Options: - --workspace PATH project directory (required) - --agent AGENT claude | agy | hermes | cline (required) - --role ROLE assigned role (required) - --session NAME tmux session name (default: derived from workspace) - --wrapper force use of ~/.local/bin/ wrapper even if not present - --dry-run print commands without executing - --tmux-server NAME specify isolated tmux server name - --submit-job PROMPT submit a job to multi-agent-mux-delegate-job registry with the given prompt - -h, --help this help -EOF -} - -WORKSPACE="" -AGENT="" -ROLE="" -SESSION_NAME="" -USE_WRAPPER=0 -DRY_RUN=0 -TMUX_SERVER_OPT="" -SUBMIT_JOB_PROMPT="" - -while [ $# -gt 0 ]; do - case "$1" in - --workspace) WORKSPACE="$2"; shift 2 ;; - --agent) AGENT="$2"; shift 2 ;; - --role) ROLE="$2"; shift 2 ;; - --session) SESSION_NAME="$2"; shift 2 ;; - --wrapper) USE_WRAPPER=1; shift ;; - --dry-run) DRY_RUN=1; shift ;; - --tmux-server) TMUX_SERVER_OPT="$2"; shift 2 ;; - --submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "ERROR: unknown arg: $1" >&2; usage; exit 2 ;; - esac -done - -if [ -n "$TMUX_SERVER_OPT" ]; then - export TMUX_SERVER_NAME="$TMUX_SERVER_OPT" -fi - -# Preflight -[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; } -[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; } -[ -n "$ROLE" ] || { echo "ERROR: --role required" >&2; usage; exit 2; } -[ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; } -command -v tmux >/dev/null || { echo "ERROR: tmux not installed" >&2; exit 1; } -command -v "$AGENT" >/dev/null || { echo "ERROR: $AGENT CLI not in PATH" >&2; exit 1; } - -# Auth Check (OAuth check for agy, loggedIn check for claude, status for hermes) -if [ "$AGENT" = "claude" ]; then - if ! claude auth status 2>/dev/null | grep -q '"loggedIn":\s*true'; then - echo "ERROR: claude not logged in. Run 'claude auth login' first." >&2 - exit 1 - fi -elif [ "$AGENT" = "agy" ]; then - if ! agy models >/dev/null 2>&1; then - echo "ERROR: agy is not authenticated. Please log in first." >&2 - exit 1 - fi -elif [ "$AGENT" = "hermes" ]; then - if ! hermes status >/dev/null 2>&1; then - echo "ERROR: hermes is not functional. Run 'hermes setup' first." >&2 - exit 1 - fi -elif [ "$AGENT" = "cline" ]; then - if ! cline history --json >/dev/null 2>&1; then - echo "ERROR: cline is not functional or configured." >&2 - exit 1 - fi -fi - -# 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A) -if [ -z "$SESSION_NAME" ]; then - SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")" -fi - -# 이미 살아있으면 실패 -if _tmux has-session -t "$SESSION_NAME" 2>/dev/null; then - echo "ERROR: tmux session '$SESSION_NAME' already exists. Use multi-agent-mux-resume to attach, or multi-agent-mux-stop first." >&2 - exit 3 -fi - -# tmux 세션 띄우기 -LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}" -WRAPPER="$LOCAL_BIN/$SESSION_NAME" - -spawn() { - case "$AGENT" in - claude) - if { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; then - nohup "$WRAPPER" >/dev/null 2>&1 & - disown - else - _tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "claude --dangerously-skip-permissions" - fi - ;; - agy) - _tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions" - ;; - hermes) - _tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "hermes" - ;; - cline) - _tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "cline -i" - ;; - *) echo "ERROR: --agent must be claude, agy, hermes or cline, got: $AGENT" >&2; exit 2 ;; - esac -} - -if [ "$DRY_RUN" = "1" ]; then - echo "[dry-run] would spawn: tmux session '$SESSION_NAME' in $WORKSPACE (agent=$AGENT)" - exit 0 -fi - -spawn - -# TUI 준비 대기 -sleep 6 - -# pane 메타 캡처 -PANE_PID=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "") -PANE_CWD=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_path}' 2>/dev/null || echo "$WORKSPACE") -PANE_CMD=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_command}' 2>/dev/null || echo "$AGENT") -TMUX_EPOCH=$(date +%s) -NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ') - -# cmd_full 결정 -case "$AGENT" in - claude) CMD_FULL='claude --dangerously-skip-permissions' ;; - agy) CMD_FULL='agy --dangerously-skip-permissions' ;; - hermes) CMD_FULL='hermes' ;; - cline) CMD_FULL='cline -i' ;; -esac - -# 시작 명령 -local_tmux="tmux" -if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then - local_tmux="tmux -L $TMUX_SERVER_NAME" -fi - -case "$AGENT" in - claude) - if [ -x "$WRAPPER" ]; then - START_CMD="$WRAPPER # ~/.local/bin 의 래퍼" - else - START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"claude --dangerously-skip-permissions\"" - fi - ;; - agy|hermes|cline) - START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" - ;; -esac - -# agent-sessions.yaml 에 append -DELEGATE_JOB_ID="" -if [ -n "$SUBMIT_JOB_PROMPT" ]; then - delegate_agent="" - if [ "$AGENT" = "claude" ]; then - delegate_agent="claude-code" - elif [ "$AGENT" = "hermes" ]; then - delegate_agent="hermes-agent" - elif [ "$AGENT" = "cline" ]; then - delegate_agent="cline-agent" - else - delegate_agent="antigravity-cli" - fi - agent_session="tmux:$SESSION_NAME" - DELEGATE_JOB_ID=$(delegate_submit_job "$SUBMIT_JOB_PROMPT" "$delegate_agent" "$agent_session") - echo "Submitted delegated job: $DELEGATE_JOB_ID" -fi - -if [ ! -f "$AGENT_SESSIONS_YAML" ]; then - mkdir -p "$(dirname "$AGENT_SESSIONS_YAML")" - echo "tmux_sessions: []" > "$AGENT_SESSIONS_YAML" -fi - -# atomic_dump_yaml: flock + temp+rename + .bak + schema validate (P0-B). -# 모든 값은 환경변수로 전달 — heredoc interpolation 없음 (P1-B). -# 자식 pid 는 bash 에서 pgrep 으로 미리 구함 (P2: 도구명 필터). -CHILD_PID=0 -if { [ "$AGENT" = "agy" ] || [ "$AGENT" = "hermes" ] || [ "$AGENT" = "cline" ]; } && [ -n "$PANE_PID" ]; then - CHILD_PID=$(pgrep -P "$PANE_PID" -x "$AGENT" 2>/dev/null | head -1 || true) - CHILD_PID="${CHILD_PID:-0}" -fi - -atomic_dump_yaml "$AGENT_SESSIONS_YAML" \ - SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \ - TMUX_EPOCH="$TMUX_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \ - CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \ - TMUX_SERVER_NAME="${TMUX_SERVER_NAME:-default}" \ - DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" <<'PYEOF' -name = os.environ['SESSION_NAME'] -agent = os.environ['AGENT'] -role = os.environ['ROLE'] -pid = os.environ.get('PANE_PID', '') -epoch = os.environ.get('TMUX_EPOCH', '') -server_name = os.environ.get('TMUX_SERVER_NAME', 'default') -server_opt = f"-L {server_name} " if server_name and server_name != 'default' else "" - -sessions = d.setdefault('tmux_sessions', []) - -# P0-D: 같은 이름 엔트리가 status=running 이면만 거부. terminated/archived 는 -# 재사용 가능 — 낡은 엔트리를 제거하고 새로 append (create -> delete -> create). -running_same = [s for s in sessions if s.get('name') == name and s.get('status') == 'running'] -if running_same: - print(f"ERROR: {name} already running in agent-sessions.yaml", flush=True) - raise SystemExit(4) -sessions[:] = [s for s in sessions if s.get('name') != name] - -entry = { - 'name': name, - 'status': 'running', - 'role': role, - 'tmux_session_created_at': os.environ['NOW_ISO'], - 'tmux_session_epoch': int(epoch) if epoch.isdigit() else 0, - 'tmux_server': server_name, - 'delegate_job_id': os.environ.get('DELEGATE_JOB_ID', '') or None, - 'pane': { - 'index': 0, - 'pid': int(pid) if pid.isdigit() else 0, - 'cmd': agent, - 'cmd_full': os.environ['CMD_FULL'], - 'cwd': os.environ['PANE_CWD'], - }, - 'start_command': os.environ['START_CMD'], - 'attach_command': f'tmux {server_opt}attach -t {name}', - 'kill_command': f'tmux {server_opt}kill-session -t {name}', -} - -if agent == 'claude': - entry['tui'] = { - 'model': '(unknown — capture after first message)', - 'provider': 'anthropic', - 'plan': '(unknown)', - 'account': '(unknown — read from claude auth status)', - 'version': '(unknown — read from TUI)', - } - entry['claude_session_id_own'] = None - entry['last_visible_status'] = "TUI started; awaiting first user message" -elif agent == 'agy': - cp = os.environ.get('CHILD_PID', '0') - entry['child_pid'] = int(cp) if cp.isdigit() else 0 - entry['agy_conversation_id_own'] = None - entry['mcp_attachments'] = [ - { - 'name': 'stitch', - 'transport': 'mcp-remote', - 'endpoint': 'https://stitch.googleapis.com/mcp' - } - ] - entry['last_visible_status'] = "TUI started; awaiting first user message" -elif agent == 'hermes': - cp = os.environ.get('CHILD_PID', '0') - entry['child_pid'] = int(cp) if cp.isdigit() else 0 - entry['hermes_conversation_id_own'] = None - entry['last_visible_status'] = "TUI started; awaiting first user message" -elif agent == 'cline': - cp = os.environ.get('CHILD_PID', '0') - entry['child_pid'] = int(cp) if cp.isdigit() else 0 - entry['cline_conversation_id_own'] = None - entry['last_visible_status'] = "TUI started; awaiting first user message" - -sessions.append(entry) - -snap = d.setdefault('snapshot', {}) -snap['taken_at'] = os.environ['NOW_ISO'] -snap['cwd'] = os.environ['PANE_CWD'] -print(f"appended: {name}", flush=True) -PYEOF - -echo -echo "=== created ===" -echo "tmux session: $SESSION_NAME (pane pid $PANE_PID, cmd $PANE_CMD, cwd $PANE_CWD)" -if [ -n "$DELEGATE_JOB_ID" ]; then - echo "delegate job: $DELEGATE_JOB_ID" - delegate_publish_event "$DELEGATE_JOB_ID" started "multi-agent-mux session created" - WD_PID=$(start_watchdog "$DELEGATE_JOB_ID" "$WORKSPACE") - echo "watchdog PID: $WD_PID" -fi -echo "agent-sessions.yaml updated" -echo -if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then - echo "Attach: tmux -L $TMUX_SERVER_NAME attach -t $SESSION_NAME" -else - echo "Attach: tmux attach -t $SESSION_NAME" -fi -echo "Delete: use multi-agent-mux-stop skill" -echo "Resume: use multi-agent-mux-resume skill (after first message creates a session id)" diff --git a/.agents/skills/multi-agent-mux-delegate-job/README.md b/.agents/skills/multi-agent-mux-delegate-job/README.md deleted file mode 100644 index 5c709a1..0000000 --- a/.agents/skills/multi-agent-mux-delegate-job/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# multi-agent-mux-delegate-job 스킬 - -작업(Job)을 자율 에이전트(claude-code/hermes/agy/cline/codex/opencode/human)에게 위임하고 MQTT -이벤트 채널로 비동기 관찰하는 범용 에이전트 협업 스킬. **시작점은 [`SKILL.md`](./SKILL.md).** - -- 프로토콜/스키마: [`job-protocol.md`](./job-protocol.md) -- 브로커 PoC→운영 전환: [`mqtt-broker-setup.md`](./mqtt-broker-setup.md) -- 레지스트리 포맷/동시성: [`registry.md`](./registry.md) -- 참조 구현: [`multi-agent-mux-delegate-job`](./multi-agent-mux-delegate-job) (bash wrapper), [`scripts/publish_event.py`](./scripts/publish_event.py), [`scripts/job_subscriber.py`](./scripts/job_subscriber.py), [`scripts/registry.py`](./scripts/registry.py), [`scripts/mqtt_common.py`](./scripts/mqtt_common.py) -- 영구 감사 로그: `.mam/delegate_job_logs//` (`meta.json`·`events.ndjson`·`status.json`) - — `multi-agent-mux-delegate-job logs ` 또는 `multi-agent-mux-delegate-job logs --list`로 조회 (SKILL.md "Audit Logs" 참조) diff --git a/.agents/skills/multi-agent-mux-delegate-job/SKILL.md b/.agents/skills/multi-agent-mux-delegate-job/SKILL.md deleted file mode 100644 index 7ff015f..0000000 --- a/.agents/skills/multi-agent-mux-delegate-job/SKILL.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: multi-agent-mux-delegate-job -description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, cline, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer." -version: 1.1.0 -author: Multi-Agent System -license: MIT -platforms: [linux, macos, windows] ---- - -# multi-agent-mux-delegate-job — Async Job Delegation over MQTT - -Delegate a unit of work to any autonomous agent, then **observe** it asynchronously instead of blocking. Every job gets a unique ID and a registry record. The worker agent publishes lifecycle events (`started`, `permission_required`, `progress`, `completed`, `error`) to a per-job MQTT topic, and the delegator/orchestrator subscribes to verify the final state. - -This skill allows any agent (`claude-code`, `hermes`, `agy`, `cline`, etc.) to play any role: **Orchestrator/Delegator**, **Worker/Implementer**, or **Reviewer**. - ---- - -## Roles in Multi-Agent Mux - -- **Orchestrator (Delegator)**: Initiates the job, coordinates other agents, handles loops and reviews, and commits final changes. -- **Worker (Implementer)**: Receives the brief file or task prompt, performs the implementation, and emits started/completed/error events. -- **Reviewer**: Evaluates git diffs or artifacts produced by the worker, and responds with a `completed` event containing `"PASS"` or feedback. - ---- - -## Core Commands (CLI) - -The `multi-agent-mux-delegate-job` bash wrapper handles job registration, subscriber management, agent session targeting, and validation hooks: - -```bash -# 1) Submit a new job to a targeted agent session (e.g. tmux session name 'demo') -multi-agent-mux-delegate-job submit \ - --agent \ - --agent-session tmux: \ - --prompt "Task description or instructions here" \ - --timeout 3600 --idle-timeout 120 - -# 2) Submit a job with a feedback loop (Worker-Reviewer Loop) -multi-agent-mux-delegate-job submit \ - --agent --agent-session tmux: \ - --type loop --reviewer --reviewer-session tmux: \ - --prompt "Task description" - -# 3) Check job status and audit logs -multi-agent-mux-delegate-job status --job -multi-agent-mux-delegate-job logs # Chronological log of events -multi-agent-mux-delegate-job list # Summary of all registered jobs - -# 4) Verify job artifacts with a validation script -multi-agent-mux-delegate-job verify --job --validate ./validate.sh -``` - ---- - -## Task Delegation Types - -Supported job types include: -- `direct` (default): Single agent execution (direct tasking). -- `loop` (Worker-Reviewer Loop): Alternates worker execution and reviewer evaluation until reviewer approves (`PASS`) or iterations run out. -- `discuss` (Research & Discussion): Collaboration between two agents to reach a consensus (e.g., agreeing on a design or plan). - -For detailed state machine diagrams and configurations, see [DELEGATION_TYPES.md](./DELEGATION_TYPES.md). - ---- - -## The Event Protocol Contract - -Every agent participating in the delegation contract must follow the same lifecycle publishing protocol using `publish_event.py`: - -1. **On Start**: Publish `started` event. - `python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --job "$JOB_ID" --event started` -2. **On Tool/Permission Prompt**: Publish `permission_required` event. - `python3 ... --job "$JOB_ID" --event permission_required --detail ":"` -3. **On Progress Update (Optional)**: Publish `progress` event. - `python3 ... --job "$JOB_ID" --event progress --detail ""` -4. **On Success**: Publish `completed` event. - `python3 ... --job "$JOB_ID" --event completed --detail ""` (Reviewer should include `"PASS"` in the detail to approve). -5. **On Failure/Feedback**: Publish `error` event. - `python3 ... --job "$JOB_ID" --event error --detail ""` - ---- - -## Audit Logs - -Job lifecycle execution events are persistently mirrored to an append-only log under `.mam/delegate_job_logs//` (containing `meta.json`, `events.ndjson`, and `status.json`). Use `multi-agent-mux-delegate-job logs ` to view the timeline. - ---- - -## Best Practices and Pitfalls - -- **Subscribe-Before-Publish**: The subscriber must be running before the agent starts publishing. The `submit` command handles this automatically by launching the subscriber in the background first. -- **Fresh job_id Propagation**: Make sure the worker agent receives the correct `JOB_ID` generated for the current run, rather than reusing stale IDs from previous sessions. -- **Brief delivery via file path**: For long or complex prompts, write the instructions to a file (e.g. `/tmp/task-brief.md`) and pass a short prompt pointing to the file path to prevent terminal buffer overflows. -- **Batch Grouping**: Group non-overlapping tasks into batches to parallelize execution across multiple agent sessions, reducing overhead. diff --git a/.agents/skills/multi-agent-mux-delegate-job/job-protocol.md b/.agents/skills/multi-agent-mux-delegate-job/job-protocol.md deleted file mode 100644 index 50e7365..0000000 --- a/.agents/skills/multi-agent-mux-delegate-job/job-protocol.md +++ /dev/null @@ -1,114 +0,0 @@ -# Job Event Protocol - -The wire contract every multi-agent-mux-delegate-job agent (claude-code, codex, opencode, -human, …) speaks. One job → one MQTT topic → JSON event payloads. Stable across -the PoC (public broker) and production (own broker) stages; only transport -hardening changes, never the payload shape. - -Reference implementation: [`./scripts/publish_event.py`](./scripts/publish_event.py) -(emit) and [`./scripts/job_subscriber.py`](./scripts/job_subscriber.py) (observe). - ---- - -## 1. Topic design - -| Topic | Purpose | -|-------|---------| -| `python/mqtt/sample` | Legacy demo topic — **never changed** (README compat). | -| `python/mqtt/jobs//events` | Per-job event stream (this protocol). | - -- One topic per job, JSON payload, `event` field discriminates the type. -- Single-direction publish only (worker → observer). No request/response. -- Future split is reserved but not required: - `/events`, `/logs`, `/artifacts`. -- `topic_prefix` is stored in the job record so publishers resolve the topic - from the registry alone (`/events`). - ---- - -## 2. Payload schema (JSON, UTF-8, `schema_version = 1`) - -```json -{ - "schema_version": 1, - "seq": 7, - "job_id": "abc12345", - "event": "started | permission_required | progress | completed | error", - "timestamp": "2026-06-19T09:32:00Z", - "detail": "generalised, whitelisted human-readable string", - "data": { "optional": "metadata" } -} -``` - -| Field | Rule | -|-------|------| -| `schema_version` | If publisher/subscriber disagree, the subscriber **drops** the event with a warning (defensive parsing). | -| `seq` | Monotonic **per `job_id`**, first publish = 1. Lets the subscriber detect reorder/duplication. Persisted in the registry (`last_seq`) so it survives restarts. | -| `job_id` | Subscriber drops any event whose `job_id` it did not subscribe for. | -| `timestamp` | Publisher host clock, **advisory only**. The delegator's timeout is measured from *receive* time, not this field. | -| `detail` | Generalised text only. **No absolute paths, keys, or tokens.** | -| `data` | Optional metadata. Production may add `hmac_sig`, `build_id`, etc. | - ---- - -## 3. Event catalogue - -| event | When emitted | `detail` example | seq | -|-------|--------------|------------------|-----| -| `started` | Agent first picks up the job | `"Job a1b2c3d4 started"` | 1 | -| `permission_required` | Agent needs a tool/permission grant | `"needs to write sort_problems.md"` | as it happens | -| `progress` | Optional intermediate checkpoint | `"creating problem 5/10"` | as it happens | -| `completed` | Successful terminal state | `"saved to sort_problems.md"` | last | -| `error` | Failure / exception terminal state | `"internal error, see logs"` | last | - -`started` and `completed`/`error` are mandatory bookends; `permission_required` -and `progress` are optional. `detail` must stay on the whitelist of generalised -phrasings — never leak secrets through it. - -### Terminal semantics - -- `completed` → subscriber exits 0; `error` → exits 1. -- The subscriber runs a **terminal state machine**: it finalises a job on the - first `completed`/`error` it sees and ignores any later terminal event for - that job (QoS-1 duplicate, or an `error`-after-`completed` reorder). When all - watched jobs are finalised it exits. -- Wall-clock timeout *or* idle timeout before a terminal event → exit 2. - ---- - -## 4. Production hardening (own broker stage) - -The payload shape is unchanged; the transport and trust model tighten. See -[`mqtt-broker-setup.md`](./mqtt-broker-setup.md) for the broker side. - -- **Auth / ACL** — username/password + per-topic ACL. `jobs/+/events` publish is - granted to the worker credential, subscribe to the Hermes credential. -- **HMAC Signature Verification (`data.hmac_sig`)** — to authenticate the publisher and verify message integrity without exposing the raw secret token over the wire, each job record contains a per-job `auth_token` (`secrets.token_urlsafe(32)`). The publisher computes an HMAC-SHA256 signature over the serialized payload (excluding `data.hmac_sig` itself) using the `auth_token` as the key, and appends it to **`data.hmac_sig`**. The subscriber reconstructs this signature and **drops any message that does not match or lacks a valid signature**. - - ```json - { "...": "...", "data": { "hmac_sig": "d2f3...", "build_id": "42" } } - ``` - -- **TLS** — port 8883 + private CA. Toggled with `MQTT_TLS=1` (+ `MQTT_CA_CERTS`); - no code change. -- **Retained terminal events** — `completed`/`error` publish with `retain=True` - so a subscriber that joins late immediately receives the last terminal state - instead of a stale view. The reference publisher auto-retains terminal events; - `--retained` forces it for any event. -- **Dual timeouts** — total wall-clock budget + last-activity idle detection, - both measured from receive time. -- **Clock trust** — never trust the payload `timestamp` for timeout decisions. - ---- - -## 5. Why a public broker is PoC-only - -On `broker.hivemq.com` anyone can publish/subscribe the same topic. Therefore: - -- No secret data in payloads. -- `started`/`completed`/`error` are *signals*, never a basis for a security - decision. -- Non-retained messages are **not queued** for absent subscribers — start the - subscriber **before** the agent (ordering dependency), or rely on retained - terminal events in production. -- Real operational decisions belong to the own-broker stage with auth + ACL. diff --git a/.agents/skills/multi-agent-mux-delegate-job/mqtt-broker-setup.md b/.agents/skills/multi-agent-mux-delegate-job/mqtt-broker-setup.md deleted file mode 100644 index 685f62e..0000000 --- a/.agents/skills/multi-agent-mux-delegate-job/mqtt-broker-setup.md +++ /dev/null @@ -1,176 +0,0 @@ -# MQTT Broker Setup — PoC → Production - -The multi-agent-mux-delegate-job scripts read **all** broker settings from environment -variables (or a job record's `broker.*` block) through a single helper, -`broker_config_from_env()` in -[`./scripts/mqtt_common.py`](./scripts/mqtt_common.py). The design goal: -**switch from the public PoC broker to your own broker with config only — no -code change.** - -| Env var | Meaning | PoC default | Production | -|---------|---------|-------------|-----------| -| `MQTT_BROKER` | host | `broker.hivemq.com` | internal hostname/IP | -| `MQTT_PORT` | port | `1883` | `8883` (TLS) | -| `MQTT_TLS` | TLS on/off (`1`/`0`) | `0` | `1` | -| `MQTT_USERNAME` / `MQTT_PASSWORD` | auth | (none) | broker-issued | -| `MQTT_CA_CERTS` | CA bundle path | (none) | private CA path | -| `MQTT_CERTFILE` / `MQTT_KEYFILE` | client cert (optional mTLS) | (none) | per-client | -| `MQTT_CLIENT_ID_PREFIX` | client id prefix | `hermes` | per-environment | - ---- - -## 1. PoC: public broker (`broker.hivemq.com`) - -**Pros** — zero setup, reachable from anywhere, perfect for wiring up the -publish/subscribe loop and the timeout/state-machine logic. - -**Cons / accepted assumptions** — no auth, no integrity, shared with the world: - -- no secrets in payloads; -- `started`/`completed`/`error` are advisory signals only; -- non-retained messages are **not queued** for absent subscribers, so the - subscriber must start before the agent; -- a re-subscribing client cannot recover past (non-retained) events. - -Use it only to validate the protocol, never for real decisions. - ---- - -## 2. Production: self-hosted Mosquitto (or EMQX) - -Both support MQTT 5 + ACL + TLS. Mosquitto shown below; EMQX is a drop-in for -the same env vars. - -### 2.1 Install - -```bash -# macOS -brew install mosquitto - -# Debian/Ubuntu -sudo apt-get update && sudo apt-get install -y mosquitto mosquitto-clients - -# Docker -docker run -d --name mosquitto -p 8883:8883 \ - -v "$PWD/mosquitto.conf:/mosquitto/config/mosquitto.conf" \ - -v "$PWD/certs:/mosquitto/certs" \ - -v "$PWD/auth:/mosquitto/auth" \ - eclipse-mosquitto:2 -``` - -### 2.2 `mosquitto.conf` (key lines) - -```conf -persistence true -persistence_location /mosquitto/data/ - -password_file /mosquitto/auth/passwd -acl_file /mosquitto/auth/acl -allow_anonymous false - -listener 8883 -cafile /mosquitto/certs/ca.crt -certfile /mosquitto/certs/server.crt -keyfile /mosquitto/certs/server.key -``` - -`persistence true` + QoS 1 + retained terminal events means a subscriber that -joins after a job finished still sees the final `completed`/`error`. - -### 2.3 Users (username/password) - -```bash -# create the file with the first user, then add more with -b -mosquitto_passwd -c /mosquitto/auth/passwd hermes # subscriber/delegator -mosquitto_passwd /mosquitto/auth/passwd claude-worker # publisher/agent -# (omit -c after the first; -c truncates the file) -``` - -### 2.4 ACL — least privilege - -The worker only **publishes** events; Hermes only **subscribes**: - -```conf -# /mosquitto/auth/acl - -# claude-worker: may publish job events, may not read others' streams -user claude-worker -topic write python/mqtt/jobs/+/events - -# hermes: observes every job's events -user hermes -topic read python/mqtt/jobs/+/events - -# keep the legacy demo topic usable for both, if desired -pattern readwrite python/mqtt/sample -``` - -### 2.5 TLS certificates - -**Quick self-signed (single host, internal only):** - -```bash -mkdir -p certs && cd certs -openssl req -x509 -newkey rsa:2048 -nodes -days 825 \ - -keyout server.key -out server.crt \ - -subj "/CN=mqtt.internal" -cp server.crt ca.crt # clients trust this as the CA bundle -``` - -**Private CA (recommended — separate CA from server cert):** - -```bash -# 1) CA -openssl genrsa -out ca.key 4096 -openssl req -x509 -new -nodes -key ca.key -days 3650 -out ca.crt -subj "/CN=Hermes-CA" -# 2) server cert signed by the CA -openssl genrsa -out server.key 2048 -openssl req -new -key server.key -out server.csr -subj "/CN=mqtt.internal" -openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ - -out server.crt -days 825 -``` - -Clients trust `ca.crt` via `MQTT_CA_CERTS=/path/to/ca.crt`. - ---- - -## 3. Cut-over verification (config-only, no code change) - -Goal: prove the **same scripts** talk to your broker by changing only env/registry. - -```bash -# 1) point the env at the new broker -export MQTT_BROKER=mqtt.internal -export MQTT_PORT=8883 -export MQTT_TLS=1 -export MQTT_CA_CERTS=$PWD/certs/ca.crt -export MQTT_USERNAME=hermes -export MQTT_PASSWORD=… # subscriber side -# (publisher side uses claude-worker creds via the job record's broker block) - -# 2) sanity-check with the mosquitto CLI first -mosquitto_sub -h "$MQTT_BROKER" -p 8883 --cafile "$MQTT_CA_CERTS" \ - -u hermes -P "$MQTT_PASSWORD" -t 'python/mqtt/jobs/+/events' -v & - -# 3) run the unchanged multi-agent-mux-delegate-job loop -PY=.venv/bin/python -JID=$($PY scripts/registry.py register --prompt "broker cutover smoke") -$PY scripts/job_subscriber.py --job "$JID" --timeout 30 & -sleep 3 -$PY scripts/publish_event.py --job "$JID" --event started -$PY scripts/publish_event.py --job "$JID" --event completed # auto-retained -``` - -Expected: -- subscriber prints the `started` and `completed` lines and exits 0; -- `mosquitto_sub` shows the same events (ACL allows `hermes` to read); -- publishing as a credential **without** write ACL is rejected by the broker; -- a subscriber started *after* `completed` still receives it (retained). - -If all four hold, the migration is config-only. Persist the broker block into -each job record so `publish_event.py` connects from the registry alone: - -```json -"broker": { "host": "mqtt.internal", "port": 8883, "tls": true, - "username": "claude-worker", "password": "…" } -``` diff --git a/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job b/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job deleted file mode 100755 index 79d6f9f..0000000 --- a/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job +++ /dev/null @@ -1,440 +0,0 @@ -#!/usr/bin/env bash -# multi-agent-mux-delegate-job — user-facing orchestrator for the multi-agent-mux-delegate-job skill. -# -# Subcommands: -# submit register a job, start the subscriber FIRST, then run the agent, -# then (optionally) run a validation script. -# status show one job record. -# list list all jobs. -# verify run a user-supplied --validate script against a job's artifacts. -# wait block until all running/pending jobs reach a terminal state. -# -# This is a reference wrapper: it shells out to the python scripts that live -# next to it. Copy it into your project and customise as needed. It never hard -# fails if `claude`/`codex`/`tmux` are missing — it prints what it would run. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Load local .env if it exists in current dir or workspace root -if [[ -f .env ]]; then - set -a; source .env; set +a -elif [[ -f "$SCRIPT_DIR/../../.env" ]]; then - set -a; source "$SCRIPT_DIR/../../.env"; set +a -fi - -# Pick an interpreter: prefer a project .venv, else python3. -pick_python() { - local py_bin - if [[ -n "${DELEGATE_JOB_PYTHON:-}" ]]; then - py_bin="$DELEGATE_JOB_PYTHON" - elif [[ -x "${WORKDIR:-.}/.venv/bin/python" ]]; then - py_bin="${WORKDIR}/.venv/bin/python" - elif [[ -x ".venv/bin/python" ]]; then - py_bin="$(pwd)/.venv/bin/python" - else - py_bin="python3" - fi - if ! "$py_bin" -c "import paho.mqtt" 2>/dev/null; then - echo "ERROR: paho-mqtt package is missing for $py_bin." >&2 - echo " Please create a virtual environment and install it:" >&2 - echo " python3 -m venv .venv && .venv/bin/pip install -r \"$SCRIPT_DIR/requirements.txt\"" >&2 - exit 1 - fi - echo "$py_bin" -} - -REGISTRY_DIR_DEFAULT=".mam/jobs" - -usage() { - cat <<'EOF' -multi-agent-mux-delegate-job [options] - - submit --agent --prompt [--workdir ] [--agent-session