Compare commits

..

5 Commits

15 changed files with 564 additions and 465 deletions
+50 -35
View File
@@ -10,21 +10,28 @@
역할군 간의 책임 및 권한을 명확히 분리하여 병목을 줄이고 작업의 완성도를 높입니다. 역할군 간의 책임 및 권한을 명확히 분리하여 병목을 줄이고 작업의 완성도를 높입니다.
### 👤 Project Manager (PM / Orchestrator) ### 👑 General Manager (총괄 매니저)
- **주요 책무**: 사용자 요구사항 접수, 상세 작업 계획 수립, 작업자 할당/지시, 전체 워크플로우 통제 및 최종 결과 보고. - **주요 책무**: 사용자와 직접 소통하여 요구사항 접수, 상세 작업 계획 수립, 팀장 에이전트 할당 및 작업 위임, 전체 워크플로우 통제 및 최종 완료 보고.
- **모호성 제거**: 사용자의 요구사항에 모호한 부분이 있다면 작업을 추측하여 진행하지 말고, 즉시 사용자에게 질문하여 명확히 해야 합니다 (`/grill-me` 슬래시 명령어 권장). - **모호성 제거**: 사용자의 요구사항에 모호한 부분이 있다면 작업을 추측하여 진행하지 말고, 즉시 사용자에게 질문하여 명확히 해야 합니다 (`/grill-me` 슬래시 명령어 권장).
- **피드백 루프 조정**: Reviewer들의 검증 의견을 분석하여 개선 방향을 의사결정합니다. 결정하기 까다로운 기술적 난제는 Worker 및 Reviewer들의 조사를 거쳐 PM 본인의 판단을 더한 최종 보고서를 작성해 사용자에게 제시하고 프로젝트의 방향을 결정합니다.
- **자가 치유 (Hermes Fallback Fix)**: Reviewer가 지적한 결함이 아주 경미하거나 단순 오탈자/설정 누락인 경우, Worker에게 재할당하지 않고 PM이 직접 소스코드를 수정하여 전체 왕복(Round-trip) 비용을 최소화합니다.
### 🛠️ Worker (Implementation Agent) ### 👥 Team Leaders (팀장)
- **주요 책무**: PM으로부터 위임받은 구체적인 비즈니스 로직 설계 및 소스코드 구현. 새롭게 생성되는 에이전트(`antigravity`, `claude`, `cline`, `hermes` 등)는 각 팀의 **팀장** 역할을 수행합니다. 총괄 매니저로부터 작업을 위임받아 개발 또는 리뷰 워크플로우를 주도합니다.
- **협업 및 소통**: 할당받은 업무 범위에서 구현 방향이 모호하거나 인터페이스 설계 변경이 필요한 경우 PM에게 질문하여 합의를 이룬 후 수술적(Surgical) 변경을 적용합니다. - **Developer Team Leader (개발 팀장)**:
- **계약 준수**: PM이 전달한 단일 작업 지침(Brief) 및 고유 Job ID 규약을 준수하며, 작업 시작 시 `started`, 종료 시 `completed`/`error` 이벤트를 백플레인에 발행해야 합니다. - 총괄 매니저로부터 작업을 위임받습니다.
- **작업 분석 및 계획**: 주어진 작업을 철저히 분석하고, 작은 단위로 문제를 나누어 세부 계획을 수립합니다.
- **내부 병렬 처리**: 내부적으로 subagent를 활용해 위임받은 작업을 병렬적으로 처리할 수 있습니다.
- **리뷰 타당성 검증 및 거부**: 리뷰어가 지적한 피드백을 면밀히 검토합니다. 타당한 제안은 수렴하여 코드를 수정하지만, 타당하지 않다고 판단되는 안건은 반영하지 않고 **그 명확한 이유를 작성하여 리뷰어에게 되돌려 보냅니다**.
- **완료 신호 송신**: 모든 리뷰어들로부터 `PASS`를 획득하고 변경 사항이 검증되면, 최초 작업을 위임받았던 개발 팀장이 총괄 매니저에게 최종 작업 완료 신호를 송신합니다.
- **Reviewer Team Leader (리뷰어 팀장)**:
- 개발 팀장으로부터 리뷰 요청을 접수합니다.
- **문제 제시에 대한 이유와 개선 방향 포함**: 단순한 반려(`NOT PASS`) 통보는 금지됩니다. 이슈를 제기할 때는 **반드시 해당 문제가 발생하는 구체적인 이유와 확실한 개선 방향(코드 대안 포함)을 함께 작성**해야 합니다.
- **합의 루프**: 모든 지적 사항이 해결되고 최종 `PASS`를 발행할 때까지 리뷰 루프에 동참합니다.
### 🔍 Reviewer (Verification Agent) ### 🛡️ 역할 범위 준수 원칙 (Role Suitability Check)
- **주요 책무**: Worker가 제출한 소스코드 변경 사항(Diff)과 구현 명세를 검증하고, 보안 결함 탐지, 성능 개선안 도출 및 설계 일관성을 심사하는 조력자. - 모든 에이전트는 자신에게 부여된 역할에 부합하는 작업만을 수행해야 합니다. (예: 개발 팀장은 최종 PASS 여부를 결정하지 않으며, 리뷰어 팀장은 직접 프로젝트 소스코드를 작성하지 않습니다.)
- **구체적 대안 제시**: 단순한 반려(`NOT PASS`) 통보를 금지하며, 문제를 제기할 때는 **안정적이고 검증된 구체적인 코드 대안(Alternative Code)이나 해결 방안을 반드시 함께 제시**해야 합니다. - **자신의 역할에 맞지 않는 작업이 지시된 경우**, 에이전트는 반드시:
- **교차 검증의 상호보완성**: 에이전트의 모델 특성(예: Flash 계열은 의미론적 셸 결함 포착에 강하고, Opus/Sonnet 계열은 API 서명 및 논리 회귀 분석에 강함)을 살려 병렬로 상호보완적 심사를 수행합니다. 1. 해당 작업을 수행하기에 가장 적합한 에이전트 세션을 추천하여 위임을 유도하거나,
2. 프로젝트 연속성을 위해 극히 필요한 경우 직접 작업을 수행합니다.
--- ---
@@ -59,34 +66,42 @@
sequenceDiagram sequenceDiagram
autonumber autonumber
actor User as 사용자 actor User as 사용자
participant PM as Project Manager participant GM as General Manager
participant W as Worker participant DTL as Developer Team Leader
participant R as Reviewers participant RTL as Reviewer Team Leaders
participant M as MQTT Backplane participant M as MQTT Backplane
User->>PM: 요구사항 전달 User->>GM: 요구사항 전달
Note over PM: grill-me 및 계획 수립 GM->>DTL: 작업 위임 (예: 랜딩 페이지 제작)
PM->>M: Job 등록 및 Subscriber 구동 Note over DTL: 작업 분석, 세분화 및 subagent 병렬 구동
PM->>W: 작업 위임 (Job ID & Brief 전달) DTL->>M: 'started' 이벤트 발행
W->>M: 'started' 이벤트 발행 Note over DTL: 코드 변경 및 구현
Note over W: 코드 변경 및 구현 DTL->>M: 'completed' 발행
W->>M: 'completed' (혹은 'error') 발행 DTL->>RTL: 리뷰 요청 (랜딩 페이지를 제작했습니다. 리뷰를 진행해주세요)
PM->>R: 병렬 리뷰 요청 (Diff 전달) Note over RTL: 교차 분석 & 검증
Note over R: 교차 분석 & 검증 alt 결함 발견 (리뷰어 피드백)
alt 결함 발견 RTL->>DTL: NOT PASS / 피드백 (반드시 이유와 확실한 개선 방향 포함)
R->>PM: NOT PASS (대안 포함 피드백) Note over DTL: DTL이 피드백의 타당성 검증
Note over PM: 경미한 결함은 PM이 직접 수정 alt 타당한 피드백
PM->>W: 피드백 반영 및 재할당 Note over DTL: DTL이 수용하여 코드 수정
else 타당하지 않은 피드백
DTL->>RTL: 반론 및 거부 이유 전달 (부적절한 항목 미반영)
end
DTL->>RTL: 재리뷰 요청 (리뷰 안건 수정 완료)
else 검증 통과 else 검증 통과
R->>PM: PASS RTL->>DTL: PASS
end end
PM->>User: 최종 검증 통과 보고 & 커밋 DTL->>GM: 최종 완료 신호 송신
GM->>User: 사용자에게 작업 완료 통보
``` ```
1. **계획 수립 및 할당**: PM은 사용자 요청을 구체화하고 의존성이 겹치지 않는 범위에서 잡을 정의합니다. 1. **계획 수립 및 할당**: 총괄 매니저는 개발 팀장에게 작업을 인가합니다.
2. **작업 개시 및 통보**: PM은 구독자를 띄운 뒤 Worker 세션에 잡을 인가하며, Worker는 로직을 수행하고 단말 이벤트를 전송해 세션을 자동 종료합니다. 2. **분석 및 내부 실행**: 개발 팀장은 작업을 분석하고 세분화하여 계획을 세운 뒤 내부 subagent를 가동하여 구현을 완료합니다. 이후 `started`를 거쳐 `completed` 이벤트를 발행하고 리뷰어에게 검수를 요청합니다.
3. **교차 검수 반복 (Review Loop)**: PM은 작업 완료 후 변경분을 Reviewer 에이전트들에게 병렬 회람시킵니다. 리뷰어 전원이 `PASS` 의견을 낼 때까지 수정-반려 주기를 무한 반복(Loop)하여 코드 완성도를 보증합니다. 3. **이의 제기 및 정제 루프**:
4. **릴리즈 및 정리**: 검증이 완료된 코드는 Git에 커밋하고, 임시 세션 리소스를 회수합니다. - 리뷰어 팀장은 상세 피드백 시 반드시 이유와 보완 방향을 제시해야 합니다.
- 개발 팀장은 의견을 검토해 타당하면 수정하고, 타당하지 않으면 반론과 근거를 회신합니다.
- 리뷰어 전원이 `PASS`를 인가할 때까지 이 과정이 반복됩니다.
4. **최종 보고**: 개발 팀장이 총괄 매니저에게 완료 신호를 보내면 총괄 매니저가 사용자에게 완료를 알립니다.
--- ---
@@ -123,4 +138,4 @@ TMUX 환경에서 실행되는 에이전트가 화면 스크롤 한계로 인해
--- ---
*본 가이드는 협업 효율성과 코드 보안의 엄격한 균형을 유지하기 위한 규범입니다. 변경 사항이 필요한 경우 PM 및 Reviewer의 전원 합의를 거쳐 본 문서를 업데이트해야 합니다.* *본 가이드는 협업 효율성과 코드 보안의 엄격한 균형을 유지하기 위한 규범입니다. 변경 사항이 필요한 경우 총괄 매니저 및 전체 팀장의 합의를 거쳐 본 문서를 업데이트해야 합니다.*
+50 -35
View File
@@ -10,21 +10,28 @@ All agents working on a new project must read this document thoroughly and compl
We clearly separate responsibilities and permissions between roles to reduce bottlenecks and enhance the quality of execution. We clearly separate responsibilities and permissions between roles to reduce bottlenecks and enhance the quality of execution.
### 👤 Project Manager (PM / Orchestrator) ### 👑 General Manager (Orchestrator)
- **Core Responsibility**: Receive user requirements, establish detailed task plans, assign and instruct workers, control the overall workflow, and report final results. - **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). - **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).
- **Feedback Loop Adjustment**: Analyze verification feedback from Reviewers to decide on improvement paths. For complex technical challenges, direct Workers and Reviewers to research options, add the PM's own assessment, and present a final report to the user to decide the project's direction.
- **Self-Healing (Hermes Fallback Fix)**: If a defect pointed out by a Reviewer is extremely minor or is a simple typo/configuration omission, the PM should directly fix the source code instead of reassigning it to the Worker, thereby minimizing the round-trip cost.
### 🛠️ Worker (Implementation Agent) ### 👥 Team Leaders (팀장)
- **Core Responsibility**: Design business logic and implement source code as delegated by the PM. 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.
- **Collaboration & Communication**: If the implementation path is ambiguous or interface design changes are required within the assigned scope, ask the PM for consensus before applying surgical changes. - **Developer Team Leader (개발 팀장)**:
- **Contract Adherence**: Comply with the single task instructions (Brief) and the unique Job ID convention provided by the PM. Workers must publish a `started` event when starting work, and a `completed` or `error` event to the backplane upon termination. - 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.
### 🔍 Reviewer (Verification Agent) ### 🛡️ Role Suitability Check Principle (자신의 역할 범위 수행 원칙)
- **Core Responsibility**: Verify source code changes (Diff) and implementation specifications submitted by Workers. Reviewers act as facilitators by detecting security vulnerabilities, proposing performance improvements, and examining design consistency. - 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).
- **Provide Concrete Alternatives**: Simply rejecting changes (`NOT PASS`) is forbidden. When raising an issue, Reviewers must propose a **concrete, stable, and verified alternative code block or solution**. - **If an agent receives a task that does not fit its role**, it must either:
- **Complementary Cross-Verification**: Leverage the unique characteristics of different agent models (e.g., Flash-class models are skilled at capturing semantic shell bugs, while Opus/Sonnet-class models excel at API signatures and logical regression analysis) to perform parallel and mutually-supportive reviews. 1. Recommend the optimal agent session to delegate the task to, or
2. Perform the task directly if strictly necessary for project continuity.
--- ---
@@ -59,34 +66,42 @@ Asynchronous communication and state management between agents are controlled vi
sequenceDiagram sequenceDiagram
autonumber autonumber
actor User as User actor User as User
participant PM as Project Manager participant GM as General Manager
participant W as Worker participant DTL as Developer Team Leader
participant R as Reviewers participant RTL as Reviewer Team Leaders
participant M as MQTT Backplane participant M as MQTT Backplane
User->>PM: Hand over requirements User->>GM: Hand over requirements
Note over PM: Run grill-me & plan tasks GM->>DTL: Delegate task (e.g., create landing page)
PM->>M: Register Job & start Subscriber Note over DTL: Analyze, breakdown & spawn parallel subagents
PM->>W: Delegate task (Provide Job ID & Brief) DTL->>M: Publish 'started' event
W->>M: Publish 'started' event Note over DTL: Modify code & implement
Note over W: Modify code & implement DTL->>M: Publish 'completed'
W->>M: Publish 'completed' (or 'error') DTL->>RTL: Request review (I created landing page. Please review it)
PM->>R: Request parallel review (Provide Diff) Note over RTL: Cross-analysis & verification
Note over R: Cross-analysis & verification alt Defect Found (Reviewer feedback)
alt Defect Found RTL->>DTL: NOT PASS / Feedback (Must include reason & improvement direction)
R->>PM: NOT PASS (Feedback with alternatives) Note over DTL: DTL checks validity of suggestions
Note over PM: PM directly fixes minor defects alt Valid feedback
PM->>W: Apply feedback & re-delegate 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 else Verification Pass
R->>PM: PASS RTL->>DTL: PASS
end end
PM->>User: Report final pass & commit changes DTL->>GM: Send completion signal
GM->>User: Notify task completion
``` ```
1. **Planning and Allocation**: The PM defines requirements and outlines independent jobs to avoid conflicting dependencies. 1. **Planning and Allocation**: The General Manager delegates the task to the Developer Team Leader.
2. **Execution and Notification**: The PM launches a subscriber, then assigns the job to a Worker session. The Worker performs the logic and sends a terminal event, automatically closing the session. 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. **Cross-Verification Iteration (Review Loop)**: Once the task is complete, the PM circulates the changes to the Reviewer agents in parallel. The modify-reject cycle repeats until all reviewers yield a `PASS`, ensuring high-quality code. 3. **Objection & Refinement Loop**:
4. **Release and Cleanup**: Code that passes verification is committed to Git, and temporary session resources are reclaimed. - 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.
--- ---
@@ -123,4 +138,4 @@ Use this checklist when deploying this agent orchestration model to a new projec
--- ---
*This guide balances collaboration efficiency with strict code security. Any required changes must be discussed and agreed upon by the PM and all Reviewers before updating this document.* *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.*
+42
View File
@@ -305,6 +305,8 @@ def _validate(d):
raise SystemExit(f"VALIDATE: tmux_sessions[{i}] not a mapping") raise SystemExit(f"VALIDATE: tmux_sessions[{i}] not a mapping")
if not s.get('name') or not s.get('status'): if not s.get('name') or not s.get('status'):
raise SystemExit(f"VALIDATE: tmux_sessions[{i}] missing name/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: if s['status'] not in valid:
raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} bad status {s['status']!r}") raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} bad status {s['status']!r}")
if not isinstance(s.get('pane'), dict): if not isinstance(s.get('pane'), dict):
@@ -366,10 +368,17 @@ try:
d['tmux_sessions'] = [] d['tmux_sessions'] = []
old_terminals = get_terminal_set(d) 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) --- # --- caller mutation (module scope: sees d, yaml, os, glob, subprocess) ---
exec(compile(os.environ['AGENT_SESSIONS_MUTATION'], '<mutation>', 'exec'), globals()) exec(compile(os.environ['AGENT_SESSIONS_MUTATION'], '<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) _validate(d)
# Separate globals and sessions for normalization # Separate globals and sessions for normalization
@@ -487,6 +496,10 @@ def hermes_exists(uuid):
return False return False
def cline_exists(uuid):
return os.path.exists(f"{home}/.cline/data/sessions/{uuid}/{uuid}.json")
def emit(u): def emit(u):
print(u) print(u)
raise SystemExit(0) raise SystemExit(0)
@@ -536,6 +549,10 @@ for s in sessions:
cand = s.get('hermes_conversation_id_own') cand = s.get('hermes_conversation_id_own')
if cand and hermes_exists(cand): if cand and hermes_exists(cand):
emit(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 # 2) disk scan scoped to THIS workspace
if agent == 'claude': if agent == 'claude':
@@ -578,6 +595,27 @@ elif agent == 'hermes':
cand = None cand = None
if cand: if cand:
emit(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 # 3) agent_identities cache, ONLY when its project_cwd == this workspace
ai = {} ai = {}
@@ -609,6 +647,10 @@ if ai_agent.get('project_cwd') == ws:
cand = ai_agent.get('session_id') or ai.get('conversation_id') cand = ai_agent.get('session_id') or ai.get('conversation_id')
if cand and hermes_exists(cand): if cand and hermes_exists(cand):
emit(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('') print('')
PYEOF PYEOF
@@ -74,12 +74,12 @@ To prevent this, you can run this skill inside an **isolated tmux server** using
``` ```
2. **Via Option Flag**: 2. **Via Option Flag**:
```bash ```bash
bash scripts/create_session.sh --workspace /path/to/project --agent claude --tmux-server multi-agent-canary bash scripts/create_session.sh --workspace /path/to/project --agent claude --role developer --tmux-server multi-agent-canary
``` ```
3. **Submit Job Integration**: 3. **Submit Job Integration**:
You can automatically register a delegated job with a prompt when creating a session: You can automatically register a delegated job with a prompt when creating a session:
```bash ```bash
bash scripts/create_session.sh --workspace /path/to/project --agent claude --submit-job "Task prompt here" bash scripts/create_session.sh --workspace /path/to/project --agent claude --role developer --submit-job "Task prompt here"
``` ```
### Recommended Alias ### Recommended Alias
@@ -173,7 +173,7 @@ Use the `agent-sessions-yaml-edit` script in `scripts/` to safely append (preser
```bash ```bash
bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
--workspace "$WORKSPACE" --agent "$AGENT" --session "$SESSION_NAME" --workspace "$WORKSPACE" --agent "$AGENT" --role "$ROLE" --session "$SESSION_NAME"
``` ```
The script handles the YAML append, pane capture, and the `last_visible_status` placeholder. The script handles the YAML append, pane capture, and the `last_visible_status` placeholder.
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# create_session.sh — multi-agent-mux-create 의 부속 스크립트 # create_session.sh — multi-agent-mux-create 의 부속 스크립트
# Usage: # Usage:
# bash create_session.sh --workspace <path> --agent <claude|agy> [--session <name>] [--wrapper] # bash create_session.sh --workspace <path> --agent <claude|agy> --role <role> [--session <name>] [--wrapper]
# #
# 동작: # 동작:
# 1) preflight: tmux/claude/agy 가용성, workspace 존재 # 1) preflight: tmux/claude/agy 가용성, workspace 존재
@@ -23,11 +23,12 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $0 --workspace <path> --agent <claude|agy|hermes> [options] Usage: $0 --workspace <path> --agent <claude|agy|hermes|cline> --role <role> [options]
Options: Options:
--workspace PATH project directory (required) --workspace PATH project directory (required)
--agent AGENT claude | agy | hermes (required) --agent AGENT claude | agy | hermes | cline (required)
--role ROLE assigned role (required)
--session NAME tmux session name (default: derived from workspace) --session NAME tmux session name (default: derived from workspace)
--wrapper force use of ~/.local/bin/<session> wrapper even if not present --wrapper force use of ~/.local/bin/<session> wrapper even if not present
--dry-run print commands without executing --dry-run print commands without executing
@@ -39,6 +40,7 @@ EOF
WORKSPACE="" WORKSPACE=""
AGENT="" AGENT=""
ROLE=""
SESSION_NAME="" SESSION_NAME=""
USE_WRAPPER=0 USE_WRAPPER=0
DRY_RUN=0 DRY_RUN=0
@@ -49,6 +51,7 @@ while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--workspace) WORKSPACE="$2"; shift 2 ;; --workspace) WORKSPACE="$2"; shift 2 ;;
--agent) AGENT="$2"; shift 2 ;; --agent) AGENT="$2"; shift 2 ;;
--role) ROLE="$2"; shift 2 ;;
--session) SESSION_NAME="$2"; shift 2 ;; --session) SESSION_NAME="$2"; shift 2 ;;
--wrapper) USE_WRAPPER=1; shift ;; --wrapper) USE_WRAPPER=1; shift ;;
--dry-run) DRY_RUN=1; shift ;; --dry-run) DRY_RUN=1; shift ;;
@@ -66,6 +69,7 @@ fi
# Preflight # Preflight
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; } [ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; }
[ -n "$AGENT" ] || { echo "ERROR: --agent 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; } [ -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 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; } command -v "$AGENT" >/dev/null || { echo "ERROR: $AGENT CLI not in PATH" >&2; exit 1; }
@@ -86,6 +90,11 @@ elif [ "$AGENT" = "hermes" ]; then
echo "ERROR: hermes is not functional. Run 'hermes setup' first." >&2 echo "ERROR: hermes is not functional. Run 'hermes setup' first." >&2
exit 1 exit 1
fi 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 fi
# 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A) # 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A)
@@ -119,7 +128,10 @@ spawn() {
hermes) hermes)
_tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "hermes" _tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "hermes"
;; ;;
*) echo "ERROR: --agent must be claude, agy or hermes, got: $AGENT" >&2; exit 2 ;; 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 esac
} }
@@ -145,6 +157,7 @@ case "$AGENT" in
claude) CMD_FULL='claude --dangerously-skip-permissions' ;; claude) CMD_FULL='claude --dangerously-skip-permissions' ;;
agy) CMD_FULL='agy --dangerously-skip-permissions' ;; agy) CMD_FULL='agy --dangerously-skip-permissions' ;;
hermes) CMD_FULL='hermes' ;; hermes) CMD_FULL='hermes' ;;
cline) CMD_FULL='cline -i' ;;
esac esac
# 시작 명령 # 시작 명령
@@ -161,7 +174,7 @@ case "$AGENT" in
START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"claude --dangerously-skip-permissions\"" START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"claude --dangerously-skip-permissions\""
fi fi
;; ;;
agy|hermes) agy|hermes|cline)
START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
;; ;;
esac esac
@@ -174,6 +187,8 @@ if [ -n "$SUBMIT_JOB_PROMPT" ]; then
delegate_agent="claude-code" delegate_agent="claude-code"
elif [ "$AGENT" = "hermes" ]; then elif [ "$AGENT" = "hermes" ]; then
delegate_agent="hermes-agent" delegate_agent="hermes-agent"
elif [ "$AGENT" = "cline" ]; then
delegate_agent="cline-agent"
else else
delegate_agent="antigravity-cli" delegate_agent="antigravity-cli"
fi fi
@@ -191,7 +206,7 @@ fi
# 모든 값은 환경변수로 전달 — heredoc interpolation 없음 (P1-B). # 모든 값은 환경변수로 전달 — heredoc interpolation 없음 (P1-B).
# 자식 pid 는 bash 에서 pgrep 으로 미리 구함 (P2: 도구명 필터). # 자식 pid 는 bash 에서 pgrep 으로 미리 구함 (P2: 도구명 필터).
CHILD_PID=0 CHILD_PID=0
if { [ "$AGENT" = "agy" ] || [ "$AGENT" = "hermes" ]; } && [ -n "$PANE_PID" ]; then 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=$(pgrep -P "$PANE_PID" -x "$AGENT" 2>/dev/null | head -1 || true)
CHILD_PID="${CHILD_PID:-0}" CHILD_PID="${CHILD_PID:-0}"
fi fi
@@ -201,9 +216,10 @@ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
TMUX_EPOCH="$TMUX_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \ TMUX_EPOCH="$TMUX_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \
CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \ CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \
TMUX_SERVER_NAME="${TMUX_SERVER_NAME:-default}" \ TMUX_SERVER_NAME="${TMUX_SERVER_NAME:-default}" \
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" <<'PYEOF' DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" <<'PYEOF'
name = os.environ['SESSION_NAME'] name = os.environ['SESSION_NAME']
agent = os.environ['AGENT'] agent = os.environ['AGENT']
role = os.environ['ROLE']
pid = os.environ.get('PANE_PID', '') pid = os.environ.get('PANE_PID', '')
epoch = os.environ.get('TMUX_EPOCH', '') epoch = os.environ.get('TMUX_EPOCH', '')
server_name = os.environ.get('TMUX_SERVER_NAME', 'default') server_name = os.environ.get('TMUX_SERVER_NAME', 'default')
@@ -222,6 +238,7 @@ sessions[:] = [s for s in sessions if s.get('name') != name]
entry = { entry = {
'name': name, 'name': name,
'status': 'running', 'status': 'running',
'role': role,
'tmux_session_created_at': os.environ['NOW_ISO'], 'tmux_session_created_at': os.environ['NOW_ISO'],
'tmux_session_epoch': int(epoch) if epoch.isdigit() else 0, 'tmux_session_epoch': int(epoch) if epoch.isdigit() else 0,
'tmux_server': server_name, 'tmux_server': server_name,
@@ -265,6 +282,11 @@ elif agent == 'hermes':
entry['child_pid'] = int(cp) if cp.isdigit() else 0 entry['child_pid'] = int(cp) if cp.isdigit() else 0
entry['hermes_conversation_id_own'] = None entry['hermes_conversation_id_own'] = None
entry['last_visible_status'] = "TUI started; awaiting first user message" 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) sessions.append(entry)
@@ -1,7 +1,7 @@
# multi-agent-mux-delegate-job 스킬 # multi-agent-mux-delegate-job 스킬
작업(Job)을 자율 에이전트(claude-code/codex/opencode/human)에게 위임하고 MQTT 작업(Job)을 자율 에이전트(claude-code/hermes/agy/cline/codex/opencode/human)에게 위임하고 MQTT
이벤트 채널로 비동기 관찰하는 Hermes 스킬. **시작점은 [`SKILL.md`](./SKILL.md).** 이벤트 채널로 비동기 관찰하는 범용 에이전트 협업 스킬. **시작점은 [`SKILL.md`](./SKILL.md).**
- 프로토콜/스키마: [`job-protocol.md`](./job-protocol.md) - 프로토콜/스키마: [`job-protocol.md`](./job-protocol.md)
- 브로커 PoC→운영 전환: [`mqtt-broker-setup.md`](./mqtt-broker-setup.md) - 브로커 PoC→운영 전환: [`mqtt-broker-setup.md`](./mqtt-broker-setup.md)
@@ -1,385 +1,94 @@
--- ---
name: multi-agent-mux-delegate-job name: multi-agent-mux-delegate-job
description: "Delegate a unit of work to any autonomous agent (claude-code, codex, opencode, or a human) and observe it asynchronously over an MQTT event channel. Each job gets a unique id, a registry record (prompt, broker, status, timeouts), and a single per-job topic that carries started/permission_required/progress/completed/error events as schema-versioned JSON. The delegator starts a subscriber first, runs the agent, and treats a completed/error event or a timeout as the job's terminal state. Ships a working reference implementation (publish_event.py, job_subscriber.py, registry.py, mqtt_common.py, multi-agent-mux-delegate-job wrapper) plus a PoC-to-production path: validate on a public broker, then move to an authenticated TLS broker by changing config only — no code change. Use when you need fire-and-observe delegation, multi-job fan-out across tmux sessions, or a uniform completion-signal protocol shared by several agent types." 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.0.0 version: 1.1.0
author: Hermes Agent author: Multi-Agent System
license: MIT license: MIT
platforms: [linux, macos, windows] platforms: [linux, macos, windows]
metadata:
hermes:
tags: [agent-delegation, mqtt, jobs, orchestration, async-completion]
related_skills: [claude-code, codex, opencode, hermes-agent-skill-authoring]
--- ---
# multi-agent-mux-delegate-job — Async Job Delegation over MQTT # multi-agent-mux-delegate-job — Async Job Delegation over MQTT
Delegate a unit of work to an autonomous agent, then **observe** it instead of 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.
blocking on it. Every job gets a unique id and a registry record; the agent
publishes lifecycle events (`started`, `permission_required`, `progress`,
`completed`, `error`) to a per-job MQTT topic; the delegator subscribes and
treats `completed`/`error` — or a timeout — as the terminal state.
This skill is a **reference implementation**: copy the files in this directory This skill allows any agent (`claude-code`, `hermes`, `agy`, `cline`, etc.) to play any role: **Orchestrator/Delegator**, **Worker/Implementer**, or **Reviewer**.
into your project and customise. The `communication_over_mqtt` project is the
canonical concrete instance.
## Overview ---
The model is deliberately small. A **job** is one delegated task. An **agent** ## Roles in Multi-Agent Mux
is a worker (a claude-code tmux session, a codex run, a human). The **registry**
(`.mam/jobs/<id>.json`) holds everything about a job so nothing important
lives in environment variables — which means one tmux session can process many
jobs sequentially, and many sessions can fan out in parallel, with no env
collisions. The **event channel** is one MQTT topic per job carrying JSON
payloads; `event` discriminates the type.
Responsibility is split into exactly one entry point each: - **Orchestrator (Delegator)**: Initiates the job, coordinates other agents, handles loops and reviews, and commits final changes.
[`publish_event.py`](./scripts/publish_event.py) emits events (registry lookup, - **Worker (Implementer)**: Receives the brief file or task prompt, performs the implementation, and emits started/completed/error events.
monotonic `seq`, retry+backoff) and [`job_subscriber.py`](./scripts/job_subscriber.py) - **Reviewer**: Evaluates git diffs or artifacts produced by the worker, and responds with a `completed` event containing `"PASS"` or feedback.
observes them (timeouts, terminal state machine, defensive parsing). Shared
logic lives in [`mqtt_common.py`](./scripts/mqtt_common.py); registry I/O in
[`registry.py`](./scripts/registry.py). The demo `publisher.py`/`subscriber.py`
in the host project stay frozen.
Two stages, same code. **PoC** runs on the public `broker.hivemq.com` to wire up ---
the protocol. **Production** moves to your own authenticated TLS broker — the
switch is **config only** (env vars + the registry `broker.*` block), never a
code change. See [`mqtt-broker-setup.md`](./mqtt-broker-setup.md).
## When to Use / When NOT to Use ## Core Commands (CLI)
**Use when:** The `multi-agent-mux-delegate-job` bash wrapper handles job registration, subscriber management, agent session targeting, and validation hooks:
- you want **fire-and-observe** delegation — kick off work and get a completion
signal rather than blocking a terminal;
- several agent types (claude-code, codex, opencode, human) must follow **one**
completion protocol;
- you need **multi-job fan-out** across tmux sessions with safe job claiming;
- you want a clean PoC → authenticated-broker upgrade path.
**Do NOT use when:**
- a one-shot `claude -p '…'` that returns inline is enough (no async signal
needed) — just use the [claude-code](../claude-code/SKILL.md) skill directly;
- you need request/response RPC or large artifact transfer (this is a
one-direction event stream, not a data bus);
- the payload would carry secrets and you're still on the public broker — move
to the own-broker stage first.
## Quick Start
The one-line wrapper handles register + subscriber-first + agent launch. If
you're new, **start here** and only fall back to the manual 5-step flow when
you need finer control.
```bash ```bash
# 1) one line: register → start subscriber → launch agent in tmux # 1) Submit a new job to a targeted agent session (e.g. tmux session name 'demo')
# (uses public broker by default; last stdout line is the audit-log dir)
multi-agent-mux-delegate-job submit \ multi-agent-mux-delegate-job submit \
--agent claude-code \ --agent <claude-code|hermes-agent|agy-agent|cline-agent|human> \
--prompt "정렬 문제 10개를 만들어 sort_problems.md로 저장" \ --agent-session tmux:<session_name> \
--workdir /path/to/project \ --prompt "Task description or instructions here" \
--agent-session tmux:demo \
--timeout 3600 --idle-timeout 120 --timeout 3600 --idle-timeout 120
# → stdout: registered job: <JID>
# subscriber pid: …
# agent launched in tmux session: demo
# subscriber output: <one line per event>
# /path/to/project/.mam/delegate_job_logs/<JID> ← audit log dir
# 2) at any time, query the job or its audit log # 2) Submit a job with a feedback loop (Worker-Reviewer Loop)
multi-agent-mux-delegate-job status --job <JID> multi-agent-mux-delegate-job submit \
multi-agent-mux-delegate-job logs <JID> # pretty timeline --agent <worker_agent> --agent-session tmux:<worker_session> \
multi-agent-mux-delegate-job logs --list # every job, live status --type loop --reviewer <reviewer_agent> --reviewer-session tmux:<reviewer_session> \
--prompt "Task description"
# 3) run a user-supplied validator against the job's artifacts # 3) Check job status and audit logs
multi-agent-mux-delegate-job verify --job <JID> --validate ./validate.sh multi-agent-mux-delegate-job status --job <JOB_ID>
multi-agent-mux-delegate-job logs <JOB_ID> # 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 <JOB_ID> --validate ./validate.sh
``` ```
The wrapper enforces the **subscribe-before-publish** ordering and **forwards ---
the freshly-minted `JOB_ID` into the agent's prompt** (so the agent calls
`publish_event.py --job <JID>` with the right id — see Pitfall §"Wrong job_id
propagated to the agent"). When you need finer control, the manual flow is:
```bash ## Task Delegation Types
# Manual 5-step (same outcome, more knobs)
PY=.venv/bin/python
SKILL=./.agents/skills/multi-agent-mux-delegate-job/scripts
# 1) register Supported job types include:
JID=$($PY "$SKILL/registry.py" register \ - `direct` (default): Single agent execution (direct tasking).
--prompt "…" --agent claude-code --agent-session tmux:demo \ - `loop` (Worker-Reviewer Loop): Alternates worker execution and reviewer evaluation until reviewer approves (`PASS`) or iterations run out.
--timeout 3600 --idle-timeout 120) - `discuss` (Research & Discussion): Collaboration between two agents to reach a consensus (e.g., agreeing on a design or plan).
# 2) START THE SUBSCRIBER FIRST (MQTT does not queue non-retained msgs) For detailed state machine diagrams and configurations, see [DELEGATION_TYPES.md](./DELEGATION_TYPES.md).
$PY "$SKILL/job_subscriber.py" --job "$JID" --timeout 3600 --idle-timeout 120 &
# 3) pass JID to the agent and instruct it to publish events with --job "$JID" ---
# (don't hard-code a job id you saw earlier — see Pitfall §"Wrong job_id")
# 4) on completion the subscriber prints events and exits 0/1/2 ## The Event Protocol Contract
# 5) inspect any time Every agent participating in the delegation contract must follow the same lifecycle publishing protocol using `publish_event.py`:
$PY "$SKILL/registry.py" get --job "$JID"
$PY "$SKILL/registry.py" logs "$JID" # positional job id
$PY "$SKILL/registry.py" logs --list
```
## Job Protocol 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 "<tool>:<reason>"`
3. **On Progress Update (Optional)**: Publish `progress` event.
`python3 ... --job "$JOB_ID" --event progress --detail "<status_update>"`
4. **On Success**: Publish `completed` event.
`python3 ... --job "$JOB_ID" --event completed --detail "<summary>"` (Reviewer should include `"PASS"` in the detail to approve).
5. **On Failure/Feedback**: Publish `error` event.
`python3 ... --job "$JOB_ID" --event error --detail "<reason_or_feedback>"`
One topic per job: `python/mqtt/jobs/<job_id>/events`. Payload (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 text",
"data": { "optional": "metadata" } }
```
- `seq` is monotonic per job (first = 1); the subscriber uses it to spot
reorder/duplication.
- `timestamp` is advisory — timeouts are measured from **receive** time.
- `detail`/`data` carry **no** secrets or absolute paths.
- A `schema_version` or `job_id` mismatch is **dropped** (defensive parsing).
`started` and `completed`/`error` are the mandatory bookends; `completed`→exit 0,
`error`→exit 1. Full catalogue + production `auth_token` handling:
[`job-protocol.md`](./job-protocol.md).
## Registry Format
```
.mam/jobs/<id>.json # metadata record (single source of truth)
.mam/jobs/<id>.events.log # append-only JSON-lines log (debug, optional)
.mam/jobs/.lock # fcntl advisory lock for the registry
```
The record holds `status`, `prompt`, `agent`, `agent_session`, a `broker` block,
`topic_prefix`, `timeout_sec`/`idle_timeout_sec`, `expected_artifacts`,
`last_seq`, and (production) `auth_token`. Because the `broker` block lives in
the record, `publish_event.py` connects from the registry alone. Concurrency,
the atomic rename trick, and multi-session job claiming are in
[`registry.md`](./registry.md).
## Audit Logs ## Audit Logs
Every job's lifecycle is mirrored to a **persistent, append-only audit log** Job lifecycle execution events are persistently mirrored to an append-only log under `.mam/delegate_job_logs/<job_id>/` (containing `meta.json`, `events.ndjson`, and `status.json`). Use `multi-agent-mux-delegate-job logs <job_id>` to view the timeline.
under `.mam/delegate_job_logs/` (override with `DELEGATE_JOB_LOGS_DIR`;
default `<cwd>/.mam/delegate_job_logs`). Unlike the registry — live state
mutated in place and liable to be cleaned up — the audit log is durable
history you can replay after the fact. It is git-ignored.
``` ---
.mam/delegate_job_logs/<job_id>/
meta.json # registration snapshot: prompt, agent, broker, timeouts, …
events.ndjson # append-only, one JSON event per line, in time order
status.json # current status only (fast point-query)
```
**What is logged, automatically:** ## Best Practices and Pitfalls
| When | `events.ndjson` line | Written by | - **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.
| job registered | `registered` (also seeds meta.json + status.json) | `registry.register_job` | - **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.
| any status change | `status_changed` (`from`/`to`; also rewrites status.json) | `update_job_status`, `pick_pending` | - **Batch Grouping**: Group non-overlapping tasks into batches to parallelize execution across multiple agent sessions, reducing overhead.
| event published | `published` (carries the exact payload — reproducible) | `publish_event.py` |
| event received | `received` (subscriber's external view) | `job_subscriber.py` |
Both the emitter side (`published`) and the observer side (`received`) are
recorded, so a dropped publish or a missed receive is still visible from the
other. Every write is **best-effort and isolated** — an fcntl-locked append
guarded by `try/except` that only ever emits a `logger.warning`, so a logging
failure can never break a publish, a subscribe, or a registry write. stdout is
never touched.
**Reading them:**
```bash
multi-agent-mux-delegate-job logs <job_id> # pretty-print one job's timeline
multi-agent-mux-delegate-job logs --list # summarise every logged job (with live status)
# or directly via the registry CLI:
$PY scripts/registry.py logs <job_id> [--tail N] [--json]
$PY scripts/registry.py logs --list [--json]
```
`submit` prints the job's audit-log directory as its last stdout line, so a
caller can `tail -n1` to locate it.
## Broker Setup
| Stage | Broker | Auth | Transport |
|-------|--------|------|-----------|
| PoC | `broker.hivemq.com` | none | 1883 plaintext |
| Production | self-hosted Mosquitto/EMQX | user/pass + ACL | 8883 TLS |
All connection settings come from env (`MQTT_BROKER`, `MQTT_PORT`, `MQTT_TLS`,
`MQTT_USERNAME`/`MQTT_PASSWORD`, `MQTT_CA_CERTS`, …) resolved by
`broker_config_from_env()`, with the registry `broker.*` block overriding per
job. Moving to your own broker is **config only**: install Mosquitto, set
`persistence true` + `acl_file` + `password_file` + a TLS `listener 8883`, grant
the worker `write python/mqtt/jobs/+/events` and Hermes `read`, then flip
`MQTT_TLS=1` and fill the registry `broker.*`. Step-by-step (conf, ACL,
`mosquitto_passwd`, self-signed/private-CA certs, cut-over verification):
[`mqtt-broker-setup.md`](./mqtt-broker-setup.md).
## Agent Adapters
Each agent voluntarily follows the contract: receive a `JOB_ID` (or registry
path), call `publish_event.py` at lifecycle points, exit 0/1/2. **The contract
in one line**: every event call uses `--job "$JOB_ID"` where `$JOB_ID` is the
**freshly-issued id from the registry record for *this* delegation** — never a
job_id you saw in an earlier session (Pitfall §"Wrong job_id propagated to the
agent").
- **claude-code** — Claude Code calls `publish_event.py` via its Bash tool at
lifecycle points. `submit --mode tmux` injects a prompt that already names
`$JOB_ID`; if you drive claude manually, hand it the id explicitly. Reference
instruction block (the wrapper injects something equivalent):
```text
Your job_id is "$JOB_ID" (read it from the registry record for this delegation —
do not reuse any job_id you saw before).
On start: $PY multi-agent-mux-delegate-job/scripts/publish_event.py --job "$JOB_ID" --event started
On permission: $PY … --job "$JOB_ID" --event permission_required --detail "<tool>:<what>"
On progress: $PY … --job "$JOB_ID" --event progress --detail "<short status>"
On success: $PY … --job "$JOB_ID" --event completed --detail "<one-line summary>"
On failure: $PY … --job "$JOB_ID" --event error --detail "<one-line reason>"
Task: <the user's prompt>
The subscriber for "$JOB_ID" is already running; your completed/error event
ends the job. Exit codes: 0 completed, 1 error, 2 publish failure.
```
See [claude-code](../claude-code/SKILL.md) for tmux orchestration patterns.
- **codex** — same contract. Invoke `codex exec "<instruction-block-above>"` or
wire `publish_event.py` as an MCP tool so the agent can call it directly.
- **opencode** — wire `publish_event.py` as a tool/command the agent can call;
identical event points.
- **human** — a person does the work, reads the registry record, then runs
`publish_event.py --job <id> --event completed` (or `error`) by hand.
## User Interface
The [`multi-agent-mux-delegate-job`](./multi-agent-mux-delegate-job) bash wrapper bundles register +
subscribe-first + run-agent + validate:
```bash
multi-agent-mux-delegate-job submit --agent claude-code \
--prompt "정렬 문제 10개를 만들어 sort_problems.md로 저장" \
--workdir /path/to/project --timeout 3600 [--validate ./validate.sh]
multi-agent-mux-delegate-job status --job <id> # one record, pretty-printed
multi-agent-mux-delegate-job list # all jobs, one line each
multi-agent-mux-delegate-job verify --job <id> --validate ./validate.sh # runs it, reports exit code
multi-agent-mux-delegate-job wait [--job <id>] # block until terminal (else --wait-any)
```
`submit` **always starts the subscriber before the agent** (the ordering
dependency), runs the agent in `--mode print` (one-shot) or `--mode tmux`, and
calls `--validate` afterward if given. The skill automates job-id generation,
registry creation, broker resolution, subscriber-first ordering, agent launch,
and completion detection; it does **not** automate the agent's internals or your
business-logic validation — those are hooks you fill (`validate.sh` reads
`$JOB_ID`/`$REGISTRY_DIR`).
## Common Pitfalls
- **Publishing before subscribing** — MQTT does not queue non-retained messages
for absent subscribers. Start `job_subscriber.py` *before* the agent, or rely
on retained terminal events (production). `submit` enforces this.
- **Wrong job_id propagated to the agent** — the wrapper prints a fresh `JOB_ID`
on every `submit`. If your agent instruction (or the wrapper's prompt template)
hard-codes an old job_id, the agent calls `publish_event.py --job <wrong>`,
the subscriber's defensive parser drops it as a `job_id` mismatch, and the
delegator waits until idle timeout (exit 2). Fix: instruct the agent to
**read the job_id from the registry record for *this* delegation** (or pass it
in via env / `--prompt` interpolation), never from prior runs. `submit`'s
default prompt template interpolates `$JOB_ID` for you — if you build a custom
prompt, do the same.
- **tmux session name collision** — `submit --mode tmux` derives the session
name from `--agent-session tmux:<name>` (default `tmux:claude`). If a session
with that name is already attached (e.g. you ran the demo and the previous
session is still open), `tmux new-session -d -s <name>` fails and the agent
never launches. Pick a unique `--agent-session` per concurrent delegation
(e.g. `tmux:demo`, `tmux:claude-a`, `tmux:claude-b`) or kill the stale one
(`tmux kill-session -t claude`) before re-running.
- **Timeout before `started`** — a cold-starting agent may not emit `started`
for a while; the wall-clock timeout starts at subscribe time so a stuck agent
still terminates. Don't set `--timeout` so low you false-positive a slow start.
- **No retry on publish** — a dropped `completed` would hang the delegator
forever; `publish_event.py` retries with exponential backoff and exits 2 if it
still fails, so the delegator is never left waiting silently.
- **QoS-1 duplicates / reorders** — a terminal event can arrive twice, or
`error` can trail `completed`; the subscriber's terminal state machine
finalises each job once and ignores the rest.
- **Trusting the public broker** — anyone can publish there; never make a real
decision on a PoC signal. Add `auth_token` + an authenticated broker first.
- **Secrets in `detail`/`data`** — keep payloads generalised; no paths, keys, or
tokens (except the production `auth_token` in `data`).
## Subagent Orchestration Pattern
When using this skill from a Hermes `delegate_task` subagent to dispatch work to
a coding-agent CLI (agy/claude) running in a tmux session, the following pattern
has been verified (2026-06-21, 6-batch refactoring sprint):
### Roles
- **Main worker** (implementation): one agent session (e.g. `agy-new`) receives
brief files and executes code changes.
- **Reviewers** (spec compliance + code quality): two other agent sessions
(e.g. `agy-existing`, `claude-existing`) review the diff in parallel.
- **Hermes** (orchestrator): dispatches subagents, verifies diffs, commits,
and falls back to direct fixes when reviewers find issues.
### Key lessons learned
1. **Brief delivery via file path** — don't paste long briefs inline via
`tmux send-keys`; the TUI may swallow them. Instead, send a short instruction
like "follow /tmp/batch1-brief.md" and let the agent read the file.
2. **Polling vs MQTT subscriber** — for short tasks (<5min), pane polling
(`capture-pane` + grep for completion markers) is simpler and more reliable
than registering a job via `registry.py` + `job_subscriber.py`. Use MQTT
subscriber only for long-running jobs (>5min) where push notification matters.
3. **Reviewers catch different bugs** — in practice, agy (Flash) caught
semantic issues (slash matching, export scope), while claude (Opus) caught
API signature mismatches (paho v2 5-arg vs 4-arg `on_disconnect`). Two
reviewers with different models provide complementary coverage.
4. **Hermes fallback fix** — when reviewers find a small, well-defined issue
(wrong argument count, missing slash), Hermes should fix it directly rather
than re-dispatching the implementer. This saves a full round-trip.
5. **Batch grouping** — group 2-3 FW items per batch when they touch different
files (no file overlap). This amortises the dispatch overhead. Items touching
the same file must be in separate batches to avoid conflicts.
6. **Pane Snapshots & Truncation Prevention** — to prevent long agent responses from being scrolled out and truncated due to TUI viewport limitations, enforce the following snapshotting pattern:
- Immediately after dispatching a brief, capture the pre-brief pane buffer via `capture-pane -S -200`.
- During long execution, run a background loop taking incremental snapshots (e.g. every 30 seconds `>> /tmp/pane-snap.txt`).
- Immediately after job termination, capture the entire final pane state to ensure no terminal logs are lost.
## Verification Checklist
- [ ] `started` → `completed` over the public broker: subscriber prints the
lines and exits **0**.
- [ ] `error` path: subscriber exits **1**.
- [ ] timeout path: no terminal event within `--timeout`/`--idle-timeout` →
exit **2**.
- [ ] polluted payload (bad JSON, wrong `schema_version`, wrong `job_id`) is
dropped with a warning, not crashed on.
- [ ] one tmux session processes two registry jobs in sequence; a second
session with a different `agent_session` claims only its own.
- [ ] broker cut-over: same scripts reach an authenticated TLS broker with env
changes only; a credential without write ACL is rejected; a late
subscriber still receives the retained terminal event.
- [ ] `publisher.py`/`subscriber.py`/`README.md` demo on `python/mqtt/sample`
still works unchanged (regression).
- [ ] **audit log integrity** — for a completed job,
`.mam/delegate_job_logs/<JID>/events.ndjson` contains `registered` →
`received started` → `published completed` (in that order), and
`status.json.status == "completed"` matches the registry record. A
logging failure (e.g. read-only log dir) does not break the publish or
subscribe path — only a `logger.warning` is emitted.
- [ ] **end-to-end demo smoke** — run
`multi-agent-mux-delegate-job submit --agent claude-code --agent-session tmux:demo-smoke
--prompt "echo hello and call publish_event.py --job <JID>
--event completed" --timeout 120` and confirm
(a) registered job id echoed, (b) subscriber pid echoed, (c) tmux session
name printed, (d) `events.ndjson` grows as the agent runs, (e) final
stdout line is the audit-log dir.
@@ -221,7 +221,6 @@ Task: $current_prompt"
# Trigger agent # Trigger agent
run_agent "$JOB_ID" "$instructions" "$current_session" run_agent "$JOB_ID" "$instructions" "$current_session"
# Wait for subscriber
# Wait for subscriber # Wait for subscriber
local sub_rc=0 local sub_rc=0
wait "$sub_pid" || sub_rc=$? wait "$sub_pid" || sub_rc=$?
@@ -253,10 +252,9 @@ Task: $current_prompt"
# Worker completed successfully, now switch to reviewer # Worker completed successfully, now switch to reviewer
current_role="reviewer" current_role="reviewer"
current_session="$REVIEWER_SESSION" current_session="$REVIEWER_SESSION"
# Build reviewer prompt based on type # Build reviewer prompt based on type
if [[ "$TYPE" == "loop" ]]; then if [[ "$TYPE" == "loop" ]]; then
current_prompt="Review the changes/artifacts generated for job $JOB_ID. Check if they meet the requirements. If correct, publish completed event with 'PASS'. If there are issues, publish error event with detailed feedback/nits." current_prompt="Review the changes/artifacts generated for job $JOB_ID. Check if they meet the requirements. If correct, publish completed event with 'PASS'. If there are issues, publish error event with detailed feedback/nits. CRITICAL: When raising issues or giving a review, you MUST include the exact reason for the issue and a clear direction for improvement (문제 제시에 대한 이유와 확실한 개선 방향을 반드시 포함해야 합니다)."
elif [[ "$TYPE" == "discuss" ]]; then elif [[ "$TYPE" == "discuss" ]]; then
current_prompt="Read draft/documents generated for job $JOB_ID. Review the feasibility and content. Write your feedback/objections. If you agree with the plan, reply with 'AGREE'." current_prompt="Read draft/documents generated for job $JOB_ID. Review the feasibility and content. Write your feedback/objections. If you agree with the plan, reply with 'AGREE'."
fi fi
@@ -265,7 +263,7 @@ Task: $current_prompt"
echo "Reviewer did not complete successfully (status: $job_status). Terminating workflow." echo "Reviewer did not complete successfully (status: $job_status). Terminating workflow."
break break
fi fi
# Reviewer finished. Check if pass/agree # Reviewer finished. Check if pass/agree
local success=0 local success=0
if [[ "$TYPE" == "loop" ]]; then if [[ "$TYPE" == "loop" ]]; then
@@ -277,7 +275,7 @@ Task: $current_prompt"
success=1 success=1
fi fi
fi fi
if [[ "$success" == "1" ]]; then if [[ "$success" == "1" ]]; then
echo "Reviewer approved the work. Finalizing job as completed." echo "Reviewer approved the work. Finalizing job as completed."
"$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" status --job "$JOB_ID" --set "completed" "$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" status --job "$JOB_ID" --set "completed"
@@ -293,7 +291,7 @@ Task: $current_prompt"
iteration=$((iteration + 1)) iteration=$((iteration + 1))
current_role="worker" current_role="worker"
current_session="$AGENT_SESSION" current_session="$AGENT_SESSION"
current_prompt="The reviewer provided the following feedback for job $JOB_ID: $feedback. Please modify the code/artifacts to address these comments." current_prompt="The reviewer provided the following feedback for job $JOB_ID: $feedback. Please modify the code/artifacts to address these comments. CRITICAL: As the Developer Team Leader, you must thoroughly review the suggested modifications, verify their validity, adopt/implement them if valid, and if you judge any recommendation to be invalid, do NOT implement it but instead explain your reasons clearly in your response and send it back to the reviewer (수정안을 최대한 꼼꼼히 검토하여 타당성을 검증하고, 타당하다면 수렴하여 수정을 진행하되, 타당하지 않다고 판단되는 부분이 있다면 그 이유를 명확히 밝혀 리뷰어에게 전달하십시오)."
fi fi
fi fi
done done
@@ -282,7 +282,7 @@ mkdir -p "$STATE_DIR"
# atomic_dump_yaml(flock + temp+rename) 로 같은 소스를 돌린다. atomic 래퍼에서는 # atomic_dump_yaml(flock + temp+rename) 로 같은 소스를 돌린다. atomic 래퍼에서는
# 'actions' 가 없으면 SystemExit(0) 으로 쓰기를 건너뛴다 (불필요한 재포맷 방지). # 'actions' 가 없으면 SystemExit(0) 으로 쓰기를 건너뛴다 (불필요한 재포맷 방지).
read -r -d '' RECON_SRC <<'PYEOF' || true read -r -d '' RECON_SRC <<'PYEOF' || true
import os, json, glob, subprocess, time import os, json, glob, subprocess, time, sqlite3
from datetime import datetime, timezone from datetime import datetime, timezone
import yaml import yaml
@@ -403,14 +403,28 @@ if tmux_confirmed:
name = t['name'] name = t['name']
if name in yaml_session_names: if name in yaml_session_names:
continue continue
if not (name.endswith('-creator-claude') or name.endswith('-creator-agy')): if name.endswith('-creator-claude'):
agent = 'claude'
elif name.endswith('-creator-agy'):
agent = 'agy'
elif name.endswith('-creator-hermes'):
agent = 'hermes'
elif name.endswith('-creator-cline'):
agent = 'cline'
else:
continue continue
srv = t.get('server', 'default') srv = t.get('server', 'default')
pm = pane_meta(name, srv) pm = pane_meta(name, srv)
if not pm: if not pm:
continue continue
agent = 'claude' if name.endswith('-creator-claude') else 'agy' if agent == 'claude':
cmd_full = 'claude --dangerously-skip-permissions' if agent == 'claude' else 'agy --dangerously-skip-permissions' cmd_full = 'claude --dangerously-skip-permissions'
elif agent == 'agy':
cmd_full = 'agy --dangerously-skip-permissions'
elif agent == 'hermes':
cmd_full = 'hermes'
elif agent == 'cline':
cmd_full = 'cline -i'
server_opt = f"-L {srv} " if srv != 'default' else "" server_opt = f"-L {srv} " if srv != 'default' else ""
entry = { entry = {
'name': name, 'name': name,
@@ -430,7 +444,7 @@ if tmux_confirmed:
entry['tui'] = {'model': '(unknown — capture after first message)', 'provider': 'anthropic', entry['tui'] = {'model': '(unknown — capture after first message)', 'provider': 'anthropic',
'plan': '(unknown)', 'account': '(unknown)', 'version': '(unknown)'} 'plan': '(unknown)', 'account': '(unknown)', 'version': '(unknown)'}
entry['claude_session_id_own'] = None entry['claude_session_id_own'] = None
else: elif agent == 'agy':
entry['child_pid'] = 0 entry['child_pid'] = 0
entry['agy_conversation_id_own'] = None entry['agy_conversation_id_own'] = None
entry['mcp_attachments'] = [ entry['mcp_attachments'] = [
@@ -440,6 +454,12 @@ if tmux_confirmed:
'endpoint': 'https://stitch.googleapis.com/mcp' 'endpoint': 'https://stitch.googleapis.com/mcp'
} }
] ]
elif agent == 'hermes':
entry['child_pid'] = 0
entry['hermes_conversation_id_own'] = None
elif agent == 'cline':
entry['child_pid'] = 0
entry['cline_conversation_id_own'] = None
d.setdefault('tmux_sessions', []).append(entry) d.setdefault('tmux_sessions', []).append(entry)
yaml_session_names.add(name) yaml_session_names.add(name)
drifts.append({'class': 'B', 'name': name, drifts.append({'class': 'B', 'name': name,
@@ -505,6 +525,66 @@ for s in d.get('tmux_sessions', []):
except Exception: except Exception:
pass pass
# === drift C (hermes): hermes 새 session id materialize (per-row own id) ===
for s in d.get('tmux_sessions', []):
if not s.get('name', '').endswith('-creator-hermes'):
continue
if s.get('status') != 'running':
continue
if s.get('hermes_conversation_id_own'):
continue
cwd = (s.get('pane') or {}).get('cwd', '')
if not cwd:
continue
hdb = f"{home}/.hermes/state.db"
if os.path.exists(hdb):
try:
conn = sqlite3.connect(hdb)
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (cwd,)).fetchone()
conn.close()
if r:
cid = r[0]
s['hermes_conversation_id_own'] = cid
drifts.append({'class': 'C', 'name': s['name'], 'msg': f"{s['name']}: conversation id materialized: {cid}"})
actions.append(f"updated conversation id: {cid}")
except Exception:
pass
# === drift C (cline): cline 새 session id materialize (per-row own id) ===
for s in d.get('tmux_sessions', []):
if not s.get('name', '').endswith('-creator-cline'):
continue
if s.get('status') != 'running':
continue
if s.get('cline_conversation_id_own'):
continue
cwd = (s.get('pane') or {}).get('cwd', '')
if not cwd:
continue
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') == cwd or sdata.get('workspace_root') == cwd:
cid = sdata.get('session_id')
if cid:
s['cline_conversation_id_own'] = cid
drifts.append({'class': 'C', 'name': s['name'], 'msg': f"{s['name']}: session id materialized: {cid}"})
actions.append(f"updated session id: {cid}")
break
except Exception:
pass
# === drift D: stale UUID (cache 의 artifact 가 사라짐) — 보고만, 변경 없음 === # === drift D: stale UUID (cache 의 artifact 가 사라짐) — 보고만, 변경 없음 ===
ai = d.get('agent_identities', {}) or {} ai = d.get('agent_identities', {}) or {}
cl = (ai.get('claude') or {}) cl = (ai.get('claude') or {})
@@ -519,6 +599,28 @@ if ag.get('conversation_id'):
if not os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{cid}.db"): if not os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{cid}.db"):
drifts.append({'class': 'D', 'name': '(agy identity cache)', drifts.append({'class': 'D', 'name': '(agy identity cache)',
'msg': f"stale UUID in agent_identities.agy.conversation_id: {cid} (.db missing)"}) 'msg': f"stale UUID in agent_identities.agy.conversation_id: {cid} (.db missing)"})
hr = (ai.get('hermes') or {})
if hr.get('session_id'):
sid = hr['session_id']
hdb = f"{home}/.hermes/state.db"
has_session = False
if os.path.exists(hdb):
try:
conn = sqlite3.connect(hdb)
r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (sid,)).fetchone()
conn.close()
has_session = r is not None
except Exception:
pass
if not has_session:
drifts.append({'class': 'D', 'name': '(hermes identity cache)',
'msg': f"stale UUID in agent_identities.hermes.session_id: {sid} (session missing from db)"})
cn = (ai.get('cline') or {})
if cn.get('session_id'):
sid = cn['session_id']
if not os.path.exists(f"{home}/.cline/data/sessions/{sid}/{sid}.json"):
drifts.append({'class': 'D', 'name': '(cline identity cache)',
'msg': f"stale UUID in agent_identities.cline.session_id: {sid} (session file missing)"})
result = { result = {
'timestamp': now_iso, 'timestamp': now_iso,
@@ -41,6 +41,7 @@ if [ -z "$AGENT" ]; then
*-creator-claude) AGENT=claude ;; *-creator-claude) AGENT=claude ;;
*-creator-agy) AGENT=agy ;; *-creator-agy) AGENT=agy ;;
*-creator-hermes) AGENT=hermes ;; *-creator-hermes) AGENT=hermes ;;
*-creator-cline) AGENT=cline ;;
*) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;; *) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;;
esac esac
fi fi
@@ -51,7 +52,7 @@ NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
PANE_PID=$(tmux list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null | head -1 || true) PANE_PID=$(tmux list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
PANE_PID="${PANE_PID:-}" PANE_PID="${PANE_PID:-}"
CHILD_PID=0 CHILD_PID=0
if { [ "$AGENT" = "agy" ] || [ "$AGENT" = "hermes" ]; } && [ -n "$PANE_PID" ]; then 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=$(pgrep -P "$PANE_PID" -x "$AGENT" 2>/dev/null | head -1 || true)
CHILD_PID="${CHILD_PID:-0}" CHILD_PID="${CHILD_PID:-0}"
fi fi
@@ -144,6 +145,13 @@ elif agent == 'hermes':
cp = os.environ.get('CHILD_PID', '0') cp = os.environ.get('CHILD_PID', '0')
if cp.isdigit() and int(cp) > 0: if cp.isdigit() and int(cp) > 0:
target['child_pid'] = int(cp) target['child_pid'] = int(cp)
elif agent == 'cline':
target['pane']['cmd'] = 'cline'
target['pane']['cmd_full'] = f'cline -i --id {uuid}'
target['cline_conversation_id_own'] = uuid
cp = os.environ.get('CHILD_PID', '0')
if cp.isdigit() and int(cp) > 0:
target['child_pid'] = int(cp)
snap = d.setdefault('snapshot', {}) snap = d.setdefault('snapshot', {})
snap['taken_at'] = now snap['taken_at'] = now
@@ -76,6 +76,7 @@ if [ -z "$AGENT" ]; then
*-creator-claude) AGENT=claude ;; *-creator-claude) AGENT=claude ;;
*-creator-agy) AGENT=agy ;; *-creator-agy) AGENT=agy ;;
*-creator-hermes) AGENT=hermes ;; *-creator-hermes) AGENT=hermes ;;
*-creator-cline) AGENT=cline ;;
*) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;; *) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;;
esac esac
fi fi
@@ -184,6 +185,7 @@ graceful_stop() {
claude) exitkey="/exit" ;; claude) exitkey="/exit" ;;
agy) exitkey="Exit" ;; agy) exitkey="Exit" ;;
hermes) exitkey="/exit" ;; hermes) exitkey="/exit" ;;
cline) exitkey="/exit" ;;
*) exitkey="/exit" ;; *) exitkey="/exit" ;;
esac esac
echo "graceful: send-keys '$exitkey' to $SESSION_NAME" echo "graceful: send-keys '$exitkey' to $SESSION_NAME"
@@ -263,6 +265,8 @@ if captured and not purge:
target['agy_conversation_id_own'] = captured target['agy_conversation_id_own'] = captured
elif agent == 'hermes': elif agent == 'hermes':
target['hermes_conversation_id_own'] = captured target['hermes_conversation_id_own'] = captured
elif agent == 'cline':
target['cline_conversation_id_own'] = captured
target['resumable'] = True target['resumable'] = True
# --purge-conversation: 워크스페이스 격리된 UUID 의 디스크 artifact 만 삭제 (P0-C) # --purge-conversation: 워크스페이스 격리된 UUID 의 디스크 artifact 만 삭제 (P0-C)
@@ -294,15 +298,21 @@ if purge and purge_uuid:
if os.path.exists(hdb): if os.path.exists(hdb):
try: try:
import sqlite3 import sqlite3
conn = sqlite3.connect(hdb) hconn = sqlite3.connect(hdb)
conn.execute("DELETE FROM sessions WHERE id=?", (purge_uuid,)) hconn.execute("DELETE FROM sessions WHERE id=?", (purge_uuid,))
conn.execute("DELETE FROM messages WHERE session_id=?", (purge_uuid,)) hconn.execute("DELETE FROM messages WHERE session_id=?", (purge_uuid,))
conn.commit() hconn.commit()
conn.close() hconn.close()
print(f"purged db records for session: {purge_uuid}", flush=True) print(f"purged db records for session: {purge_uuid}", flush=True)
except Exception as e: except Exception as e:
print(f"WARN: purge hermes db records failed: {e}", flush=True) print(f"WARN: purge hermes db records failed: {e}", flush=True)
target['hermes_conversation_id_own'] = None target['hermes_conversation_id_own'] = None
elif agent == 'cline':
sessions_dir = f"{home}/.cline/data/sessions/{purge_uuid}"
if os.path.isdir(sessions_dir):
shutil.rmtree(sessions_dir)
print(f"purged: {sessions_dir}", flush=True)
target['cline_conversation_id_own'] = None
# agent_identities 는 cache — 이 워크스페이스 것일 때만 비운다 # agent_identities 는 cache — 이 워크스페이스 것일 때만 비운다
ai = (d.get('agent_identities') or {}).get(agent) or {} ai = (d.get('agent_identities') or {}).get(agent) or {}
if ai.get('project_cwd') == ws: if ai.get('project_cwd') == ws:
@@ -317,6 +327,8 @@ if purge and purge_uuid:
ai['conversation_brain_dir'] = None ai['conversation_brain_dir'] = None
elif agent == 'hermes' and ai.get('session_id') == purge_uuid: elif agent == 'hermes' and ai.get('session_id') == purge_uuid:
ai['session_id'] = None ai['session_id'] = None
elif agent == 'cline' and ai.get('session_id') == purge_uuid:
ai['session_id'] = None
elif purge and not purge_uuid: elif purge and not purge_uuid:
print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True) print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True)
+27 -9
View File
@@ -31,11 +31,29 @@
--- ---
## 2. 환경 설정 파일 생성 (.env) ## 2. 워크스페이스 프로비저닝 (단계 0 — 참고용)
에이전트가 이 안내서를 읽기 전에 워크스페이스 설치가 선행되어야 합니다. 이 단계는 이미 개발자(사람)가 설치기를 통해 수행 완료한 상태입니다. 에이전트가 환경 검증 작업을 수행할 때 파일들이 어떤 경로로 복사되고 설치되었는지에 대한 기초 사실(Reference)을 제공하기 위해 여기에 기록해 둡니다.
### 단계 0.1: 설치 스크립트 실행 (이미 기수행됨)
인스톨러를 통해 워크스페이스를 기동하기 위해 사람이 다음 명령을 구동하였습니다:
```bash
curl -fsSL https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh | bash
```
인스톨러는 시스템 의존성 유효성을 검증하고 리포지토리를 불러와 오케스트레이션을 위한 `.agents/` 스켈레톤 및 관련 문서 파일(본 `BOOTSTRAP.ko.md` 포함)을 복사하고 `.env``.venv` 가상환경 설정을 자동으로 수행했습니다.
> [!NOTE]
> **검증 전용 경고**: 본 가이드를 읽고 있는 AI 에이전트는 인스톨러 스크립트를 스스로 **다시 실행해서는 안 됩니다.** 즉시 환경 변수 설정 및 검증 테스트 단계로 넘어가십시오.
---
## 3. 환경 설정 파일 생성 (.env)
메시징 브로커 설정 및 실행 경로를 설정하기 위해 로컬 환경 설정 파일(`.env`)을 생성하고 수정해야 합니다. 메시징 브로커 설정 및 실행 경로를 설정하기 위해 로컬 환경 설정 파일(`.env`)을 생성하고 수정해야 합니다.
### 단계 2.1: 자동 생성 스크립트 실행 ### 단계 3.1: 자동 생성 스크립트 실행
프로젝트 루트에서 제공되는 환경 설정 템플릿 복사 스크립트를 실행합니다. 프로젝트 루트에서 제공되는 환경 설정 템플릿 복사 스크립트를 실행합니다.
```bash ```bash
@@ -46,7 +64,7 @@
./scripts/generate-env.sh --force ./scripts/generate-env.sh --force
``` ```
### 단계 2.2: 환경 변수 수정 및 설정 ### 단계 3.2: 환경 변수 수정 및 설정
생성된 `.env` 파일을 열어 설정을 필요에 따라 구성합니다. 생성된 `.env` 파일을 열어 설정을 필요에 따라 구성합니다.
> [!NOTE] > [!NOTE]
@@ -66,11 +84,11 @@
--- ---
## 3. 의존성 및 가상환경 설정 (Venv Setup) ## 4. 의존성 및 가상환경 설정 (Venv Setup)
오케스트레이션 및 MQTT 메시징을 구동하기 위한 Python 3 의존성을 설정합니다. 오케스트레이션 및 MQTT 메시징을 구동하기 위한 Python 3 의존성을 설정합니다.
### 단계 3.1: Python 가상환경 구축 ### 단계 4.1: Python 가상환경 구축
프로젝트 루트에서 `.venv` 가상환경을 생성하고 활성화합니다. 프로젝트 루트에서 `.venv` 가상환경을 생성하고 활성화합니다.
```bash ```bash
@@ -81,7 +99,7 @@ python3 -m venv .venv
source .venv/bin/activate source .venv/bin/activate
``` ```
### 단계 3.2: 의존성 패키지 설치 ### 단계 4.2: 의존성 패키지 설치
`multi-agent-mux-delegate-job` 디렉터리에 기재된 `requirements.txt` 의존성 목록을 가상환경에 설치합니다. `multi-agent-mux-delegate-job` 디렉터리에 기재된 `requirements.txt` 의존성 목록을 가상환경에 설치합니다.
```bash ```bash
@@ -91,7 +109,7 @@ pip install -r .agents/skills/multi-agent-mux-delegate-job/requirements.txt
--- ---
## 4. 디렉터리 준비 및 보안 감시 가이드 ## 5. 디렉터리 준비 및 보안 감시 가이드
에이전트 제어 상태 및 잡 기록을 위해 로컬 레지스트리 디렉터리가 정상적으로 생성되었는지 확인합니다. 에이전트 제어 상태 및 잡 기록을 위해 로컬 레지스트리 디렉터리가 정상적으로 생성되었는지 확인합니다.
@@ -112,7 +130,7 @@ pip install -r .agents/skills/multi-agent-mux-delegate-job/requirements.txt
--- ---
## 5. 실행 환경 검증 및 부트스트랩 테스트 ## 6. 실행 환경 검증 및 부트스트랩 테스트
환경 구축이 오작동 없이 안전하게 완료되었는지 아래의 체크리스트를 실행해 검증합니다. 환경 구축이 오작동 없이 안전하게 완료되었는지 아래의 체크리스트를 실행해 검증합니다.
@@ -160,7 +178,7 @@ rm -f ".mam/jobs/$JID.json" ".mam/jobs/$JID.lock"
--- ---
## 6. 에이전트 온보딩 가이드 (New Agent Onboarding) ## 7. 에이전트 온보딩 가이드 (New Agent Onboarding)
본 환경 구축을 무사히 마쳤다면, 협업하는 에이전트는 즉시 .agents/ 디렉터리에 있는 **[AGENT.ko.md](.agents/AGENT.ko.md)** 문서를 읽어야 합니다. 본 환경 구축을 무사히 마쳤다면, 협업하는 에이전트는 즉시 .agents/ 디렉터리에 있는 **[AGENT.ko.md](.agents/AGENT.ko.md)** 문서를 읽어야 합니다.
+27 -9
View File
@@ -31,11 +31,29 @@ Before cloning this project into a new environment, you must first understand th
--- ---
## 2. Environment Configuration (.env) ## 2. Workspace Provisioning (Step 0 — Reference Only)
Before any agent can read this guide, the workspace must be provisioned. This step has already been performed by the human developer using the installer. It is documented here strictly as a reference for verifying agents to understand how the assets arrived.
### Step 0.1: Run the Installer One-Liner (Pre-Cloned/Provisioned)
The human developer bootstrapped the workspace by running:
```bash
curl -fsSL https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh | bash
```
The installer verified system dependencies, staged the repository, copied the runtime `.agents/` scaffolding and documentation files (including this `BOOTSTRAP.md`), and set up the default `.env` and `.venv` environments.
> [!NOTE]
> **Verify Only**: AI agents reading this guide must **not** attempt to run the installation script again. Proceed directly to configuration and verification steps.
---
## 3. Environment Configuration (.env)
To set up the messaging broker and execution paths, you must create and modify a local environment configuration file (`.env`). To set up the messaging broker and execution paths, you must create and modify a local environment configuration file (`.env`).
### Step 2.1: Run the Generation Script ### Step 3.1: Run the Generation Script
Run the environment template copy script provided in the project root: Run the environment template copy script provided in the project root:
```bash ```bash
@@ -46,7 +64,7 @@ Run the environment template copy script provided in the project root:
./scripts/generate-env.sh --force ./scripts/generate-env.sh --force
``` ```
### Step 2.2: Modify Environment Variables ### Step 3.2: Modify Environment Variables
Open the generated `.env` file to configure settings as needed. Open the generated `.env` file to configure settings as needed.
> [!NOTE] > [!NOTE]
@@ -66,11 +84,11 @@ Open the generated `.env` file to configure settings as needed.
--- ---
## 3. Dependency and Virtualenv Setup ## 4. Dependency and Virtualenv Setup
Set up the Python 3 dependencies required to run the orchestration and MQTT messaging backplane. Set up the Python 3 dependencies required to run the orchestration and MQTT messaging backplane.
### Step 3.1: Build Python Virtual Environment ### Step 4.1: Build Python Virtual Environment
Create and activate a `.venv` virtual environment in the project root: Create and activate a `.venv` virtual environment in the project root:
```bash ```bash
@@ -81,7 +99,7 @@ python3 -m venv .venv
source .venv/bin/activate source .venv/bin/activate
``` ```
### Step 3.2: Install Dependency Packages ### Step 4.2: Install Dependency Packages
Install the required packages listed in `requirements.txt` under `multi-agent-mux-delegate-job`: Install the required packages listed in `requirements.txt` under `multi-agent-mux-delegate-job`:
```bash ```bash
@@ -91,7 +109,7 @@ pip install -r .agents/skills/multi-agent-mux-delegate-job/requirements.txt
--- ---
## 4. Directory Structure and Security Audit Guide ## 5. Directory Structure and Security Audit Guide
Ensure that the local registry directories required to track agent states and jobs are successfully created: Ensure that the local registry directories required to track agent states and jobs are successfully created:
@@ -112,7 +130,7 @@ Ensure that the local registry directories required to track agent states and jo
--- ---
## 5. Execution Verification and Bootstrap Tests ## 6. Execution Verification and Bootstrap Tests
To verify that the environment has been successfully built without runtime errors, run the following verification checklist. To verify that the environment has been successfully built without runtime errors, run the following verification checklist.
@@ -161,7 +179,7 @@ rm -f ".mam/jobs/$JID.json" ".mam/jobs/$JID.lock"
--- ---
## 6. Onboarding Collaborating Agents (New Agent Onboarding) ## 7. Onboarding Collaborating Agents (New Agent Onboarding)
Once the setup is verified, onboarding agents should immediately read the **[AGENT.md](.agents/AGENT.md)** guidelines in the .agents/ directory. Once the setup is verified, onboarding agents should immediately read the **[AGENT.md](.agents/AGENT.md)** guidelines in the .agents/ directory.
+18
View File
@@ -14,6 +14,24 @@ Modern agentic workflows often suffer from session timeout, lack of process isol
3. **Multi-Agent Mux (MAM):** Combining local file-based locks (fcntl) and an ACID-compliant SQLite WAL database (`.mam/agent-sessions.db`) to manage concurrent job claims and track running agent sessions without drift. 3. **Multi-Agent Mux (MAM):** Combining local file-based locks (fcntl) and an ACID-compliant SQLite WAL database (`.mam/agent-sessions.db`) to manage concurrent job claims and track running agent sessions without drift.
4. **Automated Review & Quality Loop:** Implementing parallel reviewer loops where worker agents must receive a `PASS` rating from various specialized verification agents (e.g., Claude for high-level logic, Hermes for shell syntax/safety) before merging code. 4. **Automated Review & Quality Loop:** Implementing parallel reviewer loops where worker agents must receive a `PASS` rating from various specialized verification agents (e.g., Claude for high-level logic, Hermes for shell syntax/safety) before merging code.
---
## 📦 Installation & Setup
You can bootstrap the Multi-Agent Mux (MAM) framework in any workspace directory with a single command:
```bash
curl -fsSL https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh | bash
```
Alternatively, if you have already cloned the repository locally, run the installer directly:
```bash
bash deploy/install.sh
```
The idempotent installer automatically validates system dependencies (tmux, python3, and PyYAML), creates the python virtual environment (`.venv`), installs dependencies, copies `.env.example` as `.env`, and initializes the `.agents/` scaffolding.
--- ---
## 🛠️ Core Skills & Scaffolding ## 🛠️ Core Skills & Scaffolding
+122
View File
@@ -0,0 +1,122 @@
# Multi-Agent Mux: Skill Features and Architecture
이 문서는 `multi-agent-mux` 워크스페이스 내에 구현된 6개의 개별 스킬 및 공통 라이브러리의 핵심 기능, 상태 머신, CLI 사양, 그리고 상호 연동 방식을 종합 정리한 명세입니다. 스킬 최적화 및 팩토링 작업의 기준서로 사용됩니다.
---
## 1. 아키텍처 개요 (Architecture Overview)
`multi-agent-mux`는 다중 자율 에이전트(Claude, Agy, Cline, Hermes 등)를 격리된 Tmux 세션 환경에서 관리하고 상호 통신할 수 있게 돕는 시스템입니다.
* **중앙 상태 레지스트리**: `.mam/agent-sessions.yaml` 및 동기화된 `.mam/agent-sessions.db` (SQLite3)
* **격리 소켓**: 독립된 tmux 서버 소켓 지정 구동 가능 (예: `multi-agent-mux` 서버)
* **이벤트 버스**: MQTT 프로토콜 기반의 실시간 작업 상태 비동기 관찰 (`multi-agent-mux-delegate-job`)
---
## 2. 공통 라이브러리: `lib.sh` (Common Library)
모든 스킬 스크립트가 로드하여 사용하는 핵심 공유 헬퍼 라이브러리입니다.
* **상태 파일 원자적 덤프 (`atomic_dump_yaml`)**:
* NFS(네트워크 파일 시스템) 감지 시 SQLite `PRAGMA journal_mode=DELETE` 폴백, 로컬 환경에서는 `PRAGMA journal_mode=WAL` 설정.
* 독점 잠금(`BEGIN IMMEDIATE`)을 활성화해 멀티프로세스 환경에서 Read-Modify-Write 데이터 유실(lost update race condition) 방지.
* 트랜잭션 커밋 완료 후 `.bak` 백업 파일 생성 및 임시파일 생성 후 `os.replace` 원자적 대체 기법 적용.
* **에이전트 세션 실재성 판단 (`*_exists` 함수군)**:
* `claude`: 프로젝트 디렉터리 하위 `<uuid>.jsonl` 존재성
* `agy`: `.gemini/antigravity-cli/conversations/<uuid>.db` 존재성
* `hermes`: `~/.hermes/state.db``sessions` 테이블 내 존재성 (SQLite 쿼리 검증)
* `cline`: `.cline/data/sessions/<uuid>/<uuid>.json` 존재성
* **세션 ID 해석 엔진 (`find_workspace_uuid` 분기 구조)**:
* **Tier 1 (YAML 직접 조회)**: YAML 내 기록된 에이전트별 전용 필드(`claude_session_id_own` 등) 조회.
* **Tier 2 (디스크 잔해 스캔)**: 워크스페이스 디렉터리(`cwd` / `workspace_root`)와 매칭되는 디스크 상의 세션 로그 중 가장 최근 수정일(`mtime`) 기준 정렬 후 최신 UUID 반환.
* **Tier 3 (아이덴티티 캐시)**: 레지스트리 상단 `agent_identities` 캐시 데이터 연동.
---
## 3. 스킬별 상세 핵심 기능 (Skill Specifications)
### 3.1. `multi-agent-mux-create` (생성 스킬)
* **용도**: 신규 에이전트 동작용 격리된 Tmux 컨테이너 생성 및 레지스트리 신규 등록.
* **핵심 기능**:
* **사전 기능 검증 (Preflight Check)**:
* `claude`: `claude auth status`를 통한 로그인 상태(`"loggedIn": true`) 검증
* `agy`: `agy models`를 통한 API 연동 정상 상태 검증
* `hermes`: `hermes status`를 통한 연동 상태 검증
* `cline`: `cline history --json` 동작 및 설정 상태 사전 검증
* **Tmux 세션 생성 및 초기화**: 에이전트별 최적화된 화면 크기(`-x 140 -y 40`) 및 작업 디렉터리(`-c`)를 적용해 세션 백그라운드 생성.
* **초기 상태 YAML 등록**: 사용자 필수 지정 역할(`--role`), `status: running`, `pane` 세부정보(인덱스, PID, CWD, CMD_FULL), 시작 명령 및 `mcp_attachments` 기록.
* **역할 불변성 보장**: 에이전트 생성 시 부여된 역할(`role`)은 사후 수정이 불가하며, 임의 변경 시도 시 데이터 검증(`atomic_dump_yaml`) 단계에서 예외 처리되어 방어됨.
### 3.2. `multi-agent-mux-resume` (재개 스킬)
* **용도**: 중지되었거나 유실된 에이전트의 이전 컨텍스트 그대로 Tmux 세션 및 TUI 연결 복원.
* **핵심 기능**:
* **세션 ID 해석 위임**: `lib.sh::find_workspace_uuid`을 구동하여 대상 워크스페이스의 UUID 확인.
* **세션 복원 기동**:
* `claude`: `claude --dangerously-skip-permissions -r <UUID>`
* `agy`: `agy --dangerously-skip-permissions --conversation <UUID>`
* `hermes`: `hermes --resume <UUID>`
* `cline`: `cline -i --id <UUID>`
* **TUI 바이패스 자동화 (Claude)**: 기동 직후 백그라운드에서 `Enter``Down``Enter` 키스트로크를 주입하여 권한 우회 및 복구 확인 대화상자 자동 수락.
* **동기화**: `update_yaml_resumed.sh`를 구동해 상태를 `running`으로 전이하고 기동 시점에 맞춘 하위 자식 PID 갱신 및 기존 종료 메타데이터 제거.
### 3.3. `multi-agent-mux-stop` (종료 스킬)
* **용도**: 세션을 안전하게 정리하고, 상태 및 UUID를 안전하게 저장 및 동기화.
* **핵심 기능**:
* **종료 전 TUI 스냅숏 저장**: `tmux capture-pane`을 수행해 최종 화면 상태를 `last_visible_status_at_termination` 필드에 보존.
* **다단계 Graceful 종료 프로토콜**:
1. TUI 안전 종료 키스트로크 주입 (`/exit` 또는 `Exit`) 후 3초 대기.
2. 생존 시 `tmux kill-session` 전송 및 5초 대기.
3. 최후 수단으로 감지된 자식 PID에 `kill -9` 전송.
* **디스크 소거 (--purge-conversation)**:
* `resumable``false`로 설정하고 상태를 `terminated`로 기록.
* 에이전트별 데이터 경로에 접근해 해당 세션 파일 파쇄.
* `claude`: `<proj-key>/<uuid>.jsonl` 삭제
* `agy`: `conversations/<uuid>.db``brain/<uuid>` 폴더 삭제
* `hermes`: `sessions/session_<uuid>.json` 삭제 및 `state.db` 내 이력 삭제 (내부 독자 커넥션 `hconn` 사용으로 상위 YAML DB 충돌 차단)
* `cline`: `~/.cline/data/sessions/<uuid>` 폴더 소거
### 3.4. `multi-agent-mux-delegate-job` (위임 스킬)
* **용도**: 타 에이전트에게 비동기적으로 작업을 위임하고, MQTT 이벤트로 실행 상태 관찰.
* **핵심 기능**:
* **작업 지시 유형 (Delegation Types)**:
* `direct` (기본값): 단일 타겟 세션 기동 후 작업 전달 및 대기.
* `loop` (협업 루프): 구현자(Worker)의 작업 완료 후 검토자(Reviewer)가 코드 검수를 수행하여 `"PASS"` 의견이 나올 때까지 작업 수정을 자동 반복 지시.
* `discuss` (토론/합의): 두 에이전트 간 공동 토론을 추진하여 최종 기획 및 계획 합의 도출.
* **MQTT 이벤트 규격**: `publish_event.py``job_subscriber.py`를 매핑하여 `started``permission_required``progress``completed`/`error` 상태 전이 추적 및 자동 이중 타임아웃 검사 (전체 실행 예산 3600초 + 120초 유휴 타임아웃).
* **감사 로그 기록**: `.mam/delegate_job_logs/<job_id>/``meta.json`, `status.json` 및 원시 NDJSON 형식의 `events.ndjson`을 영속 기록.
### 3.5. `multi-agent-mux-status` (현황 스킬)
* **용도**: 레지스트리를 읽어와 실행 중인 모든 에이전트의 구동 세션 현황을 즉시 표기.
* **핵심 기능**:
* **읽기 전용 안정성**: DB 수정이나 상태 전이 유발 없이 순수 조회만 수행.
* 실시간 tmux 프로세스 상태 정보와 YAML 간의 이름 매핑 정합성을 검증하여 콘솔에 요약 출력.
### 3.6. `multi-agent-mux-monitor` (화해 스킬)
* **용도**: 운영체제 Tmux 런타임과 YAML 레지스트리 데이터 불일치를 백그라운드 루프로 감지해 자동 화해(Reconciliation) 처리.
* **핵심 기능**:
* **Drift 감지 및 복구 매뉴얼**:
* **Drift A (Crash/죽은 세션)**: YAML 상 `running`이나 실제 tmux 프로세스가 죽은 경우 감지 ➔ 상태를 `terminated`로 격하 조정.
* **Drift B (새 세션 감지)**: YAML에 없으나 tmux 상에 임의로 떠 있는 `*-creator-*` 세션을 레지스트리에 자동 등록 및 자식 PID 정보 갱신.
* **Drift C (실시간 UUID 갱신)**: 새로 시작된 에이전트가 첫 명령을 받아 세션 ID를 생성했을 때, 디스크 상의 세션 로그 중 가장 수정시간이 일치하는 최신 UUID를 찾아 `*_conversation_id_own` 필드에 주입.
* **Drift D (캐시 정합성 점검)**: 레지스트리 및 캐시 상의 세션 UUID가 실제 디스크에 존재하는지 검사하여 소거된 세션을 리포트.
---
## 4. 에이전트 상태 머신 (Agent State Machine)
시스템 전반에 걸쳐 에이전트 세션은 아래 흐름을 따라 전이됩니다.
```mermaid
stateDiagram-v2
[*] --> running : multi-agent-mux-create / Drift B
running --> stopped : multi-agent-mux-stop (default)
running --> terminated : multi-agent-mux-stop (--purge-conversation) / Drift A
stopped --> running : multi-agent-mux-resume
terminated --> [*]
```
## 5. 최적화 및 팩토링 작업 시 주의 사항
1. **원자적 쓰기 무력화 금지**: `lib.sh`에 설정된 `atomic_dump_yaml`은 다중 에이전트 병렬 기동 시 데이터 꼬임을 막는 중추 역할을 합니다. DB 잠금 및 트랜잭션 흐름을 훼손하지 않아야 합니다.
2. **Cline 및 Claude의 TUI 입력 바인딩 유지**: 세션 재개나 중지 시, 각 에이전트가 내부적으로 사용하는 프롬프트 제어 명령어(예: `/exit`, `--id <session>`)의 세세한 차이를 유지해야 예외 없이 동작합니다.
3. **데이터베이스 변수 충돌 주의**: 서브셸 또는 인라인 Python 스크립트 실행 시 전역 SQLite 커넥션(`conn`)의 이름 공간을 절대 오염시키지 마십시오. (예: `stop_session.sh` 버그 재발 방지).