refactor: update multi-agent rules to TMUX & refactor MAM skills scripts

- Update .agents/MULTI_AGENT_RULES.md & .ko.md to align with TMUX concepts
- Add .agents/INSTALL.md guide for MAM setup and workflows
- Clean up legacy markdown reports under .agents/reports/
- Refactor MAM scripts in .agents/skills/ (rename resolve_herdr_session to resolve_herdr_workspace)
- Improve workspace pane splitting/re-use logic in lib.sh
- Reduce paste safety sleep in send_keys_safe and support linking ~/.claude.json
This commit is contained in:
2026-07-22 06:57:32 +09:00
parent e4b1fb3329
commit 36a087af58
49 changed files with 258 additions and 4361 deletions
+123
View File
@@ -0,0 +1,123 @@
# 🛠️ Multi-Agent Mux (MAM) 설치 및 적용 가이드
MAM은 단일 워크스페이스 상에서 복수의 에이전트(Claude, Cline, Agy, Hermes 등)들이 서로의 상태를 오염시키지 않고 협업할 수 있도록 프로세스 격리 및 라이프사이클 관리를 제공하는 프레임워크입니다.
이 가이드는 기존의 다른 프로젝트/레포지토리에 MAM을 신속하게 도입하고 적용하는 절차를 설명합니다.
---
## 1. ⚙️ 사전 요구사항
MAM 스킬 및 스크립트들은 호스트 시스템의 다음 도구들에 의존합니다. 설치 전에 확인해 주세요.
* **herdr**: 에이전트를 백그라운드 격리 Pane/Workspace에서 구동 및 관제하기 위한 프로세스 컨테이너
* **python3**: 세션 레지스트리(YAML/SQLite DB) 파싱 및 유효성 검사 (내장 `sqlite3` 모듈 필수)
* **uuidgen**: 격리 세션 생성 시 고유의 UUID 할당
* **rsync**: 인스톨러(`deploy/install_mam.sh`)가 `.agents/` 오케스트레이터 및 스킬 폴더를 타겟 프로젝트에 복제하는 데 사용 (설치 시 필요)
* **python3-yaml (pyyaml)**: 세션 데이터 YAML 저장 및 로드 의존성 (`pip install pyyaml`)
---
## 2. 🚀 자동 설치 방법
MAM의 자동 설치 스크립트(`deploy/install_mam.sh`)를 사용하여 10초 만에 필요한 규칙과 라이프사이클 툴킷을 타겟 프로젝트에 이식할 수 있습니다. 스크립트는 실행 시 자동으로 시스템의 `tmux`, `python3`, `rsync`, `uuidgen` 및 필수 파이썬 모듈들을 진단합니다.
> [!IMPORTANT]
> **설치 전제조건**: MAM 스킬을 타겟 프로젝트에 설치하려면 **먼저 MAM 레포지토리가 로컬 머신에 clone 되어 있어야 합니다.**
### 설치 스크립트 실행
MAM 레포지토리 루트 디렉토리로 이동한 후 다음 명령어를 실행합니다.
```bash
# 기본 사용법 (타겟 프로젝트 경로 지정)
$ bash deploy/install_mam.sh --target /path/to/your/project
# 만약 이미 타겟에 AGENTS.md 가 존재하여 강제로 덮어쓰고 싶다면:
$ bash deploy/install_mam.sh --target /path/to/your/project --force
```
### 설치 스크립트가 수행하는 작업:
1. **의존성 진단**: 시스템에 `tmux`, `python3`, `rsync`, `uuidgen` CLI 바이너리와 파이썬 `pyyaml`/`sqlite3` 모듈이 설치되어 있는지 확인합니다.
2. **규칙 및 스킬 복제**: 오케스트레이션 가이드(`.agents/` 하위 전체)를 타겟 프로젝트 하위로 이식합니다.
3. **지침 전파**: 에이전트가 로드하고 복종할 행동 지침 문서(`AGENTS.md`)를 프로젝트 루트에 복사합니다.
4. **형상 제외 설정**: 세션 DB 및 격리 캐시 저장소인 `.mam/` 디렉토리를 타겟 프로젝트의 `.gitignore` 에 자동 주입하여 불필요한 형상 관리를 방지합니다.
---
## 3. 🎯 핵심 사용 워크플로우 (Quick Start)
설치가 완료되면, 타겟 프로젝트 루트에서 에이전트들을 기동 및 관리할 수 있습니다.
### 1) 에이전트 격리 세션 생성 (Create)
새로운 에이전트를 독립된 격리 가상 디렉토리에서 띄웁니다.
```bash
$ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
--workspace "/path/to/your/project" \
--agent claude \
--role developer \
--session my-project-dev-claude \
--isolate \
--herdr-workspace multi-agent-mux
```
* `--isolate` 옵션을 주면 `.mam/agent_homes/<uuid>/` 하위에 로그인 및 설정은 유지하되 대화 내역은 격리되는 홈이 형성됩니다.
### 2) 세션 접속 (Attach)
백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다.
```bash
$ herdr session attach my-project-dev-claude
```
* **화면 탈출**: 대화 중 세션을 유지한 채 터미널로 돌아오려면 `Ctrl + B`를 누른 뒤 `D` 키를 차례로 입력합니다.
### 3) 에이전트 상태 복원 (Resume)
세션이 중지되었거나, 호스트 재기동으로 tmux가 소멸한 경우에도 이전 대화 ID 및 격리 디렉토리를 원자적으로 이어받아 다시 기동할 수 있습니다.
```bash
# 1단계: 복원 대상 세션의 UUID 자동 조회 (DB/YAML 레지스트리 기반)
$ WORKSPACE="/path/to/your/project"
$ AGENT="claude"
$ SESSION_NAME="my-project-dev-claude"
$ UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh \
--workspace "$WORKSPACE" --agent "$AGENT" --session "$SESSION_NAME")
# 복원 대상 세션의 유효성 검사 (M-1)
$ [ -n "$UUID" ] || { echo "[ERROR] 매칭되는 활성 세션 이력이 없습니다. create_session.sh를 통해 먼저 세션을 생성해 주세요."; exit 1; }
# 2단계: 세션 재기동 (이전 대화 컨텍스트 복원 기동)
# (주의: 만약 create 시 격리(--isolate) 세션으로 생성했다면, CLAUDE_CONFIG_DIR 환경변수를 YAML에 기록된 isolation.root 경로로 지정하여 띄워야 합니다. 상세 격리 복원 커맨드는 .agents/skills/multi-agent-mux-resume/SKILL.md 문서를 필독해 주세요.)
$ herdr run -d --name "$SESSION_NAME" --workspace "$WORKSPACE" -- \
"claude --dangerously-skip-permissions -r $UUID"
# 3단계: 레지스트리 세션 상태를 running 으로 동기화 갱신
$ bash .agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh \
--session "$SESSION_NAME" --uuid "$UUID" --agent "$AGENT"
```
### 4) 세션 종료 및 정리 (Stop / Purge)
세션을 정지시키고 대화 컨텍스트를 동결하거나(default), 완전히 소멸시킵니다(`--purge-conversation`).
```bash
# 대화 메타데이터를 백업 및 영속화하고, 안전하게 종료 (status=stopped)
$ bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
--session my-project-dev-claude --agent claude
# 대화 내용 및 격리 홈 디렉토리를 완전히 청소하고 종료 (status=terminated, resumable=false)
$ bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
--session my-project-dev-claude --agent claude --purge-conversation --yes
```
### 5) 자율 반복 정제 루프 기동 (Mux-Loop)
계획 수립(Planner) ➜ 코드 수정(Creator) ➜ 교차 검증(Reviewer) ➜ 수정 정제 피드백을 단일 명령으로 자동 순환하는 반복 정밀 관제 루프를 기동합니다.
```bash
# 플래너 협력 계획 단계를 활성화하고, 리뷰어의 PASS 합의 하에 자율 루프 구동 (최대 3회 교정)
$ bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \
--plan \
--plan-talk 1 \
--reviewer "reviewer-a,reviewer-b" \
--max-loop 3 \
--target-agent my-project-dev-claude \
--task "구현할 명확한 개발 작업 목표"
```
* `--max-loop`는 코드 오류 발견 시 최대 교정(반복 수정) 횟수 제한 가드레일 역할을 합니다.
* **참고**: 리뷰 단계에서 코드 변경분을 정확하게 추적하기 위해, 타겟 프로젝트 디렉토리는 `git` 저장소로 기동 및 관리되고 있는 것을 권장합니다.
---
## 🛡️ 협업 및 보안 가이드라인
* MAM을 사용할 때 모든 에이전트(개발자, 리뷰어)들은 루트의 `AGENTS.md` 지침을 우선 숙지하도록 설계해야 오탐과 무분별한 리팩토링 범람을 방지할 수 있습니다.
* 각 에이전트 역할별로 리뷰 프로세스를 돌릴 시, 최종 승인 결과 보고서(.md)는 형상 관리가 추적할 수 있도록 버전 관리 대상 경로(구체적으로 `.agents/reports/<session_name>/` 또는 `docs/reports/` 등) 하위로 이관 복사하여 커밋하는 규약(`.agents/MULTI_AGENT_RULES.md`)을 준수해 주세요.
+7 -7
View File
@@ -1,6 +1,6 @@
# MULTI_AGENT_RULES.md
본 문서는 새로운 프로젝트에 **MQTT 메시징 백플레인 및 Herdr 기반 멀티 에이전트 오케스트레이션 워크플로우**를 도입하고, 협업하는 에이전트들이 일관된 규칙과 아키텍처에 따라 안전하고 견고하게 작업을 수행할 수 있도록 정의한 공통 지침 및 규약입니다.
본 문서는 새로운 프로젝트에 **MQTT 메시징 백플레인 및 Tmux 기반 멀티 에이전트 오케스트레이션 워크플로우**를 도입하고, 협업하는 에이전트들이 일관된 규칙과 아키텍처에 따라 안전하고 견고하게 작업을 수행할 수 있도록 정의한 공통 지침 및 규약입니다.
새로운 프로젝트에서 작업하는 모든 에이전트는 작업을 시작하기 전 이 문서를 반드시 정독하고 규약을 준수해야 합니다.
@@ -54,7 +54,7 @@
### 🗃️ 레지스트리 및 상태 관리
- 본 아키텍처는 목적에 따라 두 가지 레지스트리를 분리하여 운영합니다:
- **잡 레지스트리 (Job Registry)**: 각 비동기 잡의 메타데이터와 생명주기는 개별 JSON 파일(`.mam/jobs/<id>.json`)로 기록되며, 다중 세션 간의 동시 청구(claiming) 경합은 파일 단위의 `fcntl` advisory lock(`registry_lock` via `registry.py`)을 통해 방어합니다.
- **세션 레지스트리 (Session Registry)**: Herdr 모니터링 상태 및 에이전트 구동 정보는 SQLite WAL 데이터베이스(`.mam/agent-sessions.db`)를 통해 단일 호스트 내에서 안정적인 동시 트랜잭션으로 일관되게 제어합니다. 단, SQLite WAL 모드는 NFS(네트워크 파일 시스템) 환경에서는 완전한 파일 락이 보장되지 않으므로 로컬 파일 시스템 사용을 권장합니다.
- **세션 레지스트리 (Session Registry)**: TMUX 모니터링 상태 및 에이전트 구동 정보는 SQLite WAL 데이터베이스(`.mam/agent-sessions.db`)를 통해 단일 호스트 내에서 안정적인 동시 트랜잭션으로 일관되게 제어합니다. 단, SQLite WAL 모드는 NFS(네트워크 파일 시스템) 환경에서는 완전한 파일 락이 보장되지 않으므로 로컬 파일 시스템 사용을 권장합니다.
### 🛡️ 보안 프로토콜 (HMAC-SHA256)
- **무인증 PoC 모드**: 잡 레지스트리 생성 시 `auth_token``null`로 지정된 경우(PoC 기본 모드), 별도의 서명 검증을 생략하고 모든 이벤트를 수용합니다 (`verify_hmac`이 항상 `True`를 반환).
@@ -113,22 +113,22 @@ sequenceDiagram
장기 실행 에이전트 분석 중 발생하는 유실 및 인프라적 장애를 예방하기 위한 중요 지침입니다.
### 📸 TUI 뷰포트 절단 방지 (Pane Snapshotting 3대 규칙)
Herdr 환경에서 실행되는 에이전트가 화면 스크롤 한계로 인해 이전 출력이나 장문의 디버깅 로그를 잃지 않도록 아래의 **스냅샷 패턴을 의무적으로 수행**합니다.
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 상태를 마지막으로 캡처하여 전체 작업 궤적을 보존합니다.
### 📄 마크다운 기반 협업 및 결과 전달 (Markdown-Based Workflow & Communication)
- **핵심 원칙**: herdr `send-keys`나 입력 버퍼를 통해 긴 지시사항을 직렬로 입력하는 과정에서 문자 누락이나 레이아웃 유실이 발생하는 것을 방지하기 위해, 에이전트 간의 모든 주요 협업 소통은 파일 기반 마크다운 문서 생성을 원칙으로 합니다.
- **핵심 원칙**: TMUX `send-keys`나 입력 버퍼를 통해 긴 지시사항을 직렬로 입력하는 과정에서 문자 누락이나 레이아웃 유실이 발생하는 것을 방지하기 위해, 에이전트 간의 모든 주요 협업 소통은 파일 기반 마크다운 문서 생성을 원칙으로 합니다.
- **세부 규칙 및 규약**:
- **예외 사항**: 1~2줄 내외의 매우 단순한 요청, 상태 확인, 수락 진행 등의 단발성 프롬프트는 herdr 입력을 통해 직접 보낼 수 있습니다.
- **예외 사항**: 1~2줄 내외의 매우 단순한 요청, 상태 확인, 수락 진행 등의 단발성 프롬프트는 tmux 입력을 통해 직접 보낼 수 있습니다.
- **작업 위임**:
- *수동 경로*: 상세 사양과 계획 수립 등의 복잡한 작업 지시는 먼저 로컬 마크다운 파일(예: `.mam/reports/brief-<job_id>.md` 또는 지정된 워크스페이스 경로)로 작성한 후, 에이전트에게 `"Read <파일경로> and execute."` 라는 실행 명령만 전달하십시오.
- *자동 경로*: 자동화 잡 런너(`multi-agent-mux-delegate-job submit`)는 잡 등록 시 `.mam/jobs/<job_id>/brief.md` 디렉터리에 지시서를 자동 집필하고 단일 라인 포인터 프롬프트만 에이전트 세션에 인가합니다.
- **결과 안내 및 피드백**:
- *수동/영구 리뷰*: 상세 리뷰 결과, 설계 제안서 등은 `.mam/reports/<herdr_session_name>/report-<job_id>.md` 경로에 저장합니다.
- *수동/영구 리뷰*: 상세 리뷰 결과, 설계 제안서 등은 `.mam/reports/<tmux_session_name>/report-<job_id>.md` 경로에 저장합니다.
- *자동화 잡 보고서*: 자동 위임된 비동기 작업의 완료 결과는 잡 디렉터리 하위인 `.mam/jobs/<job_id>/<agent_name>-reports/report-final.md` (루프/Discuss 위임 시에는 `<clean_session_name>-reports/`) 경로에 기록해야 합니다.
- *버전 관리 이관*: 버전 관리가 필요한 주요 산출물(최종 설계 계획, 최종 리뷰 보고서, 보안 감사 리포트 등)은 gitignore 대상인 `.mam/` 하위가 아닌, 버전 관리 대상 경로(구체적으로 `.agents/reports/<herdr_session_name>/` 또는 `docs/reports/` 등)로 명시적으로 복사하여 이관 보존해야 합니다.
- *버전 관리 이관*: 버전 관리가 필요한 주요 산출물(최종 설계 계획, 최종 리뷰 보고서, 보안 감사 리포트 등)은 gitignore 대상인 `.mam/` 하위가 아닌, 버전 관리 대상 경로(구체적으로 `.agents/reports/<tmux_session_name>/` 또는 `docs/reports/` 등)로 명시적으로 복사하여 이관 보존해야 합니다.
- **디스크 정리 및 보존 정책 계약 (Cleanup & Retention)**: `.mam/jobs/<job_id>/``.mam/reports/` 폴더 아래의 파일들은 휘발성 감사 이력(audit-trail) 산출물입니다. 버전 관리가 필요한 문서들은 `.agents/reports/` 하위로 수동 복사하여 커밋해야 하며, `stop_session.sh` 세션 종료 스크립트는 이들 보고서 디렉터리를 자동으로 삭제하지 않으므로 수동 또는 주기적 클린업이 권장됩니다.
### ⏱️ 타임아웃 구성 및 정렬 규칙
+6 -6
View File
@@ -1,6 +1,6 @@
# MULTI_AGENT_RULES.md
This document serves as the common guidelines and protocol for introducing the **MQTT messaging backplane and Herdr-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.
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.
@@ -54,7 +54,7 @@ Asynchronous communication and state management between agents are controlled vi
### 🗃️ 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/<id>.json`). Concurrency conflicts (claiming races) across multiple sessions are prevented via file-based `fcntl` advisory locks (`registry_lock` via `registry.py`).
- **Session Registry**: Herdr 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.
- **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`).
@@ -113,7 +113,7 @@ sequenceDiagram
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 Herdr environments do not lose debug logs or previous outputs due to screen scrollback limits, the following **snapshotting pattern must be enforced**:
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.
@@ -121,14 +121,14 @@ To ensure that agents running in Herdr environments do not lose debug logs or pr
### 📄 Markdown-Based Workflow & Communication (마크다운 기반 협업 및 결과 전달)
- **Core Principle**: To prevent TUI character loss, truncation, and layout breakage during sequential input typing, all collaborative workflows must favor file-based markdown communication.
- **Rules & Protocols**:
- **Exception**: Extremely simple prompts (e.g., "Re-evaluate", "Check status", "Proceed") of 1 or 2 lines may be sent directly via herdr input buffers.
- **Exception**: Extremely simple prompts (e.g., "Re-evaluate", "Check status", "Proceed") of 1 or 2 lines may be sent directly via tmux input buffers.
- **Task Delegation**:
- *Manual path*: Detailed task briefs may be written to a local Markdown file (e.g., `.mam/reports/brief-<job_id>.md` or a workspace path) first. The sender then issues a simple trigger command: `"Read <file_path> and execute."`
- *Automated path*: The automated job runner (`multi-agent-mux-delegate-job submit`) automatically provisions the brief at `.mam/jobs/<job_id>/brief.md` and sends a short pointer instruction to the agent.
- **Result Reporting & Feedback**:
- *Manual/Durable reviews*: Detailed reviews, design proposals, or audit reports must be saved under `.mam/reports/<herdr_session_name>/report-<job_id>.md`.
- *Manual/Durable reviews*: Detailed reviews, design proposals, or audit reports must be saved under `.mam/reports/<tmux_session_name>/report-<job_id>.md`.
- *Automated job reports*: Automated execution results are saved directly to `.mam/jobs/<job_id>/<agent_name>-reports/report-final.md` (or `<clean_session_name>-reports/` for loops) as transient files.
- *Versioned promotions*: Any final design plans, review verdicts, or security audit reports that require version control must be explicitly copied to tracked directory paths (specifically under `.agents/reports/<herdr_session_name>/` or `docs/reports/`).
- *Versioned promotions*: Any final design plans, review verdicts, or security audit reports that require version control must be explicitly copied to tracked directory paths (specifically under `.agents/reports/<tmux_session_name>/` or `docs/reports/`).
- **Cleanup & Retention Contract**: Files under `.mam/jobs/<job_id>/` and `.mam/reports/` are transient audit-trail artifacts. While durable outcomes are committed to version control under `.agents/reports/`, ephemeral directory trees can be cleaned up manually as needed; `stop_session.sh` does not automatically purge these report trees during session exit.
### ⏱️ Timeout Configuration & Alignment Rules
@@ -1,56 +0,0 @@
# 🔍 리뷰 리포트 — 세션 ID 격리 종합 설계/구현 계획서 검토
- **리뷰어**: Claude (Reviewer A — 상위 논리/설계 정합성)
- **검토 대상**: `session_isolation_discussion.md` (Planner 통합본, Rev.2 반영)
- **대조 기준**: 본인 작성 `implementation_plan.session_isolation.md` (Rev.2) + 2차 토론 합의
- **job_id**: 655fb695
- **날짜**: 2026-07-10
- **최종 판정**: **PASS** (경미한 비차단 권고 3건 동반)
---
## 1. 검토 기준별 결과
### 기준 1 — 의견 반영의 정합성 ✅
본인이 개진한 핵심 논지가 **유실 없이** 통합됨을 확인:
| 원안 논지 | 통합본 위치 | 상태 |
|---|---|---|
| 3계층 하이브리드 (L1 인자주입 / L2 디렉터리 격리 / R1 불변식) | §2.1, mermaid 다이어그램 | ✅ 충실 |
| L1: `uuidgen` 사전발급 → `--session-id` 주입 → `*_own` 즉시 기록 | L1 (line 36-37) | ✅ |
| L2: env 격리 + `env_overrides` 영속화 + resume 재적용 | L2 (line 38-40), T6 | ✅ |
| R1 claimed-set 필터 + R2 생성-시 유일성 assert | R1/R2 (line 41-43), T1/T2 | ✅ |
| Phase 0 검증 게이트, 게이트 통과 전 구현 금지 | line 51, §Phase 0 | ✅ |
| agent별 ID 발급 비대칭(claude UUID vs cline `epoch_rand`) | RK3 (line 104) | ✅ |
### 기준 2 — 논리적 정합성 ✅
- **Phase 0→(L1/L2 분기)→Phase 1→Phase 2/3→Phase 4** 로드맵이 인과적으로 타당.
- **Phase 0가 게이트로서 올바르게 기능**: line 51에서 "Phase 0 통과 전 코드 구현 착수 금지"를 대전제로 명시하고, G1/G2 실측 매트릭스가 L1/L2 라우팅을 결정(line 55-56, 73) — 게이트 의미론 정확. cline 미지원 리스크가 코드 착수 전에 해소되는 구조라 사이드이펙트 예방 설계가 성립.
- Phase 1(R1/R2)이 전략 무관 선행 가능하다는 원안 취지도 반영(line 58, "선행 가능").
### 기준 3 — 누락 확인 ✅
- **RC-2 청소 계약**: §2.1 RC-2(line 44-45) + T7(line 88) + RK4(line 105)로 3중 기입 — 누락 없음.
- **R1/R2 이중 안전장치**: line 41-43에 defense-in-depth로 명확히 기입.
- Phase별 DoD, Phase 4 회귀검증(V1~V3) 모두 존재.
---
## 2. 🟡 비차단 권고 (구현 계획 세련화용, PASS 조건 아님)
- **A-1. Non-Goal 명시 부재**: 원안의 명시적 Non-Goal("CLI(claude/cline/agy/hermes) 자체 미수정 — 인자/환경변수 인터페이스만 사용")이 통합본엔 문장으로 빠져 있음(접근법상 내재되어 있으나 암묵적). 오해 방지 위해 1줄 명문화 권고.
- **A-2. RK5 `--isolate-strict` 신규 요소**: 원안엔 없던 "격리 활성화를 세션 다중성/명시 플래그로 제어"(line 106)가 추가됨 — 합리적 개선이나 **새 설계 결정**이므로 Phase 0/1 계획 시 스코프로 확정 필요(플래그 기본값·발동 조건).
- **A-3. Phase 1 순서 표기 미세 모호**: 다이어그램(line 53-63)은 Phase 1을 Phase 0 아래에 선형 배치하나 본문은 "선행 가능"이라 표기 — Phase 1이 Phase 0 산출물에 의존하지 않음을 한 줄로 명확화하면 좋음(기능적 문제 아님).
---
## 3. 판정 요약
| 관점 | 결과 |
|---|---|
| 의견 반영 정합성 | ✅ 핵심 논지 유실 없음 |
| 논리적 정합성 / Phase 0 게이트 | ✅ 인과 타당, 게이트 의미론 정확 |
| 누락 확인 (RC-2, R1/R2) | ✅ 누락 없음 |
| 비차단 권고 | 🟡 A-1/A-2/A-3 (계획 세련화용) |
통합본은 2차 토론 합의와 Rev.2 구현 계획을 **충실·완전하게** 반영했고, 결정적으로 **Phase 0 실측 게이트가 구현 전에 위치**하여 잔여 불확실성(특히 cline)이 코드 착수 전에 해소되는 안전 구조를 갖췄습니다. 구현 계획으로 전환하는 데 이견 없습니다. A-1~A-3는 Phase 0 착수 시 함께 반영 권고.
**PASS**
@@ -1,75 +0,0 @@
# Root Markdown Analysis — Cross-Check Report (Creator Claude)
- **Reviewer**: Creator Claude (`canary-projects-multi-agent-mux-creator-claude`)
- **Date**: 2026-07-11
- **Brief**: `.mam/reports/brief-root-markdowns.md`
- **Cross-checked against**: `.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-markdown-analysis.md` (Reviewer Cline, 2026-07-11)
- **Method**: independent read of all 7 files + `git log --follow` per file + repo-wide inbound-link grep + verification of implementation claims against shipped commits.
---
## Verdict Summary
Cline's verdicts (3 DELETE / 4 KEEP) are **confirmed in substance**, with **one amendment**: `session_isolation_discussion.md` cannot be deleted standalone without breaking two inbound links in live tracked docs (see §6).
| # | File | My Verdict | Agrees with Cline? |
|---|------|-----------|--------------------|
| 1 | `task.md` | **DELETE** | ✅ |
| 2 | `implementation_plan.md` | **DELETE** | ✅ |
| 3 | `BOOTSTRAP.md` | **KEEP** | ✅ |
| 4 | `FUTURE_WORKS.ko.md` | **KEEP** | ✅ |
| 5 | `DONE.md` | **KEEP** | ✅ |
| 6 | `session_isolation_discussion.md` | **DELETE — with link cleanup** | ⚠️ amended |
| 7 | `AGENTS.md` | **KEEP** | ✅ |
---
## Per-File Analysis
### 1. `task.md` — DELETE
- **Purpose**: Developer checklist (Rev.1) for the "deploy URL parameterization" task (`MAM_REPO_URL` / `MAM_ARCHIVE_URL` / `MAM_INSTALLER_URL`).
- **Status**: The work **shipped in commit `6408f4a`** (2026-07-09, `feat(deploy): parameterize distribution URLs via MAM_*_URL env vars`) touching exactly the four files the plan prescribed (`deploy/install.sh`, `deploy/update.sh`, `.env.example`, `deploy/README.md`). Independently verified: all three `${MAM_*_URL:-…}` patterns exist at the planned locations (`install.sh:57-58`, `update.sh:139`) and both docs carry the variables. Yet every checkbox in the file is still `[ ]`, and the file ends with a stray accidental-paste line (`agy --conversation=20cc2d8e-…`). Note the file was only ever committed once — bundled into the unrelated isolation-docs commit `d76e470`.
- **Justification**: Fully superseded by the shipped commit; retaining an all-unchecked checklist for done work actively misleads future agents. `task.md`/`implementation_plan.md` are per-cycle scratch names per `.agents/multi_agent_workflow.md` — the *convention* survives deletion of this instance.
### 2. `implementation_plan.md` — DELETE
- **Purpose**: Planner design doc (Rev.1) for the same deploy URL parameterization task; still marked "Draft (사용자 승인 대기)".
- **Status**: Same as above — implemented byte-for-byte in `6408f4a` (default values preserved, `.env` non-sourcing decision honored, mirror examples added to `deploy/README.md:35-40`).
- **Justification**: Superseded by shipped code. The only inbound link is from `task.md`, which is deleted in the same set. Design rationale worth preserving is already encoded in the commit message, `.env.example` comments, and `deploy/README.md`.
### 3. `BOOTSTRAP.md` — KEEP
- **Purpose**: Agent-facing setup/verification guide (env config, venv, MQTT handshake test).
- **Status**: Active. Referenced from `README.md:177,186`, and explicitly in the deploy installer's runtime-doc **allowlist** (`deploy/install.sh:131` copies `MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md AGENTS.md`) — deleting it would silently degrade every fresh install. Last substantively updated 2026-07-09.
- **Justification**: Load-bearing runtime asset, not a dev leftover. (Same verdict extends to `BOOTSTRAP.ko.md`.)
### 4. `FUTURE_WORKS.ko.md` — KEEP
- **Purpose**: Korean roadmap of pending improvements (FW-P1~P7, FW-W1~W7, FW-D2~D4 open; FW-D1 resolved).
- **Status**: Active backlog — most items remain unimplemented (e.g., FW-P6 root-marker detection, FW-P7 monitor HMAC hardening). Maintained mirror of `FUTURE_WORKS.md`.
- **Justification**: This is the project's only backlog tracker; deletion loses planned work. (Same verdict for the English `FUTURE_WORKS.md`.)
### 5. `DONE.md` — KEEP
- **Purpose**: Verified completion record for 28 items (FW-01~FW-16, FW-L1~L3, FW-N1~N7, FW-W3) with per-item commits and 3-agent cross-verification results.
- **Status**: Static historical record; explicitly linked from `FUTURE_WORKS.md:4` ("For completed items, see `DONE.md`") and its Korean twin.
- **Justification**: Deleting it dangles the FUTURE_WORKS reference and erases the audit trail mapping FW-IDs to commits. Zero maintenance cost. (Same for `DONE.ko.md`.)
### 6. `session_isolation_discussion.md` — DELETE, **but only with link cleanup** (amendment to Cline)
- **Purpose**: Rev.3 consolidated design discussion for the session-ID isolation feature.
- **Status**: Superseded — the doc itself declares `implementation_plan.session_isolation.md` (Rev.3) the single source of truth, and the feature is fully implemented, integration-tested, and PASSed by both reviewers (commits through `dad99f5`).
- **Amendment**: Cline's report misses that **two live tracked docs still link to it**: `implementation_plan.session_isolation.md:6` (관련 자료) and `task.session_isolation.md:3` (기준 문서). Standalone deletion creates dangling links.
- **Recommendation**: Treat the whole isolation doc set (`session_isolation_discussion.md`, `Problem_Definition.md`, `implementation_plan.session_isolation.md`, `task.session_isolation.md`, `session_isolation_handover.md` — the latter four out of this brief's scope) as one unit: either delete/archive them **together** (the feature is done and PASSed; the durable outcome lives in `.agents/reports/*/report-isolation-review.md` and git history), or if only the discussion doc goes now, remove the two inbound link references in the same commit.
### 7. `AGENTS.md` — KEEP
- **Purpose**: Core behavioral guidelines for all agents; entry pointer to `.agents/MULTI_AGENT_RULES.md`.
- **Status**: Active and essential — copied to target projects by **both** installers (`scripts/install_mam.sh:127,138` and `deploy/install.sh:131`), referenced by README and the orchestration rules.
- **Justification**: Deleting it breaks both install paths and the documented onboarding flow.
---
## Out-of-Scope Observations (for the record)
1. The root holds **18** markdown files; the brief covered 7. The undiscussed siblings share the fates above: `*.ko.md` twins follow their English counterparts; the four other session-isolation docs should be dispositioned as a set with #6.
2. The uncommitted working-tree `.gitignore` change adding `.agents/reports` (flagged in my installer verdict) is still present and would ignore the very reports directory this brief writes into — it should be resolved before committing any deletions.
3. Deletions of tracked files require `git rm` + commit; per standing constraints I have not deleted or committed anything — this report is analysis only.
## Final Verdict
**PASS on Cline's analysis with the §6 amendment**: 3 DELETE / 4 KEEP confirmed; `session_isolation_discussion.md` must be deleted together with cleanup of its two inbound links (or as part of archiving the whole isolation doc set).
@@ -1,175 +0,0 @@
# Prompt-Lock / Input Delivery Failure — Code-Level Analysis (Creator Claude)
- **Analyst**: Creator Claude (`canary-projects-multi-agent-mux-creator-claude`)
- **Date**: 2026-07-11
- **Brief**: `.mam/reports/brief-prompt-lock-fix.md`
- **Roadmap linkage**: this is the concrete design for **FW-W2** ("블라인드 TUI 키 입력 방지를 위한 실행 준비도 검증", FUTURE_WORKS P2) — landing this closes that item.
---
## 1. Symptom → Root Cause → Code Mapping
The reported symptom — instruction text visible in the prompt box but never submitted, or a frozen cursor — is reproducible from the current code through three distinct paths:
| # | Root cause (brief) | Code path that triggers it |
|---|---|---|
| RC-A | Renderer thread bottleneck during heavy output | `inject_instructions()` pastes, sleeps a **fixed 0.5 s**, sends **one blind `C-m`**. If the TUI (Ink/Blessed) is still flushing startup output, the paste lands but the Enter is consumed while the input widget isn't accepting submits → text sits unsubmitted forever. No verification, no retry. |
| RC-B | Permission/trust dialog steals focus | `wait_for_tui_ready()` **classifies dialogs as "ready"** (see §2-B), so injection proceeds while a modal is up: the pasted text is swallowed by the dialog widget and the `C-m` blindly activates whatever dialog button is focused. |
| RC-C | OAuth / list-selection blocks intercept keys | Same as RC-B (no dialog detection anywhere), plus the resume workflow's **unconditional** `Enter/Down/Enter` sequence, which malfunctions in *both* directions (§2-C). |
Downstream damage: when injection silently fails on a delegated job, no `started` event is ever published — the delegator waits until watchdog timeout, and the pane holds a zombie prompt. The failure is invisible because `inject_instructions()` **always returns 0**.
---
## 2. Exact Code Locations (Deliverable 1)
### 2-A. `lib.sh:1088-1100` — `inject_instructions()` — **primary defect**
```bash
$local_tmux set-buffer -b "job_buf_$job_id" "$instructions"
$local_tmux paste-buffer -b "job_buf_$job_id" -t "$sess"
sleep 0.5
$local_tmux send-keys -t "$sess" C-m
```
Sole caller: `create_session.sh:384` (every `--submit-job` / `--onboard` session). Defects: fixed delay instead of readiness evidence; single unverified `C-m`; no dialog check before pasting; no success/failure contract. **All three root causes converge here.**
### 2-B. `lib.sh:1042-1084` — `wait_for_tui_ready()` — defective gate
- The claude readiness regex (`lib.sh:1056`) is `"Anthropic|Assistant|Chat|Dangerously|dangerously|Enter|Welcome|projects"`. The **trust/bypass dialogs themselves contain "Enter" and "Dangerously"**, so an open modal is reported as "✅ ready" and injection fires straight into it (RC-B).
- On timeout it prints a warning and **"Proceeding anyway"** (`lib.sh:1083`) with no failure return — the caller cannot distinguish ready from not-ready.
- "Banner text painted" is the wrong readiness signal; it says nothing about the input box accepting keys (RC-A).
### 2-C. `multi-agent-mux-resume/SKILL.md:150-156` — blind dialog navigation
```bash
sleep 5; tmux send-keys -t "$SESSION_NAME" Enter
sleep 3; tmux send-keys -t "$SESSION_NAME" Down
sleep 0.3; tmux send-keys -t "$SESSION_NAME" Enter
```
Sent **unconditionally** after claude resume. Two failure modes: (a) if no dialog appears, `Down` puts the fresh prompt into history navigation and the second `Enter` can **re-submit a historical prompt** — spurious re-execution; (b) if the dialog appears later than 5 s under load, the keys land in the prompt and the dialog then blocks all subsequent input — the exact lock symptom. Note the asymmetry: the create path has *no* dialog handling while resume has *blind* handling; neither is correct.
### 2-D. `multi-agent-mux-stop/scripts/stop_session.sh:181-199` — `graceful_stop()`
`tmux send-keys -t "$SESSION_NAME" "$exitkey" Enter` (line 192) is blind: with a dialog open, `/exit` is swallowed, the 3 s check fails, and the session escalates to `kill-session`/SIGKILL — losing the agent's own state flush. Mitigated by the fallback chain (severity: low), but it produces avoidable hard-kills.
### 2-E. `multi-agent-mux-create/SKILL.md:214-215` — documented probe
`tmux send-keys -t "$SESSION_NAME" "" Enter` instructs operators to fire a stray Enter as a liveness probe — with a dialog up, this blindly accepts its focused default. Documentation fix.
### 2-F. Checked and NOT vulnerable (per brief scope)
- `multi-agent-mux-resume/scripts/update_yaml_resumed.sh` — pure registry update; contains no `send-keys`/`paste-buffer`. No change needed.
- `scripts/install_mam.sh` — the epilogue only **prints** quick-start commands for a human; it never drives a TUI. No direct vulnerability; its create quick-start simply funnels into site 2-A, which the fix below covers.
- `MULTI_AGENT_RULES.ko.md:122` already mandates file-based briefs over long serialized typing — correct policy, but insufficient: this incident shows even the short `Read <brief> and execute.` line needs guaranteed delivery.
---
## 3. Proposed Prevention Helper (Deliverable 2)
Add to `lib.sh` (next to the existing pane helpers). Three functions; `send_keys_safe` is the public entry point.
```bash
# ---------------------------------------------------------------------------
# Prompt-lock safe delivery (FW-W2). Contract: send_keys_safe returns 0 only
# if the text was verifiably submitted; callers must handle non-zero.
# ---------------------------------------------------------------------------
_sks_tmux() { # server-aware tmux (same rule as inject_instructions)
if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then
tmux -L "$TMUX_SERVER_NAME" "$@"
else
tmux "$@"
fi
}
_pane_capture() { _sks_tmux capture-pane -p -t "$1" 2>/dev/null || echo ""; }
# _pane_quiescent <sess> [tries=20] [interval=0.5]
# Renderer settled = two consecutive identical non-empty captures.
_pane_quiescent() {
local sess="$1" tries="${2:-20}" interval="${3:-0.5}" prev="__none__" cur i
for ((i = 0; i < tries; i++)); do
cur=$(_pane_capture "$sess")
[ -n "$cur" ] && [ "$cur" = "$prev" ] && return 0
prev="$cur"; sleep "$interval"
done
return 1
}
# _pane_dialog_open <sess> — focus-stealing modal signatures (trust /
# permission / OAuth / list-selection). Tokens must NOT appear in normal
# prompt idle screens; keep this list curated per agent TUI release.
_pane_dialog_open() {
_pane_capture "$1" | grep -Eq \
'Do you trust the files|Yes, proceed|No, exit|Approve\b|Allow this|Deny\b|Press Enter to continue|Sign in|browser to authenticate|Use arrow keys|Esc to cancel'
}
# send_keys_safe <sess> <text> [job_id]
# 1. Wait for renderer quiescence (defeats RC-A).
# 2. Refuse to paste while a dialog is open (defeats RC-B/RC-C): wait up to
# SKS_DIALOG_TIMEOUT (default 30 s) for it to clear; if SKS_DIALOG_ESCAPE=1
# send a single Escape and re-check. NEVER a blind Enter — accepting an
# unknown dialog is a policy decision, not a delivery detail.
# 3. Paste via unique buffer; verify the text landed (marker visible in pane).
# 4. Submit C-m; verify submission (marker left the input area); retry the
# C-m up to 3 times with backoff — safe because re-Enter on the same
# unsubmitted text is idempotent.
send_keys_safe() {
local sess="$1" text="$2" job_id="${3:-adhoc}"
local marker deadline
marker=$(printf '%s' "$text" | head -c 200 | tail -c 24) # verification token
_pane_quiescent "$sess" || { echo "send_keys_safe: pane never quiesced ($sess)" >&2; return 1; }
deadline=$(( $(date +%s) + ${SKS_DIALOG_TIMEOUT:-30} ))
while _pane_dialog_open "$sess"; do
if [ "${SKS_DIALOG_ESCAPE:-0}" = "1" ]; then
_sks_tmux send-keys -t "$sess" Escape; sleep 1
fi
[ "$(date +%s)" -ge "$deadline" ] && { echo "send_keys_safe: dialog blocking input ($sess)" >&2; return 2; }
sleep 2
done
_sks_tmux set-buffer -b "sks_$job_id" "$text"
_sks_tmux paste-buffer -b "sks_$job_id" -t "$sess"
_sks_tmux delete-buffer -b "sks_$job_id" 2>/dev/null || true
sleep 0.5
_pane_capture "$sess" | grep -Fq "$marker" || { echo "send_keys_safe: paste not visible ($sess)" >&2; return 3; }
local try
for try in 1 2 3; do
_sks_tmux send-keys -t "$sess" C-m
sleep "$try"
if ! _pane_capture "$sess" | tail -n 5 | grep -Fq "$marker"; then
return 0 # input box cleared → submitted
fi
done
echo "send_keys_safe: Enter not accepted after 3 tries ($sess)" >&2
return 4
}
```
Design decisions worth recording:
- **Quiescence over fixed sleeps**: two identical captures prove the renderer drained its queue — directly addresses the Blessed/Ink bottleneck; a fixed `sleep` can only ever be wrong in one direction or the other.
- **Escape opt-in, never blind Enter/Ctrl+C**: `Escape` cancels dialogs but *also* clears typed prompt text in some TUIs, and `Ctrl+C` can interrupt a running agent turn — so focus restoration is gated behind `SKS_DIALOG_ESCAPE=1` and only fires when a dialog signature is positively detected. Default behavior is to wait and then fail loudly (distinct exit codes 1-4 tell the caller what blocked).
- **Marker-based submit verification**: agent-agnostic — no per-TUI spinner parsing. The last 24 chars of the text must appear after paste and must leave the bottom 5 lines after Enter. Retrying Enter while the marker is still in the input box is idempotent.
- **Distinct non-zero exit codes** let `create_session.sh` publish a precise `error` event instead of leaving a zombie job.
---
## 4. Draft Migration Plan (Deliverable 3)
| Step | Change | Files | Risk |
|---|---|---|---|
| **M1** | Add the three helpers; rewrite `inject_instructions()` body as a thin wrapper over `send_keys_safe` (same signature). Sole caller `create_session.sh:384` inherits the fix with zero call-site change; add return-code check that publishes `error` + lets the still-armed cleanup trap roll the session back. | `lib.sh`, `create_session.sh` | Low — single choke point |
| **M2** | Harden `wait_for_tui_ready`: drop dialog-ambiguous tokens (`Enter`, `Dangerously`, `dangerously`) from the claude regex; treat `_pane_dialog_open` as *not ready*; make timeout `return 1` and let the caller decide (delegated-job path should abort + rollback rather than "proceed anyway"). | `lib.sh` | Low |
| **M3** | Resume workflow: replace the unconditional `Enter/Down/Enter` block with a conditional loop — poll `_pane_dialog_open`; send navigation keys only when a trust-dialog signature is actually present; skip cleanly otherwise. | `multi-agent-mux-resume/SKILL.md` (embedded shell) | Medium — needs scratch-spawn validation |
| **M4** | Stop graceful path: before sending `$exitkey`, run the dialog check (+ optional single Escape); deliver exitkey via `send_keys_safe`; keep the SIGTERM/SIGKILL fallback chain untouched. | `stop_session.sh` | Low |
| **M5** | Docs: fix the stray-Enter probe example (`create/SKILL.md:214-215`) to use `capture-pane` readiness; document `send_keys_safe` in create/delegate-job SKILL.md; mark **FW-W2 resolved** in `FUTURE_WORKS.md` / `.ko.md`. | docs only | None |
**Verification gate (DoD)** — all on a scratch tmux server (`-L sks-test`), never real sessions:
1. **RC-A stress**: mock TUI that floods output for 10 s before reading stdin → `send_keys_safe` must wait, then deliver; old `inject_instructions` demonstrably drops the Enter.
2. **RC-B/C dialog**: mock script printing a trust-dialog signature and swallowing keys → helper must refuse to paste, honor timeout/Escape policy, and return code 2.
3. **E2E regression**: real `create --submit-job` on a scratch workspace → instructions submitted, `started` event observed; normal create/stop/resume unchanged.
4. `bash -n` + `shellcheck` on `lib.sh`, `create_session.sh`, `stop_session.sh`: 0 new findings.
5. Estimated diff: ~70 lines added to `lib.sh`, <15 lines each elsewhere.
---
## 5. Summary
Every injection site in the codebase shares one flaw: **keys are sent on a timer, not on evidence.** The fix is a single evidence-based delivery helper (`send_keys_safe`: quiescence → dialog gate → paste-verify → submit-verify-retry) plus honesty in the readiness gate (`wait_for_tui_ready` must not call a modal dialog "ready" and must be allowed to fail). Migration touches one library, two scripts, and two docs, and closes roadmap item FW-W2.
@@ -1,85 +0,0 @@
# Prompt-Lock Fix — Final Review (Creator Claude)
- **Reviewer**: Creator Claude (`canary-projects-multi-agent-mux-creator-claude`)
- **Brief**: `.mam/reports/brief-rereview-prompt-lock.md`
- **Round 1** (2026-07-11, commit `e613f4a`): ❌ FAIL — full findings preserved in git history (this file as committed in `da895fc`).
- **Round 2** (2026-07-11, commit `da895fc`): ❌ **FAIL — one single-line blocker remains** (F5, new in the fix commit). Everything else is verified fixed.
- **Round 3** (2026-07-11, working tree on top of `da895fc`): ✅ **PASS** — see below.
---
## Round 3 Verdict: ✅ PASS (working-tree state; commit required)
The F5 fix is applied in the working tree of `create_session.sh:387-388` **byte-identical to the prescribed replacement**:
```bash
rc=0
inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID" || rc=$?
```
- Idiom correctness under `set -euo pipefail` was already proven empirically in round 2 (P2: event published with true `rc=4`, EXIT trap fires, script exits 1). The `||` form suppresses `set -e` for the command, so injection failures now reach `delegate_publish_event error` — MS-7's no-zombie-jobs contract holds on the failure path.
- `bash -n` passes; shellcheck `-S warning`: **0 findings**.
- Diff scope verified: the only code change versus `da895fc` is this 4-line block; all other working-tree changes are review reports.
- lib.sh is unchanged since round 2, where the full functional suite passed 5/5 against the committed helpers (T-A3…T-E3: dialog refusal rc=2 / flood rc=1 / happy-path rc=0 / instant banner / one-Enter trust acceptance).
**Conditions attached to this PASS:**
1. The fix is **uncommitted** — it must be committed for the verdict to bind to a ref (suggested: `fix(create): make injection-failure error event survive set -e (|| rc=$?)`). Include the pending review reports (this file, Reviewer Cline's staged modification and new v2 report) per the durable-reports convention.
2. **DoD-5 follow-up** (non-blocking, reaffirmed): capture-validate the dialog signature tokens for agy/hermes/cline in the field; claude tokens match known real CLI text and unmatched tokens now fail loud, not silent.
3. Update FW-W2's resolution commit reference once the fix commit exists.
---
## Round 2 Verdict: ❌ FAIL (NOT PASS) — F5 only
### ✅ F1 (blank-padded viewport windows) — VERIFIED FIXED
`da895fc` applies the prescribed `_pane_tail()` helper verbatim (lib.sh:1117) and rewires all three windowing sites (`_pane_dialog_open`, `send_keys_safe` submit-verify, `handle_startup_dialogs`). Re-ran the full functional suite against the **committed** code on an isolated scratch server (`tmux -L sks-review`):
| Test | Scenario | Expected | Result |
|---|---|---|---|
| T-A3 | dialog mock, `SKS_DIALOG_TIMEOUT=6` | rc=2, zero paste leakage | ✅ rc=2, 0 occurrences in pane |
| T-B3 | perpetually flooding pane | rc=1 (quiescence gate) | ✅ rc=1 |
| T-C3 | happy-path mock prompt TUI | rc=0, line received | ✅ rc=0, `RECEIVED-OK len=41` |
| T-D3 | ready banner on screen | fast return 0 | ✅ rc=0 in 0 s |
| T-E3 | trust dialog then banner | exactly one Enter, ready detected | ✅ rc=0 in 2 s, banner reached |
`bash -n` passes; shellcheck `-S warning` on lib.sh: 0 findings.
### ❌ F5 — NEW BLOCKER: the F3 fix regressed error-event publication under `set -e`
`create_session.sh` runs under `set -euo pipefail` (line 20). The new form (lines 387-392):
```bash
inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID"
rc=$?
if [ "$rc" -ne 0 ]; then
delegate_publish_event "$DELEGATE_JOB_ID" error "instruction injection failed (prompt-lock, rc=$rc)"
```
Under `set -e`, a **bare failing command aborts the script immediately**`rc=$?` and the `delegate_publish_event error` line are never reached. Proven empirically:
```
set -euo pipefail; f(){ return 4; }; trap "echo TRAP-FIRED" EXIT
f; rc=$?; echo "EVENT-PUBLISHED rc=$rc" → output: TRAP-FIRED only, exit 4
rc=0; f || rc=$?; if [ "$rc" -ne 0 ]; ... → output: EVENT-PUBLISHED rc=4, TRAP-FIRED, exit 1
```
Consequence on injection failure: the EXIT trap still rolls back the session and isolation home, but **no terminal `error` event is ever published** — the delegator waits for watchdog timeout. That is precisely the zombie-job outcome MS-7 exists to prevent, so the fix traded round 1's cosmetic `rc=0` misreport (F3) for a functional regression on the same path. Ironically the round-1 code *did* publish the event.
**Required fix (verified above, one line):**
```bash
rc=0
inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID" || rc=$?
if [ "$rc" -ne 0 ]; then
```
Scope confirmed limited to this one site: the delegate-job wrapper uses the `if ! send_keys_safe …` guard form and `stop_session.sh` uses `send_keys_safe … || echo …` — both are `set -e`-safe and report correct rc.
### Remaining non-blocking items
1. **DoD-5 (real-TUI token validation)** — still no recorded capture evidence. Partially mitigated: the claude tokens (`Do you trust the files`, `Yes, proceed`/`No, exit`) match the real Claude Code CLI dialog text, and with fail-loud semantics an unmatched token now degrades to a loud rc≠0 + rollback rather than a silent lock. Recommendation to Planner: accept with a follow-up task to capture-validate agy/hermes/cline dialog text in the field, rather than blocking on it again.
2. **Working-tree hygiene**: `.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-prompt-lock-analysis.md` has an uncommitted modification — a committed audit record edited in place (not by me). Commit or revert it deliberately alongside the F5 fix.
3. FW-W2 stays legitimately marked resolved once F5 lands; update its commit reference then.
---
## Summary for the Planner
The hard problem is solved and proven: dialog gating, quiescence, banner detection, and trust-dialog acceptance all behave correctly on the committed helpers (5/5 functional tests). What remains is a one-line `set -e` idiom fix in `create_session.sh` (`|| rc=$?`) so injection failures publish their terminal error event — the empirical proof and exact replacement are above. Fix that line, decide the stray report edit, and round 3 is a rubber stamp.
@@ -1,163 +0,0 @@
# MAM Skill Optimization Analysis (Creator Claude)
- **Analyst**: Creator Claude (`canary-projects-multi-agent-mux-creator-claude`)
- **Date**: 2026-07-11
- **Brief**: `.mam/reports/brief-skill-optimization-analysis.md`
- **Scope**: all 8 shell entry points under `.agents/skills/` — 3,422 lines total (`lib.sh` 1212, `reconcile.sh` 644, delegate-job wrapper 440, `create_session.sh` 408, `stop_session.sh` 370, `update_yaml_resumed.sh` 164, `status.sh` 140, `resolve_session_id.sh` 44)
- **Audit baseline**: working tree on `da895fc` + the uncommitted prompt-lock F5 fix in `create_session.sh` (reviewed PASS, awaiting commit)
- **Method**: full-tree greps (sleeps, tmux-resolution variants, `|| true`/`2>/dev/null`, BASH_SOURCE, heredocs, eval), shellcheck run, targeted reads of every flagged site.
## Baseline strengths (for calibration)
Uniform `#!/usr/bin/env bash` + `set -euo pipefail` across all 7 executables (lib.sh correctly bare as a sourced library); zero `eval` in any executable script; zero warning-level shellcheck findings beyond 6 pre-existing ones; the worst sleep offenders (resume's blind `sleep 5/3`, inject's `sleep 0.5`+blind C-m) were already eliminated by the FW-W2 `send_keys_safe` work. The findings below are the next tier.
---
## Focus 1 — Inefficient Polling / Sleeps
### S1 (HIGH VALUE) — `stop_session.sh:193,200`: fixed post-kill waits
```bash
tmux send-keys … # graceful exitkey
sleep 3 # ← always pays 3 s
tmux kill-session …
sleep 5 # ← always pays 5 s
```
Every graceful stop pays the full 3 s even when the agent exits in 200 ms, and the kill path always pays 5 s. Worse than slow: on a loaded host an agent needing >3 s to flush **falsely escalates** to SIGTERM. **Proposal**: add to lib.sh —
```bash
# _wait_session_gone <sess> <max_sec> — returns 0 as soon as the session dies
_wait_session_gone() {
local sess="$1" max="${2:-5}" i
for ((i = 0; i < max * 4; i++)); do
_sks_tmux has-session -t "$sess" 2>/dev/null || return 0
sleep 0.25
done
return 1
}
```
Replace `sleep 3` with `_wait_session_gone "$SESSION_NAME" 5` and `sleep 5` with `_wait_session_gone "$SESSION_NAME" 8`. Reactive (typical stop drops from ~8 s to <1 s), *and* more tolerant of slow exits.
### S2 (HIGH VALUE) — delegate-job `:119` and `:205`: `sleep 1` as MQTT handshake
The comment admits the ordering dependency ("MQTT does not queue non-retained messages for absent subscribers"), then guesses: if CONNACK+SUBACK takes >1 s (public broker `broker.hivemq.com` over WAN — entirely realistic), the agent's `started` event is **lost silently** and the job idles to timeout; on a local broker the 1 s ×2 per review-loop iteration is pure waste. **Proposal** (event-driven handshake, also fixes E4): `job_subscriber.py` already logs to `$logf` — have it print a sentinel line (e.g. `SUBSCRIBED <topic>`) from its `on_subscribe` callback (flush immediately), then in the wrapper replace both sleeps with:
```bash
for _ in {1..25}; do grep -q '^SUBSCRIBED ' "$logf" 2>/dev/null && break; sleep 0.2; done
grep -q '^SUBSCRIBED ' "$logf" || { echo "ERROR: subscriber never reached SUBACK (see $logf)" >&2; exit 1; }
```
Removes the race instead of betting on it, and converts a dead-on-arrival subscriber (see E4) into a loud failure.
### S3 (minor) — `lib.sh:1042-1085` `wait_for_tui_ready`
Two `capture-pane` invocations per iteration (`_pane_dialog_open` + its own `content=$(…)`) with a hand-rolled `local_tmux`. Capture once per iteration into a variable and test both predicates on it; use `_pane_capture` (see D4). The 15×1 s poll budget itself is fine.
### S4 (accepted as-is) — `reconcile.sh:273` `sleep "$POLL_INTERVAL"` broker-down fallback loop is by design; see E1 for its real problem (silence, not pacing).
---
## Focus 2 — Helper Duplication & Modularization
### D1 (HIGH VALUE) — four divergent server-aware tmux resolutions
| Site | Form |
|---|---|
| `lib.sh:1104-1110` `_sks_tmux()` | function — **canonical, word-split-safe** |
| `lib.sh:1043-1046` (`wait_for_tui_ready`) | `local_tmux="tmux -L $NAME"` string |
| `create_session.sh:212-215` | same string pattern (file also has a `_tmux` helper used by its trap — two mechanisms in one script) |
| delegate-job `:347-350` | `_tmux="tmux -L $NAME"` string |
The string variants rely on unquoted word-splitting (`$local_tmux send-keys …`) — the exact idiom class shellcheck SC2086 exists for, and each future call site must re-remember the `!= default` rule. **Proposal**: rename/promote `_sks_tmux` to `mam_tmux()` (keep `_sks_tmux` as an alias for compatibility) and replace all three string variants. Mechanical, ~10 lines net deletion.
### D2 — delegate-job wrapper: duplicated subscriber-spawn + instruction template
The register→spawn-subscriber→sleep→build-`instructions` block appears twice (direct path `:113-131`, review-loop path `:199-215`) and the copies have already drifted (log filename schema differs; the review-loop copy carries iteration metadata the direct copy lacks). Extract `_spawn_job_subscriber <job_id> <logf>` and `_job_instruction_block <job_id> <pub_cmd> <task_text>`; combine with S2 so the handshake logic exists exactly once.
### D3 — ready/dialog token lists duplicated inside lib.sh
Claude ready-tokens `'Anthropic|Assistant|Chat|Welcome|projects'` at **both** `lib.sh:1058` (`wait_for_tui_ready`) and `lib.sh:1205` (`handle_startup_dialogs`); trust-dialog tokens split between `_pane_dialog_open` (`:1137-1139`) and `handle_startup_dialogs`' specific greps (`:1199,1201`). Token-list drift between two grep sites is **precisely the class of defect just fixed in the prompt-lock round** — next TUI release, someone updates one list and not the other. **Proposal**: single-source constants near the top of lib.sh —
```bash
_MAM_DIALOG_TOKENS='Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel'
_MAM_READY_TOKENS_CLAUDE='Anthropic|Assistant|Chat|Welcome|projects'
```
plus a `_ready_regex_for <agent>` case-helper so `wait_for_tui_ready`'s per-agent regexes live in one lookup. All grep sites reference the variables.
### D4 — `wait_for_tui_ready` predates its own library's capture helpers
It hand-builds `local_tmux` and calls raw `capture-pane` instead of `_pane_capture`/`_pane_tail`. Folding it onto the helpers (with S3's single-capture-per-iteration) deletes ~8 lines and closes D1's second row for free.
### D5 (micro) — `stop_session.sh:128-129`: two `python3 -c` processes to read two JSON fields from the same `$MAPPED_DATA`. One process printing both (`'…; d=json.load(sys.stdin); print(d.get("cwd",""), d.get("job_id",""), sep="\t")'`) halves the fork cost; or add a tiny `json_get` helper to lib.sh if more call sites appear.
---
## Focus 3 — Portability & POSIX Compliance
Shebang discipline means raw-`sh` execution is not a real exposure; the genuine gaps are three, and two are **already roadmapped** — listed here with confirmations, not double-counted as new:
### P1 (= FW-P1 / FW-D3, confirmed at `lib.sh:254-255`)
```bash
mountpoint="$(df --output=target "$f" 2>/dev/null | tail -1)" || return 1
if mount | grep -q "$mountpoint.*nfs|…"
```
GNU-only `df --output` + Linux `mount` output format. On macOS/BSD, `df` errors → suppressed by `2>/dev/null``_check_is_nfs` silently returns "not NFS" → **the NFS/WAL safety switch is dead exactly where flock is least reliable**. Portable replacement: `mountpoint="$(df -P "$f" 2>/dev/null | awk 'NR==2{print $6}')"` (`df -P` is POSIX) and probe filesystem type via `stat -f -c %T` on Linux / `stat -f %T` on BSD behind a `case "$(uname)"` — or at minimum log a warning when detection is unavailable instead of silently passing.
### P2 (= FW-P5, confirmed at `lib.sh:17`)
`SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"` — under zsh sourcing (documented agent workflow: `source .agents/skills/lib.sh`), `BASH_SOURCE` is empty → `dirname ""``.``SKILL_DIR` silently becomes the caller's cwd, and every relative resolution downstream (`:950`, `:966`) misroutes. **Proposal (fail-loud, 3 lines at the top of lib.sh)**:
```bash
if [ -z "${BASH_SOURCE:-}" ]; then
echo "lib.sh must be sourced from bash (zsh/sh detected)" >&2; return 1 2>/dev/null || exit 1
fi
```
Silent misresolution becomes an immediate, explained failure. (Full zsh support via `${(%):-%N}` is possible but not worth the dual-dialect maintenance.)
### P3 (= FW-P6, confirmed) — depth-hardcoded root resolution
`status.sh:29` (`…/../../../../`), `reconcile.sh:48`, and the `../..` sourcing prologue in all 6 skill scripts. One directory-layout refactor breaks all of them at once. FW-P6's marker-walk (`find_workspace_root()` ascending to `.git`/`.mam`/`.env`, exported once as `WORKSPACE_ROOT`) remains the right fix; the sourcing prologues can stay relative (they express a true structural invariant *within* the skills tree) — it's the **workspace-root** hops that should go through the marker walk.
### P4 (non-issues, verified): fractional `sleep 0.5/0.25` (GNU+BSD+busybox all accept), `head -c`/`tail -c`/`awk NF` (POSIX), no `grep -P`, no `sed -i`, no `readlink -f`, no `eval` — clean.
---
## Focus 4 — Error Handling & Robustness
### E1 (HIGHEST SEVERITY in this audit) — `reconcile.sh:269`: degraded mode is fully silent
```bash
bash "$_self" --once --emit-diff >/dev/null 2>&1 || true
```
This runs *only* in the broker-down fallback — the mode whose entire purpose is "keep reconciling when eventing is gone" — and it discards stdout, stderr, **and** the exit code. If reconciliation itself is failing every cycle (locked DB, missing python module, corrupted YAML), the operator sees a healthy-looking monitor while drift accumulates unboundedly. **Proposal**:
```bash
if ! out=$(bash "$_self" --once --emit-diff 2>&1); then
fails=$((fails + 1))
echo "[$(date -u +%FT%TZ)] poll-reconcile failed ($fails consecutive): ${out##*$'\n'}" >&2
[ "$fails" -ge 5 ] && { echo "FATAL: 5 consecutive reconcile failures — exiting for supervisor restart" >&2; exit 1; }
else
fails=0
fi
```
### E2 — deliberate vs. accidental suppression (survey result)
The 18 `|| true` / 31 `2>/dev/null` sites were individually reviewed. The large majority are **legitimate idempotency guards** (stop's kill-chain probing possibly-absent sessions; `_pane_capture`'s probe semantics) — no action. The accidental class is E1 (above) and E4 (below).
### E3 — `stop_session.sh:128-129`: unguarded JSON parse under `set -e`, no EXIT trap
Malformed registry JSON kills the stop mid-flight with a bare Python traceback; unlike `create_session.sh`, `stop_session.sh` has **no cleanup/context trap**, so the operator gets no indication of what state the stop reached (exitkey sent? captured? row updated?). Cheap fix: wrap the parse (`… || { echo "ERROR: corrupt registry row for '$SESSION_NAME' — run monitor reconcile" >&2; exit 1; }`) and add a minimal `trap 'echo "stop aborted at stage $STAGE" >&2' ERR` with a `STAGE` variable advanced at each phase.
### E4 — delegate-job subscriber spawned fire-and-forget
`"$PY" job_subscriber.py … >"$logf" 2>&1 &` followed only by `sleep 1`: if the subscriber dies instantly (bad `--registry-dir`, missing paho-mqtt in the venv), the wrapper proceeds, the agent publishes into the void, and the job "runs" with zero audit trail until timeout. The S2 SUBACK-sentinel handshake converts this to a loud early failure — one fix, two findings (S2+E4).
### E5 — shellcheck backlog (6 warnings, pre-existing)
`SC2034` ×2 (`ONCE`, `EMIT_DIFF` "unused" in reconcile.sh — likely consumed inside the python heredoc via env; verify and either export or rename with `_` prefix to document intent), `SC2155` ×2, `SC2164` ×2 (`cd` without `|| exit` — real hazard under odd cwd removal). All are ≤2-line fixes; clearing them makes future "0 new findings" review gates strict.
---
## Prioritized Optimization Plan
| # | Item | Files | Effort | Impact |
|---|---|---|---|---|
| 1 | E1 silent degraded loop → logged + bounded failures | reconcile.sh | S | Correctness/observability of the safety net |
| 2 | S2+E4 SUBACK sentinel handshake replaces both `sleep 1` | delegate-job wrapper, job_subscriber.py | M | Eliminates event-loss race on slow brokers; loud subscriber failures |
| 3 | S1 `_wait_session_gone` reactive stop | lib.sh, stop_session.sh | S | ~7 s faster stops; no false SIGTERM escalation |
| 4 | D1+D4 `mam_tmux()` unification (retire 3 string variants) | lib.sh, create_session.sh, delegate-job | S | Single source of truth; kills word-split hazard |
| 5 | D3 token-list constants + `_ready_regex_for` | lib.sh | S | Prevents recurrence of the F1-class drift bug |
| 6 | P2 fail-loud non-bash source guard | lib.sh | S | Converts silent misresolution to instant diagnosis (FW-P5) |
| 7 | E3 stop parse guard + stage trap | stop_session.sh | S | Debuggable partial-stop states |
| 8 | D2 delegate-job block extraction | delegate-job wrapper | M | Stops copy drift (already observable) |
| 9 | P1 POSIX NFS detection (FW-P1/FW-D3) | lib.sh | M | Restores the WAL safety switch on macOS/BSD |
| 10 | P3 marker-walk root resolution (FW-P6) | lib.sh, status.sh, reconcile.sh | M | Layout-refactor resilience |
| 11 | E5 shellcheck backlog + D5 micro | reconcile.sh, lib.sh, stop_session.sh | S | Strict lint gate for future reviews |
Items 17 are low-risk and independently landable; 910 discharge existing roadmap entries (FW-P1/FW-D3/FW-P5/FW-P6 — update FUTURE_WORKS on landing). Every DoD should include the scratch-server functional suite from the prompt-lock review (T-A…T-E) plus `bash -n` + shellcheck zero-new.
## Summary
The tree is in good structural shape — consistent strict-mode headers, no eval, and the recent FW-W2 work already modernized the highest-risk delivery path. The remaining debt clusters into: two **timing bets** that should be handshakes (stop waits, MQTT subscribe), one **silent failure mode** in exactly the code path that exists for resilience (reconcile fallback), and **four copies** of the tmux-server rule plus **two copies** of the TUI token lists — the same drift pattern that caused the last production bug. Eleven changes, mostly small, none speculative.
@@ -1,13 +0,0 @@
# 📑 Code Review Report: Skill Optimization Implementation
- **Reviewer**: Creator Claude (`canary-projects-multi-agent-mux-creator-claude`)
- **Date**: 2026-07-11
- **Reviewed against**: `.mam/reports/brief-rereview-skill-optimization.md`
- **Verdict**: **PASS**
## 🔎 Implementation Review Details
1. **OP-1 (stop_session.sh wait)**: Reactive wait prevents 7 seconds of magic sleeps on shutdown. `|| true` safely shields the caller from `set -e` aborts on slow exits.
2. **OP-2 (delegate-job subscription handshake)**: Sentinel checking loop with `$sub_pid` liveness guard successfully prevents the WAN event loss race.
3. **OP-3 (reconcile.sh wait)**: Dynamic `threading.Event().wait` pacing reduces CPU wake-ups to zero during idle cycles.
4. **OP-4 (mam_tmux dispatcher)**: Infinite recursion successfully resolved via direct execution of `$_REAL_TMUX_PATH`.
5. **OP-6 & OP-7 (lib.sh constants and zsh guard)**: Sourcing guard and token variables pass syntax and safety review.
@@ -1,127 +0,0 @@
# Root Markdown Cleanup Plan (Planner Claude — Final)
- **Planner**: Planner Claude (`canary-projects-multi-agent-mux-planner-claude`)
- **Date**: 2026-07-11
- **Brief**: `.mam/reports/brief-planner-cleanup.md`
- **Inputs consolidated**:
1. Reviewer Cline — `.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-markdown-analysis.md`
2. Creator Claude — `.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-markdown-analysis-crosscheck.md`
- **Executor**: Antigravity
---
## 1. Final Verdict Checklist (Consolidated)
Both reviewers agree on all 7 verdicts. The single discrepancy is *how* to delete #6, not *whether*.
| # | File | Cline | Creator Claude | **Final** |
|---|------|-------|----------------|-----------|
| 1 | `task.md` | DELETE | DELETE | ☑ **DELETE** |
| 2 | `implementation_plan.md` | DELETE | DELETE | ☑ **DELETE** |
| 3 | `BOOTSTRAP.md` | KEEP | KEEP | ☑ **KEEP** |
| 4 | `FUTURE_WORKS.ko.md` | KEEP | KEEP | ☑ **KEEP** |
| 5 | `DONE.md` | KEEP | KEEP | ☑ **KEEP** |
| 6 | `session_isolation_discussion.md` | DELETE (standalone) | DELETE **+ inbound-link cleanup** | ☑ **DELETE + link cleanup** (Creator Claude's amendment adopted) |
| 7 | `AGENTS.md` | KEEP | KEEP | ☑ **KEEP** |
**Discrepancy resolution (#6)**: Creator Claude's cross-check found two live tracked docs still linking to `session_isolation_discussion.md` (`implementation_plan.session_isolation.md:6`, `task.session_isolation.md:3`), which Cline's report missed. A standalone `git rm` would leave dangling links. **Decision: delete the file and surgically remove the two inbound link references in the same commit.** The broader option (archiving the entire session-isolation doc set — `Problem_Definition.md`, `implementation_plan.session_isolation.md`, `task.session_isolation.md`, `session_isolation_handover.md`) is **out of scope** for this plan: those four files were never analyzed under the brief's 7-file scope, so deleting them now would be an unauthorized scope expansion. They are listed in §4 as a recommended follow-up requiring separate GM authorization.
---
## 2. Impact Assessment
Verified by repo-wide grep (`--include='*.md'` plus `deploy/install.sh`, `scripts/install_mam.sh`):
| File to delete | Inbound references | Impact after this plan |
|---|---|---|
| `task.md` | Only from `implementation_plan.md` (deleted in same commit). `.agents/multi_agent_workflow.md` references the *filename convention* for future planning cycles, not this instance. | ✅ None |
| `implementation_plan.md` | Only from `task.md:3` (deleted in same commit). | ✅ None |
| `session_isolation_discussion.md` | **Live**: `implementation_plan.session_isolation.md:6`, `task.session_isolation.md:3`**fixed by T2/T3 edits below**. **Historical** (briefs/reports under `.agents/reports/**`): intentionally left untouched — they are immutable audit records describing a past review of a then-existing file. | ✅ None after T2/T3 |
KEEP-file safety confirmed: `BOOTSTRAP.md` is in the deploy installer's doc allowlist (`deploy/install.sh:131`); `AGENTS.md` is copied by both installers (`deploy/install.sh:131`, `scripts/install_mam.sh:127,138`); `DONE.md` is linked from `FUTURE_WORKS.md:4`; `FUTURE_WORKS.ko.md` is the active backlog mirror. None are touched.
No documentation build system exists in this repo (no mkdocs/sphinx config); link integrity is the only build-type concern.
**Precondition check (resolved)**: the previously flagged uncommitted `.gitignore` change (adding `.agents/reports`) is no longer present — `git diff` is clean. No blocker remains. The only untracked files are the two reviewer reports, which must be committed per the durable-reports convention (`.agents/MULTI_AGENT_RULES.md`).
---
## 3. Execution Instructions (for Antigravity)
Run from the repo root. All steps are non-interactive. **Do not use `rm` — the three files are git-tracked; use `git rm` so the deletion is staged.**
### T0 — Preflight (abort if it fails)
```bash
cd /home/godopu16/PuKi/laa/canary_projects/multi-agent-mux
git diff --quiet && git diff --cached --quiet || { echo "ABORT: dirty tree"; exit 1; }
```
(Untracked files are fine and expected: the two reviewer reports.)
### T1 — Delete the three files
```bash
git rm task.md implementation_plan.md session_isolation_discussion.md
```
### T2 — Remove the inbound link in `implementation_plan.session_isolation.md` (line 6)
Replace the line:
```
- **관련 자료**: [Problem_Definition.md](Problem_Definition.md), [session_isolation_discussion.md](session_isolation_discussion.md)
```
with:
```
- **관련 자료**: [Problem_Definition.md](Problem_Definition.md)
```
### T3 — Remove the inbound link in `task.session_isolation.md` (line 3)
Replace the line:
```
> 기준 문서: [implementation_plan.session_isolation.md](implementation_plan.session_isolation.md) (Rev.3) / [session_isolation_discussion.md](session_isolation_discussion.md)
```
with:
```
> 기준 문서: [implementation_plan.session_isolation.md](implementation_plan.session_isolation.md) (Rev.3)
```
**Surgical constraint (AGENTS.md §3): change only these two lines. No other edits to either file.**
### T4 — Verify no dangling references remain outside the immutable report archive
```bash
grep -rn --include='*.md' 'session_isolation_discussion\|\](task\.md)\|\](implementation_plan\.md)' \
--exclude-dir=.git . | grep -v '^\./\.agents/reports/' | grep -v '^\./\.mam/'
```
**Expected output: empty** (exit code 1). Any hit = stop and report back.
### T5 — Stage the analysis reports and this plan, then commit (single atomic commit)
```bash
git add .agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-markdown-analysis.md \
.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-markdown-analysis-crosscheck.md \
.agents/reports/canary-projects-multi-agent-mux-planner-claude/report-cleanup-plan.md \
implementation_plan.session_isolation.md task.session_isolation.md
git commit -m "chore(docs): remove obsolete root planning docs per 3-agent markdown audit
- Delete task.md / implementation_plan.md (deploy URL parameterization
shipped in 6408f4a; checklists were stale) and
session_isolation_discussion.md (superseded by
implementation_plan.session_isolation.md; feature shipped and PASSed)
- Remove the two inbound links to the deleted discussion doc
- Add reviewer analysis reports and this cleanup plan under .agents/reports/"
```
### T6 — Post-commit sanity
```bash
git status --short # expected: empty
bash -n scripts/install_mam.sh deploy/install.sh # unchanged, but cheap regression guard
```
---
## 4. Out-of-Scope Follow-Ups (require separate GM authorization)
1. **Session-isolation doc set retirement**: `Problem_Definition.md`, `implementation_plan.session_isolation.md`, `task.session_isolation.md`, `session_isolation_handover.md` are also completed-work artifacts. Recommend a follow-up brief to analyze and disposition them as one unit (the durable outcomes already live in `.agents/reports/*/report-isolation-review.md` and git history).
2. **`.ko.md` twins**: verdicts here extend naturally to counterparts (`DONE.ko.md`, `FUTURE_WORKS.md`, `BOOTSTRAP.ko.md`) — all KEEP; no action.
3. The root still holds 18→15 markdown files after this cleanup; a future pass may consider moving design docs to a `docs/` subtree, but that is a layout decision, not cleanup.
---
## Final Authorization
**Plan status: APPROVED for execution** by Antigravity exactly as written in §3. Deviations (non-empty T4 output, preflight failure, edit-line mismatch) must halt execution and be reported back to the Planner.
@@ -1,45 +0,0 @@
# 🏛️ Planner Claude — MAM Installer Final Architecture Re-Review
- **Scope**: Brief `.agents/reports/brief-rereview-all.md` §3 (Planner Claude)
- **Commits under review**: `d7e19fe``66fd1c4``5cb8c39`
- **Date**: 2026-07-11 (supersedes prior NOT PASS revision of this report)
- **Method**: Static diff review + live end-to-end install diagnostics on native host PATH (fresh target, re-run idempotency, pre-existing `AGENTS.md`/`.gitignore`, symlink invocation, shellcheck)
---
## Verdict: ✅ PASS — with one working-tree regression that must NOT be committed
Both blockers from my prior review (B-1 attach inconsistency in `INSTALL.md`, B-2 false `sqlite3` CLI dependency) are resolved and verified live. The architecture now aligns with MAM standards on the version-control axis. However, an **uncommitted `.gitignore` change adding `.agents/reports`** directly contradicts the durable-reports policy ratified in `d7e19fe` and must be reverted before any commit.
---
## ✅ Resolved and verified
| Item | Evidence |
|---|---|
| B-1: RC-1 attach alignment | `INSTALL.md` §3-1 create example now includes `--tmux-server multi-agent-mux`, matching the §3-2 attach socket. Manual flow is now internally consistent (default server in `lib.sh:33` is `default`, so the explicit flag is required and now present). |
| B-2: sqlite3 dependency | `sqlite3` CLI removed from `DEPS`; replaced with hard `python3 -c "import yaml, sqlite3"` check — matching actual runtime usage (all DB access is via Python module in heredocs). **Verified live: install now succeeds on this host's native PATH, which has no `sqlite3` binary.** `INSTALL.md` §1 updated accordingly. |
| `flock` removal (correction) | My earlier review implied `flock` was a CLI dependency; on inspection all locking is Python `fcntl.flock` (`reconcile.sh:76`) — the other grep hits are comments. Removing `flock` from `DEPS` in `66fd1c4` was **correct**. |
| `uuidgen` retained | Genuine CLI dependency (`create_session.sh:125,127`, required for `--isolate`); correctly kept in `DEPS`. |
| Report migration | `git ls-files .mam/` empty; durable reports tracked under `.agents/reports/<session>/`; `MULTI_AGENT_RULES.md` (+`.ko`) and `INSTALL.md` §🛡️ consistent. Anchored rsync `--exclude='/reports/'` verified to keep internal reports out of targets. |
| `AGENTS.md` non-invasive injection | Verified live with pre-existing `AGENTS.md`: content preserved, marker block appended once, rerun is a no-op. Version-control safe. |
| Hygiene | `bash -n` + `shellcheck` clean; symlink invocation resolves `SRC_DIR`; idempotent second run on fresh and pre-populated targets. |
---
## 🚫 Must fix before commit
**Working-tree `.gitignore` adds `.agents/reports`** (uncommitted). This un-does the durable-reports migration for all *future* reports: already-committed files stay tracked, but new mandated artifacts (e.g., the Reviewer Cline `report-mam-installer-final.md` this brief requires, and this very report) would be silently untracked — reintroducing the exact audit-trail loss the migration fixed. It also conflicts verbatim with `MULTI_AGENT_RULES.md` ("must be explicitly copied to tracked directory paths (specifically under `.agents/reports/<tmux_session_name>/`…)"). **Recommendation: revert this hunk.** If the intent was to exclude transient briefs, ignore a narrower pattern (e.g., `.agents/reports/brief-*.md`) — but do not ignore the reports tree itself.
## ⚠️ Minor (non-blocking)
1. `INSTALL.md` §2 step 1 still says the installer checks "`tmux`, `python3`, `sqlite3`" — stale; actual check is `tmux`, `python3`, `rsync`, `uuidgen` + Python `yaml`/`sqlite3` modules.
2. `INSTALL.md` §1 omits `uuidgen` (hard dep for `--isolate`) and presents `rsync` without noting it is installer-only.
3. Source `AGENTS.md` lacks the MAM marker block, so a fresh-copy install converges only on the second run (pointer self-injection). Cosmetic; append the marker at copy time to converge in one run.
4. Still open from planning (roadmap, not gating): `.agents/.mam-version` stamping + `--update --delete` upgrade mode; shared `check_deps.sh` used by both installer and `create_session.sh`; bilingual `INSTALL.md`/`INSTALL.ko.md` split per repo convention.
---
## Architecture alignment summary
With `66fd1c4` and `5cb8c39`, the installer satisfies MAM standards: dependency diagnosis now reflects the true runtime contract, the manual's create/attach flow is consistent, the `.mam/` runtime tree vs. tracked `.agents/` configuration boundary is crisp, and downstream projects' behavioral guidelines are preserved. Deployment across other projects is approved once the `.gitignore` working-tree regression is discarded; the minor doc drift can ride along in a follow-up docs commit.
@@ -1,274 +0,0 @@
# Prompt-Lock Fix Implementation Plan (Planner Claude — Final)
- **Planner**: Planner Claude (`canary-projects-multi-agent-mux-planner-claude`)
- **Date**: 2026-07-11
- **Brief**: `.mam/reports/brief-planner-prompt-lock-plan.md`
- **Inputs consolidated**:
1. Reviewer Cline — `.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-prompt-lock-analysis.md`
2. Creator Claude — `.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-prompt-lock-analysis.md`
- **Executor**: Antigravity
- **Roadmap linkage**: closes **FW-W2** (`FUTURE_WORKS.md:25` / `FUTURE_WORKS.ko.md:24`)
---
## 0. Consolidation Verdict
Both analyses agree on the root causes (keys sent on **timers, not evidence**; no dialog detection; `wait_for_tui_ready` misclassifies dialogs as ready) and on the remedy shape (evidence-based `send_keys_safe` in `lib.sh`). I adopt **Creator Claude's helper design** as the base with four planner amendments (A1A4 below).
**Discrepancy resolved — the delegate-job duplicate.** Cline's Site B is **real and Creator Claude missed it**: `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:365-369` is a raw copy-paste of the `inject_instructions` body (verified in code; Creator's "sole caller is `create_session.sh:384`" is technically true of the *function* but ignores the duplicated *logic*). This is the highest-traffic delegation path and **must** be in the mod-sites list (MS-7). I verified `lib.sh` is side-effect-free at source time (only variable defaults + function definitions), so the delegate-job wrapper can safely `source` it — this also retires the copy-paste that violates lib.sh's single-source-of-truth mandate (header §4.1).
**Planner amendments to Creator's helper:**
- **A1 — marker derivation**: Creator's `head -c 200 | tail -c 24` can straddle a newline in the multi-line instructions built at `create_session.sh:374-381`, producing a marker that can never match a single captured line. Amended: last 24 chars of the **last non-empty line**.
- **A2 — submit verification**: Creator's "marker left `tail -n 5`" alone risks a false *failure* (Claude's TUI echoes the submitted prompt into the transcript just above the input box, so the marker can linger in the bottom 5 lines after a successful submit → spurious retries → exit 4 → spurious rollback). Amended: submitted = marker left the **bottom 3 lines** *AND* the pane visibly changed relative to a pre-Enter snapshot. Line count is DoD-tunable (DoD-6).
- **A3 — dialog detection scope**: grep the **bottom 20 lines** of the pane, not the full viewport — dialog signatures appearing in agent *conversation output* higher up must not block delivery forever.
- **A4 — resume needs an accept-policy helper, not `send_keys_safe`**: `send_keys_safe` deliberately *refuses* to type into dialogs; the resume flow must *accept* the trust/bypass dialogs. That is a separate policy helper, `handle_startup_dialogs` (conditional, signature-gated — replaces the blind `Enter/Down/Enter` both reports condemned).
Non-goal (out of scope, per surgical principle): adding dialog auto-accept to the **create** path — it has never had dialog handling; with MS-2/MS-5 a dialog during create now fails loudly with rollback instead of silently prompt-locking. If field data shows trust dialogs during create, a follow-up can reuse `handle_startup_dialogs`.
---
## 1. Deliverable 1 — Concrete Helper Implementation
**Insert into `.agents/skills/lib.sh` immediately after `inject_instructions` (after current line 1100).** Plain bash, no new dependencies.
```bash
# ---------------------------------------------------------------------------
# Prompt-lock safe delivery (FW-W2). Keys are sent on evidence, not timers.
# send_keys_safe returns 0 only if the text was verifiably submitted.
# Exit codes: 1=pane never quiesced 2=dialog blocking input
# 3=paste not visible 4=Enter not accepted
# Callers MUST handle non-zero.
# ---------------------------------------------------------------------------
# Server-aware tmux (same isolation rule as inject_instructions).
_sks_tmux() {
if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then
tmux -L "$TMUX_SERVER_NAME" "$@"
else
tmux "$@"
fi
}
_pane_capture() { _sks_tmux capture-pane -p -t "$1" 2>/dev/null || echo ""; }
# _pane_quiescent <sess> [tries=20] [interval=0.5]
# Renderer settled = two consecutive identical non-empty captures.
# Defeats RC-A (Blessed/Ink renderer bottleneck) without a magic fixed sleep.
_pane_quiescent() {
local sess="$1" tries="${2:-20}" interval="${3:-0.5}" prev="__none__" cur i
for ((i = 0; i < tries; i++)); do
cur=$(_pane_capture "$sess")
[ -n "$cur" ] && [ "$cur" = "$prev" ] && return 0
prev="$cur"
sleep "$interval"
done
return 1
}
# _pane_dialog_open <sess> — focus-stealing modal signatures (trust /
# permission / OAuth / list-selection), checked in the bottom 20 pane lines
# only (dialogs render near the input area; conversation text above must not
# trigger this). Tokens must NOT appear on normal idle prompt screens —
# validate against real captures per agent TUI release (DoD-5).
_pane_dialog_open() {
_pane_capture "$1" | tail -n 20 | grep -Eq \
'Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel'
}
# send_keys_safe <sess> <text> [job_id]
# 1. Wait for renderer quiescence (RC-A).
# 2. Refuse to paste while a dialog is open (RC-B/RC-C): wait up to
# SKS_DIALOG_TIMEOUT (default 30 s); if SKS_DIALOG_ESCAPE=1, send a single
# Escape per poll and re-check. NEVER a blind Enter — accepting an unknown
# dialog is a policy decision, not a delivery detail.
# 3. Paste via unique buffer; verify the text landed (marker visible).
# 4. Submit C-m; verify submission (marker left the input area AND the pane
# changed); retry up to 3 times — re-Enter on unsubmitted text is idempotent.
send_keys_safe() {
local sess="$1" text="$2" job_id="${3:-adhoc}"
local marker pre_submit deadline try
# Verification token: last 24 chars of the last non-empty line (multi-line safe).
marker=$(printf '%s' "$text" | tr -d '\r' | awk 'NF {line=$0} END {print line}' | tail -c 24)
_pane_quiescent "$sess" || { echo "send_keys_safe: pane never quiesced ($sess)" >&2; return 1; }
deadline=$(( $(date +%s) + ${SKS_DIALOG_TIMEOUT:-30} ))
while _pane_dialog_open "$sess"; do
if [ "${SKS_DIALOG_ESCAPE:-0}" = "1" ]; then
_sks_tmux send-keys -t "$sess" Escape
sleep 1
fi
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "send_keys_safe: dialog blocking input ($sess)" >&2
return 2
fi
sleep 2
done
_sks_tmux set-buffer -b "sks_$job_id" "$text"
_sks_tmux paste-buffer -b "sks_$job_id" -t "$sess"
_sks_tmux delete-buffer -b "sks_$job_id" 2>/dev/null || true
sleep 0.5
_pane_capture "$sess" | grep -Fq "$marker" || { echo "send_keys_safe: paste not visible ($sess)" >&2; return 3; }
for try in 1 2 3; do
pre_submit=$(_pane_capture "$sess")
_sks_tmux send-keys -t "$sess" C-m
sleep "$try"
# Submitted = input area released the text AND rendering changed after Enter.
if ! _pane_capture "$sess" | tail -n 3 | grep -Fq "$marker" \
&& [ "$(_pane_capture "$sess")" != "$pre_submit" ]; then
return 0
fi
done
echo "send_keys_safe: Enter not accepted after 3 tries ($sess)" >&2
return 4
}
# handle_startup_dialogs <sess> [timeout_sec=20]
# Post-start/resume dialog policy for claude: accept the trust / bypass
# dialogs ONLY when their signature is positively on screen; return as soon
# as the TUI banner is ready. Replaces the blind Enter/Down/Enter sequence.
# Non-fatal by design: on timeout the caller proceeds (attach shows leftovers).
handle_startup_dialogs() {
local sess="$1" timeout="${2:-20}" waited=0 pane
while [ "$waited" -lt "$timeout" ]; do
pane=$(_pane_capture "$sess" | tail -n 20)
if printf '%s\n' "$pane" | grep -q 'Do you trust the files'; then
_sks_tmux send-keys -t "$sess" Enter # accept trust prompt
elif printf '%s\n' "$pane" | grep -q 'Yes, proceed'; then
_sks_tmux send-keys -t "$sess" Down # select "Yes, proceed"
sleep 0.3
_sks_tmux send-keys -t "$sess" Enter
elif printf '%s\n' "$pane" | grep -Eq 'Anthropic|Assistant|Chat|Welcome|projects'; then
return 0 # ready, no dialog
fi
sleep 2
waited=$((waited + 2))
done
return 0
}
```
> ⚠️ **Signature-token caveat (both analyses inherited this)**: neither input report captured the *actual* dialog text of current claude/agy/hermes/cline TUI builds. The token lists above are the best available hypotheses. **DoD-5 makes validating them against real `capture-pane` output a merge blocker** — the executor must adjust tokens to observed text before committing.
---
## 2. Deliverable 2 — Surgical Mod-Sites List
Every change traces to a verified defect site. No other lines are touched.
| # | File : lines (current) | Change |
|---|---|---|
| **MS-1** | `.agents/skills/lib.sh` (insert after 1100) | Add the §1 helper block verbatim. |
| **MS-2** | `.agents/skills/lib.sh:1056` | `wait_for_tui_ready` claude regex: `"Anthropic\|Assistant\|Chat\|Dangerously\|dangerously\|Enter\|Welcome\|projects"``"Anthropic\|Assistant\|Chat\|Welcome\|projects"` (drop the three tokens that also match trust/bypass dialogs — RC-B fix). |
| **MS-3** | `.agents/skills/lib.sh:1050-1053` | Inside the retry loop, before the `case`: skip the ready-check while a dialog is up — insert `if _pane_dialog_open "$sess"; then sleep 1; continue; fi` after the capture (requires MS-1 helpers; they are defined later in the file but resolved at call time — bash allows this). |
| **MS-4** | `.agents/skills/lib.sh:1083` | `echo "⚠️ Warning: ... Proceeding anyway..."``echo "⚠️ TUI readiness check timed out for '$sess'." >&2; return 1` — the gate must be allowed to fail. |
| **MS-5** | `.agents/skills/lib.sh:1088-1100` | Rewrite `inject_instructions` body as a thin wrapper: `inject_instructions() { send_keys_safe "$1" "$2" "${3:-onboard}"; }` (same signature; sole caller inherits the fix; keep the function comment, note the delegation). |
| **MS-6** | `.agents/skills/multi-agent-mux-create/scripts/create_session.sh:199` | `wait_for_tui_ready "$SESSION_NAME" "$AGENT"` → explicit guard: `if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then echo "ERROR: agent TUI never became ready — aborting (rollback via trap)" >&2; exit 1; fi` (the `trap cleanup_tmux_on_error EXIT` armed at line 196 kills the session and removes the isolation home). |
| **MS-7** | `.agents/skills/multi-agent-mux-create/scripts/create_session.sh:384` | Guard the injection: `if ! inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID"; then delegate_publish_event "$DELEGATE_JOB_ID" error "instruction injection failed (prompt-lock, rc=$?)"; exit 1; fi` — the job gets a terminal `error` event (no more zombie jobs waiting for watchdog timeout), then the EXIT trap rolls the session back. `started` (line 386) now only publishes on verified delivery. |
| **MS-8** | `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:364-369` | Replace the duplicated raw block (`set-buffer`/`paste-buffer`/`sleep 0.5`/`C-m`/`delete-buffer`) with: `source "$SCRIPT_DIR/../lib.sh"` (immediately before use; lib.sh is load-side-effect-free — verified) then `if ! send_keys_safe "$sess" "$instructions" "$job_id"; then echo "ERROR: 프롬프트 주입 실패 — 세션 '$sess' (프롬프트 잠금 의심)" >&2; return 1; fi`. Keep the local `_tmux` definition (lines 347-350) — still used by `has-session` (352) and the attach hint (371). The `return 1` propagates under `set -euo pipefail` and fires the EXIT trap at 358-362, which publishes the `error` event. |
| **MS-9** | `.agents/skills/multi-agent-mux-resume/SKILL.md:150-156` | Replace the blind block (`sleep 5; Enter; sleep 3; Down; sleep 0.3; Enter`) with: `handle_startup_dialogs "$SESSION_NAME" 20` (the embedded script already sources `lib.sh` at line 68). Fixes both failure directions: no-dialog → no stray keys into the prompt/history; late dialog → polled, not raced. |
| **MS-10** | `.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh:192` | `tmux send-keys -t "$SESSION_NAME" "$exitkey" Enter 2>/dev/null \|\| true``send_keys_safe "$SESSION_NAME" "$exitkey" "stop$$" \|\| echo "graceful: safe delivery failed (rc=$?) — falling back to kill chain"` (script already sources lib.sh at line 32; the SIGTERM→SIGKILL fallback chain at 193-207 stays byte-identical). |
| **MS-11** | `.agents/skills/multi-agent-mux-create/SKILL.md:214-215` | Replace the stray-Enter probe example with a passive one: `tmux capture-pane -t "$SESSION_NAME" -p -S -20 # TUI ready = agent banner visible, no dialog text` (never fire keys as a liveness probe). |
| **MS-12** | `FUTURE_WORKS.md:25`, `FUTURE_WORKS.ko.md:24` | Mark **FW-W2** resolved using the existing FW-D1 strikethrough convention (`~~...~~` + resolution date 2026-07-11 + commit ref), both languages. |
**Explicitly NOT touched** (verified non-vulnerable, matching Creator §2-F): `update_yaml_resumed.sh`, `scripts/install_mam.sh`, `reconcile.sh`, all Python backplane scripts.
---
## 3. Deliverable 3 — Execution Instructions (for Antigravity)
Run from the repo root, in order. Halt and report back on any non-zero step that isn't explicitly tolerated.
### T0 — Preflight
```bash
cd /home/godopu16/PuKi/laa/canary_projects/multi-agent-mux
git diff --quiet && git diff --cached --quiet || { echo "ABORT: dirty tree"; exit 1; }
```
(Untracked files are expected: the two analysis reports and this plan.)
### T1 — Apply MS-1…MS-5 to `lib.sh`
Insert the §1 helper block after line 1100; apply the four edits to `wait_for_tui_ready` / `inject_instructions` exactly as specified in §2. Then:
```bash
bash -n .agents/skills/lib.sh
```
### T2 — Apply MS-6/MS-7 to `create_session.sh`, MS-8 to the delegate-job wrapper, MS-9 to `resume/SKILL.md`, MS-10 to `stop_session.sh`, MS-11 to `create/SKILL.md`
```bash
bash -n .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job
command -v shellcheck >/dev/null && shellcheck -S warning \
.agents/skills/lib.sh \
.agents/skills/multi-agent-mux-create/scripts/create_session.sh \
.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh || true
```
Zero *new* findings allowed (pre-existing findings are out of scope).
### T3 — Verification gate (Definition of Done) — scratch server only, never real sessions
All tmux activity on `tmux -L sks-test`; `tmux -L sks-test kill-server` afterwards.
1. **DoD-1 RC-A (renderer stall)**: mock TUI that floods stdout for 10 s before reading stdin (`while :; do echo spam; done & sleep 10; kill %1; cat`) → `send_keys_safe` must wait out quiescence and deliver (rc 0); confirm by capturing the mock's received line.
2. **DoD-2 RC-B/C (dialog block)**: mock that prints `Do you trust the files in this folder?` and swallows input → `send_keys_safe` must refuse to paste and return **2** after `SKS_DIALOG_TIMEOUT=6`; with `SKS_DIALOG_ESCAPE=1` verify a single Escape per poll, still no blind Enter.
3. **DoD-3 E2E create**: real `create_session.sh --submit-job` on a scratch workspace → instructions verifiably submitted, `started` event observed, normal stop afterwards.
4. **DoD-4 E2E delegate + resume + stop**: delegate-job `submit` to a running scratch session (delivery via the new path); resume a stopped claude scratch session **twice** — once where the trust dialog appears, once where it doesn't — confirm no stray keys land in the prompt in the second case; `--graceful` stop delivers `/exit` via the helper and the fallback chain still engages when the pane is blocked.
5. **DoD-5 signature validation (merge blocker)**: `capture-pane` the real trust/bypass/permission dialogs of the current claude build; confirm every `_pane_dialog_open` and `handle_startup_dialogs` token matches observed text and none appears on the idle prompt screen; adjust tokens to reality before committing.
6. **DoD-6 A2 tuning**: during DoD-3, confirm the submit check (marker leaves bottom-3-lines + pane change) doesn't false-fail on the TUI's transcript echo; tune the `tail -n 3` count if needed and record the final value in the commit message.
### T4 — Commit (single atomic commit)
```bash
git add .agents/skills/lib.sh \
.agents/skills/multi-agent-mux-create/scripts/create_session.sh \
.agents/skills/multi-agent-mux-create/SKILL.md \
.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job \
.agents/skills/multi-agent-mux-resume/SKILL.md \
.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
FUTURE_WORKS.md FUTURE_WORKS.ko.md \
.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-prompt-lock-analysis.md \
.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-prompt-lock-analysis.md \
.agents/reports/canary-projects-multi-agent-mux-planner-claude/report-prompt-lock-plan.md
git commit -m "fix(skills): evidence-based prompt delivery (send_keys_safe) to end prompt-lock (FW-W2)
- Add send_keys_safe + quiescence/dialog-detection helpers to lib.sh:
keys are sent on pane evidence, never fixed timers; distinct exit
codes 1-4; dialogs are never blindly Enter-ed
- inject_instructions delegates to send_keys_safe; create --submit-job
publishes a terminal error event on delivery failure (no zombie jobs)
- wait_for_tui_ready: drop dialog-ambiguous tokens, treat open dialogs
as not-ready, return 1 on timeout instead of proceeding
- delegate-job wrapper: replace copy-pasted raw paste/C-m block with
lib.sh send_keys_safe (restores single source of truth)
- resume: conditional signature-gated dialog handling replaces blind
Enter/Down/Enter; stop --graceful delivers exitkey safely, fallback
chain unchanged
- create SKILL: passive capture-pane probe instead of stray Enter
- Mark FW-W2 resolved; add 3-agent analysis/plan reports"
```
### T5 — Post-commit sanity
```bash
git status --short # expected: empty
grep -n "Proceeding anyway" .agents/skills/lib.sh # expected: no match
grep -rn "sleep 0.5$" .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job # expected: no match
```
**Halt conditions**: any DoD failure, any `bash -n` error, any new shellcheck warning, or dialog tokens that cannot be validated (DoD-5) → stop, do not commit, report back to Planner with the failing capture.
---
## 4. Risk Register
| Risk | Severity | Mitigation |
|---|---|---|
| Dialog signature tokens don't match real TUI text | High (silently defeats RC-B fix) | DoD-5 is a merge blocker; tokens curated per agent release |
| A2 submit-check false-fails on transcript echo | Medium (spurious rollback) | Pane-change AND-condition + DoD-6 tuning |
| `wait_for_tui_ready` now failing hard changes create-path behavior on slow hosts | Medium | 15×1s budget unchanged; failure now rolls back loudly instead of injecting blind — strictly better; monitor first real runs |
| Longer stop latency (`--graceful` quiescence wait) | Low | Fallback chain untouched; worst case ≈ +10 s before kill-session |
| delegate-job sourcing lib.sh in copied-out installs | Low | Installers ship `.agents/skills/` as a tree incl. lib.sh; verified side-effect-free load |
---
## Final Authorization
**Plan status: APPROVED for execution** by Antigravity exactly as written in §3. DoD-5 (real-capture validation of dialog signatures) is a hard merge blocker. Any deviation halts execution and returns to the Planner.
@@ -1,13 +0,0 @@
# 📑 Planner Validation Report: Skill Optimization Plan
- **Reviewer**: Planner Claude (`canary-projects-multi-agent-mux-planner-claude`)
- **Date**: 2026-07-11
- **Target Plan**: `.agents/reports/canary-projects-multi-agent-mux-planner-claude/report-skill-optimization-plan.md`
- **Verdict**: **PASS (APPROVED)**
## 🔎 Validation Details
1. **Recursion Risk (OP-4)**: The initial draft of `mam_tmux` was flagging an infinite recursion loop due to calling the raw `tmux` shell function instead of the resolved path. The hotfix successfully mapped this to `_REAL_TMUX_PATH`, neutralizing the stack overflow risk.
2. **Error Safety (OP-1)**: Calling `_wait_session_gone` as a standalone statement was a severe `set -e` abort hazard in `stop_session.sh`. The integration of the `|| true` guard successfully resolves this.
3. **Correctness**: The event-driven loop and wait structures are functionally safe.
The plan is approved for immediate integration.
@@ -1,100 +0,0 @@
# 📑 Multi-Agent Mux (MAM) Skill Optimization Plan
Based on the joint code audits conducted by **Reviewer Cline** and **Creator Claude**, this plan identifies the structural inefficiencies, duplicate code paths, and latent portability risks in the MAM skills library (`.agents/skills/`), and provides a phased execution blueprint for refactoring and optimization.
---
## 📊 Summary of Optimization Focus Areas
The audit of all 8 shell entry points (~3,422 lines) revealed three key areas where the skills codebase can be significantly optimized:
1. **Sleeps to Handshakes (Timing Bets)**: Replacing fixed timing loops with event-driven or reactive waits (e.g., reactive tmux stop, MQTT suback event check).
2. **Structural Consolidation (DRY principle)**: Reducing code duplication across scripts, such as 7 identical copies of the SQLite/YAML loader block and 4 copies of tmux server resolution.
3. **Portability & Observability**: Guarding against zsh path resolution anomalies when sourcing `lib.sh`, and eliminating silent failures inside monitor loops.
---
## 🛠️ Detailed Optimization Items
### 1. Inefficient Polling & Sleep Reductions
#### 🚀 OP-1: Reactive Tmux Graceful Stopping (`stop_session.sh`)
* **Location**: `stop_session.sh:193,200`
* **Defect**: Graceful stopping uses fixed sleeps (`sleep 3` after sending exitkey, `sleep 5` after kill-session). Every stop operation incurs an unconditional 38 s delay, even if the agent session exits in milliseconds.
* **Optimization**: Implement `_wait_session_gone` helper in `lib.sh` that polls `tmux has-session` at a high frequency (e.g., every 250 ms) up to a deadline.
* **Outcome**: Reduces average session stop time from **8 s to <0.3 s** under ordinary circumstances.
#### 🚀 OP-2: MQTT Subscriber Event-Driven Handshake (`delegate-job`)
* **Location**: `multi-agent-mux-delegate-job:119,205`
* **Defect**: Sponsoring a subscriber runs in the background, followed by a blind `sleep 1` to win the race against the agent's startup event publish. If HiveMQ CONNACK/SUBACK is slow, the start event is lost; if fast, 1 s is wasted.
* **Optimization**: Modify `job_subscriber.py` to write a sentinel line (e.g. `SUBSCRIBED <topic>`) to its log file on a successful SUBSCRIBE callback. Replace `sleep 1` in the wrapper with a fast-poll loop matching this sentinel.
* **Outcome**: Eliminates event-loss race conditions over WAN brokers, while dropping the startup delay to the physical minimum.
#### 🚀 OP-3: Main Event Loop Pacing (`reconcile.sh`)
* **Location**: `reconcile.sh:243-256` (MQTT client wait)
* **Defect**: The foreground loop spins on a CPU-wake polling model `while True: time.sleep(0.5)` just to compare time differentials for deadlines, bypassing python's event capabilities.
* **Optimization**: Use a `threading.Event()` wait state (`stop.wait(timeout=next_deadline - now)`) to suspend the main thread until a true timeout occurs or an interrupt event fires.
* **Outcome**: Zero-CPU footprint while idling.
---
### 2. Code Duplication & Modularization (DRY)
#### 🚀 OP-4: Unify Divergent Tmux Server Resolvers
* **Location**: `lib.sh:1043-1046`, `create_session.sh:212`, `delegate-job:347`
* **Defect**: String resolution for tmux servers (`local_tmux="tmux -L $TMUX_SERVER_NAME"`) is duplicated 4 times, leading to potential word-splitting hazards (shellcheck SC2086).
* **Optimization**: Extract a single, canonical `mam_tmux()` dispatch function into `lib.sh` that safely handles server arguments and exports them cleanly.
#### 🚀 OP-5: Single-Source the YAML / SQLite Load Boilerplate (7× Duplicate)
* **Location**: `lib.sh` (3 sites), `stop_session.sh:87`, `status.sh:42`, `update_yaml_resumed.sh:66`, `reconcile.sh:298`
* **Defect**: The ~20 lines of Python heredoc code that dynamically queries merged YAML and SQLite state is copy-pasted in 7 separate files, each with slightly drifted error policies.
* **Optimization**: Implement `load_state_json` in `lib.sh` which executes the Python boilerplate exactly once and emits the state to stdout as a JSON document. Script files can then parse this single JSON document.
#### 🚀 OP-6: Consolidate TUI Ready / Dialog Tokens
* **Location**: `lib.sh:1058` and `lib.sh:1205`
* **Defect**: Regular expressions for Claude ready-states and trust dialog tokens are duplicated. Updates to one block (e.g. for new Claude versions) can lead to drift and prompt-lock bugs.
* **Optimization**: Declare central constants (`_MAM_DIALOG_TOKENS`, `_MAM_READY_TOKENS_CLAUDE`) at the top of `lib.sh` and refer to them.
---
### 3. Portability & Robustness
#### 🚀 OP-7: Guard against Non-Bash Sourced Environments
* **Location**: `lib.sh:17` and all 8 script headers
* **Defect**: If a user runs a zsh session and types `source .agents/skills/lib.sh`, `${BASH_SOURCE[0]}` resolves to empty, leading to silent path resolution failure.
* **Optimization**: Add a zsh-aware fallback detection block for the parent script path (`ZSH_VERSION` check) or print an explicit exit message warning users not to source from a foreign shell.
#### 🚀 OP-8: Make Degraded Mode Failures Observable (`reconcile.sh`)
* **Location**: `reconcile.sh:269`
* **Defect**: Fallback polling mode (`bash reconcile.sh --once --emit-diff >/dev/null 2>&1 || true`) discards stderr and exit codes. If database locks or SQLite faults occur, the monitor stays silently broken.
* **Optimization**: Capture stdout/stderr of the one-off run. Log errors and exit the loop for supervisor restart if 5 consecutive runs fail.
---
## 📅 Actionable Optimization Roadmap
We recommend executing these optimizations in three sequential phases:
```mermaid
gantt
title MAM Skill Optimization Roadmap
dateFormat YYYY-MM-DD
section Phase 1 (Latency)
OP-1 (Reactive Tmux Stop) :active, p1, 2026-07-12, 1d
OP-2 (MQTT Subscribe Handshake):active, p2, after p1, 2d
OP-3 (Event Loop CPU Wait) :p3, after p2, 1d
section Phase 2 (DRY & Consolidate)
OP-4 (Tmux Dispatcher) :p4, 2026-07-15, 1d
OP-5 (JSON Loader Helper) :p5, after p4, 2d
OP-6 (Ready Token Constants) :p6, after p5, 1d
section Phase 3 (Portability & Safety)
OP-7 (zsh Source Guard) :p7, 2026-07-19, 1d
OP-8 (Reconcile Observability) :p8, after p7, 1d
```
---
## 📋 Definition of Done (DoD) for Optimizations
1. **Shell Linting**: `bash -n <script>` passes; zero new `shellcheck` warnings.
2. **Functional verification**: All tests in the prompt-lock test suite (T-A through T-E) pass on an isolated scratch server (`-L sks-test`).
3. **Drift-Free**: `reconcile.sh` correctly resolves running tmux sessions after code unification.
4. **Interactive testing**: Graceful stop runs successfully and reports exit time under 1 s.
@@ -1,46 +0,0 @@
# 리뷰 리포트 — Job 20a83d73
- **리뷰 대상**: 커밋 `f79fd99``create_session.sh` / `resume_session.sh`에 에이전트 바이너리 절대 경로 해석(`command -v`) 및 macOS 격리 속성 해제(`xattr -d com.apple.quarantine`) 추가로 macOS 타임아웃 오류 수정
- **리뷰어**: claude (planner-reviewer)
- **리뷰 방식**: 정적 분석(bash -n, shellcheck 기준선 대비) + 격리 tmux 서버에서의 실제 실행 재현 검증
## 1. 설계 타당성 — 실행으로 검증함
macOS에서의 실제 고장 메커니즘은 "tmux 서버가 축소된 PATH로 기동 → pane에서 `claude`/`agy` 미발견 → pane 즉사 → `wait_for_tui_ready` 타임아웃"이다. 이 메커니즘과 수정 효과를 Linux에서 격리 tmux 서버(`-L mam_rev_20a83d73`, `env -i PATH=/usr/bin:/bin`)로 직접 재현했다:
- **Case A (수정 전 시나리오)**: PATH 밖의 가짜 에이전트를 bare name으로 `new-session`**pane 즉사 확인** (타임아웃 전조 재현 성공).
- **Case B (수정 후 시나리오)**: 동일 조건에서 절대 경로로 `new-session`**세션 생존 + 에이전트 실제 실행 확인** (마커 파일 기록됨).
호출 스크립트(전체 PATH 보유) 시점에 `command -v`로 해석해 절대 경로를 명령 문자열에 굽는 설계는 이 문제의 정확한 해법이다. Gatekeeper quarantine 해제도 macOS 최초 실행 지연/행에 대한 합리적 보완책이다(Darwin 전용 가드로 Linux 무영향).
## 2. 정적 분석
- `bash -n` 양 파일 통과.
- `shellcheck -S warning`: 변경 전 기준선(31ca11c 시점 파일을 추출해 비교) 대비 **신규 경고 0건**. `resume_session.sh:40`의 SC2155 1건은 이번 diff와 무관한 기존 경고로 변화 없음.
## 3. 동작성 검증 (실행 기반)
- **해석 스니펫 단독 실행**: PATH에 있는 `claude``/home/godopu16/.local/bin/claude`로 정상 해석. PATH에 없는 이름 → bare name으로 안전한 폴백, `set -euo pipefail` 하에서 exit 0 (조건문 내 `command -v` 실패가 set -e를 트립하지 않음을 실측).
- **실제 스크립트 스모크**: `create_session.sh --dry-run`(실제 claude 에이전트, 격리 서버명 지정)으로 신규 블록 포함 전체 경로가 exit 0으로 통과 — 부수효과 없이 CMD_FULL 확정 지점까지 실행됨.
- **xattr 안전성**: Darwin 가드로 Linux에서 완전 스킵. macOS에서 `xattr` 부재/실패 시에도 `2>/dev/null || true` 패턴이 `set -e`를 트립하지 않음을 동형 재현으로 확인. `[ -f "$RESOLVED_BIN" ]` 가드 덕에 미해석(bare name) 상태에서는 실행 자체가 스킵됨.
## 4. 유실 검사
- 4개 에이전트(claude/agy/hermes/cline)의 플래그(`--dangerously-skip-permissions`, `-i`, `-r/--conversation/--resume/--id $UUID`) 및 `ISO_ENV_PREFIX`/`ISO_CMD_ARGS` 배치가 변경 전과 전부 동일하게 보존됨. cline이 env prefix를 받지 않는 기존 비대칭도 그대로 유지(회귀 없음).
- claude wrapper 경로(비격리 시 `~/.local/bin/<session>` 우선)는 양 스크립트 모두 변경되지 않음.
- 다운스트림 영향: drift 클래스 AD(reconcile.sh)와 status.sh는 `cmd_full`/`start_command`를 비교 로직에 사용하지 않고 표시용으로만 전달함을 확인 — 절대 경로가 들어가도 오탐 없음.
## 5. 비차단(Non-blocking) 지적 사항
1. **경로 내 공백 취약**`RESOLVED_BIN`이 공백 포함 경로로 해석되면 CMD_FULL이 깨짐을 격리 tmux에서 실측으로 확인(pane 즉사). 다만 대상 CLI들의 표준 설치 경로(`/opt/homebrew/bin`, `~/.local/bin`, npm global 등)에는 공백이 없고 macOS 홈 디렉터리 short name에도 공백이 없어 실사용 확률은 낮음. 후속 개선 시 `printf %q` 또는 인용 부호 처리를 권장(단, resume 쪽 `eval` 이중 해석 계층 고려 필요).
2. **중복 분기**`cline` 분기와 else 분기가 기능적으로 완전 동일(`command -v cline` == `command -v "$AGENT"` when AGENT=cline). 동작 문제는 없으나 단순화 여지 있음(양 파일 공통).
3. **resume 후 메타데이터 불일치(외관상)**`update_yaml_resumed.sh`가 resume 후 `cmd_full`을 bare name 형태로 되써서, 실제 pane은 절대 경로로 실행됐는데 YAML 기록은 bare name이 됨. 비교 로직에 쓰이지 않는 표시 전용 필드라 실해는 없음.
4. **macOS 실기기 미검증** — 본 리뷰 환경은 Linux이므로 `xattr` 실효(quarantine 속성 실제 제거) 자체는 실측 불가. 가드/에러 억제 로직의 안전성은 동형 재현으로 확인했고, 명령·플래그는 표준 macOS 관행과 일치함.
참고: 리뷰 중 발견된 저장소 내 `multi-agent-mux-delegate-job.27194_12342.tmp` 파일은 고아 파일이 아니라 **본 job(20a83d73)을 디스패치 중인 살아있는 delegate_job_safe 임시 사본**(PID 확인됨)으로, 직전 라운드에서 검증한 trap 정리 대상이다. 결함 아님.
## 6. 결론
수정의 핵심 메커니즘(절대 경로 baking)이 재현 실험으로 실효성이 입증되었고, 기존 동작 유실·신규 경고·다운스트림 회귀가 전무하다. 비차단 지적 4건은 모두 후속 개선 수준이며 설계 재작업이 필요한 사항은 없다.
[VERDICT: PASS]
@@ -1,33 +0,0 @@
# 리뷰 리포트 — Job 4094502a (MULTI_AGENT_RULES.md/ko.md tmux→herdr 개정)
- **리뷰 대상**: 작업 트리 미커밋 diff — `.agents/MULTI_AGENT_RULES.md`(6개소), `.agents/MULTI_AGENT_RULES.ko.md`(7개소), cline 작성
- **리뷰어**: claude (planner-reviewer)
## 1. 변경 무결성 — 토큰 단위 검증
`git diff --word-diff` 전수 집계 결과, 변경은 **정확히 23개 토큰 치환**(TMUX/Tmux/tmux → Herdr/herdr, `Tmux-based``Herdr-based`, `<tmux_session_name>``<herdr_session_name>` 경로 플레이스홀더 4건 포함)뿐이며 **문장 추가·삭제·구조 변경 0건**. 번역 유실이나 mermaid/sequenceDiagram 블록 훼손 없음. 양 언어판의 치환 지점이 상호 대응함(ko의 send-keys 문구 1건은 원래 ko에만 존재하는 기존 번역 차이로, 이번 diff와 무관).
## 2. 잔존 레거시 스윕
- 두 파일 모두 대소문자 무시 `tmux` 검색 **0건** — 누락된 레거시 없음.
- 저장소 전체에서 `<tmux_session_name>` 플레이스홀더 잔존은 `.agents/reports/` 하위 **아카이브된 과거 리뷰 리포트 3건뿐** — 역사적 감사 기록이므로 개정 대상이 아님(방치 아님).
## 3. herdr 사양 교차 검증 (실구현 대조)
| 문서 표기 | 실구현 근거 | 판정 |
|---|---|---|
| "herdr `send-keys`" / "herdr 입력" | shim(`.mam/shim/herdr:318`)에 `send-keys` 의사 명령 실재 | ✅ 부합 |
| `capture-pane -S -200` (유지된 기존 문구) | shim `capture-pane` 의사 명령 실재(:301) | ⚠️ 명령은 실재하나 아래 비차단 지적 1 참조 |
| `.agents/reports/<herdr_session_name>/` 관례 | 실제 디렉터리(`.agents/reports/canary-projects-…-cline` 등)가 세션명 기반으로 운영 중 | ✅ 부합 |
| "Herdr 모니터링 상태 … SQLite WAL" | `.mam/agent-sessions.db` + `herdr_sessions` 스키마 현행 일치 | ✅ 부합 |
## 4. 비차단(Non-blocking) 지적
1. **`capture-pane -S -200`의 시맨틱 공백(기존 문구, 이번 diff 무관)**: shim의 `capture-pane``-S -200` 플래그를 파싱하지 않고 조용히 무시하며 항상 `agent read --source visible --lines 100`으로 동작한다. 즉 문서가 약속하는 "스크롤백 200행 백업"이 실제로는 "가시 영역 100행"으로 축소 실행된다. 스냅샷 규칙의 취지(뷰포트 절단 방지)가 약화되므로, 후속 개선으로 (a) shim이 `-S -N``--lines N`으로 매핑하거나 (b) 문서에서 플래그 표기를 herdr 실사양으로 갱신할 것을 권장.
2. **의사 명령 전제 미표기**: 다른 SKILL.md들은 `send-keys`/`capture-pane`이 "lib.sh 소싱 후에만 동작하는 tmux-compat 의사 명령"임을 명시하나, 본 규칙 문서는 전제 없이 사용한다. 규칙서가 신규 에이전트의 첫 관문임을 고려하면 각주 1줄 추가 가치가 있음.
## 5. 결론
치환은 토큰 단위로 정밀하고(내용 유실 0), 잔존 레거시 0건이며, 도입된 어휘가 현행 스킬/shim/디렉터리 관례와 전부 부합한다. 비차단 2건은 이번 diff가 만들지 않은 기존 문구의 후속 개선 사항이다.
[VERDICT: PASS]
@@ -1,77 +0,0 @@
# 구현 계획서 — deploy/ 배포 설정 tmux→herdr 전환 (Job baa15c96)
- **Planner**: claude (planner-reviewer)
- **입력**: `.mam/deploy_brief.md` + `deploy/` 전수 분석 + 현행 스킬 구현(단일 진실 소스) 대조
- **핵심 원칙**: 문서·스크립트가 브리프의 *추정* 어휘가 아니라 **현행 스킬이 실제 소비하는 어휘**와 일치해야 한다. 실측 결과 브리프의 제안 중 2건은 실제 구현과 다르므로 아래와 같이 교정한다:
- ~~`HERDR_SESSION_NAME`~~ → **`HERDR_SERVER_NAME`** (스킬 전체가 이 이름만 소비, `TMUX_SERVER_NAME` 소비처는 0곳)
- ~~`--herdr-session`~~ → **`--herdr-server`** (`create_session.sh:63`이 수용하는 유일한 플래그, `--tmux-server`는 이미 제거되어 **미지원**)
## 0. 실측 현황 (전수 스윕: `grep -in "tmux" deploy/` + 스킬 대조)
| 파일 | 행 | 현재 내용 | 판정 |
|---|---|---|---|
| install_mam.sh | 224 | `--tmux-server multi-agent-mux` (Quick Start 예시) | 🔴 **기능 파손** — create_session.sh가 이 플래그를 거부(unknown arg, exit 2). 사용자가 복붙 시 즉시 실패 |
| install_mam.sh | 227 | `$ tmux -L multi-agent-mux attach -t <session_name>` | 🔴 죽은 명령 |
| install.sh | 31 | `check_cmd tmux` | 🟠 잘못된 의존성 진단(herdr 미검사) |
| install.sh | 249 | `.env``TMUX_SERVER_NAME=default` 기록 | 🟠 죽은 설정(소비처 0) |
| README.md | 9 | requirements "(`tmux`, `python3`)" | 🟡 문서 불일치 |
| INSTALL.md | 21, 37 | 진단 목록에 `tmux` | 🟡 문서 불일치(install_mam.sh:86 실제 DEPS는 이미 `herdr python3 rsync uuidgen`) |
| INSTALL.md | 69 | "호스트 재기동으로 tmux가 소멸한 경우" | 🟡 문서 불일치 |
| plugin.json | 3 | `"... Backplane on Tmux & MQTT."` | 🟡 메타데이터 불일치 |
| remove.sh / update.sh | — | tmux/kill 로직 **없음**(파일 삭제·venv·문서 갱신뿐) | ✅ 수정 불요(브리프 검토 항목 종결) |
| generate-env.sh / gitea-ci.yml | — | tmux 참조 없음 | ✅ 수정 불요 |
## 1. 파일별 수정 계획 (변경 대비표)
### 1-1. `deploy/install_mam.sh` (우선순위 1 — 기능 파손 수정)
| 행 | 변경 전 | 변경 후 |
|---|---|---|
| 224 | `--tmux-server multi-agent-mux` | `--herdr-server multi-agent-mux` |
| 227 | `$ tmux -L multi-agent-mux attach -t <session_name>` | `$ HERDR_SERVER_NAME=multi-agent-mux herdr agent attach <session_name>` |
근거: 224는 `create_session.sh:63`의 실제 플래그. 227은 create_session.sh:328이 YAML `attach_command`로 방출하는 **canonical 형식**(`HERDR_SERVER_NAME=<server> herdr agent attach <name>`)과 동일하게 맞춘다(개별 에이전트 pane attach). 전체 서버 화면이 필요하면 `herdr session attach multi-agent-mux`도 각주로 병기 가능.
### 1-2. `deploy/install.sh`
| 행 | 변경 전 | 변경 후 |
|---|---|---|
| 31 | `check_cmd tmux` | `check_cmd herdr` |
| 249 | `TMUX_SERVER_NAME=default` | `HERDR_SERVER_NAME=default` |
권장 추가(선택): `check_cmd herdr` 실패 시 install_mam.sh:9698과 동일한 설치 안내(`curl -fsSL https://herdr.dev/install.sh \| sh`)를 출력하도록 `check_cmd` 호출부 뒤에 힌트 블록 추가 — 두 인스톨러의 UX 일관성 확보.
마이그레이션 노트: 기존 설치본 `.env``TMUX_SERVER_NAME`은 소비처가 없어 잔존해도 무해하므로 자동 치환 로직은 불요(계획서 기록으로 갈음).
### 1-3. `deploy/README.md`
| 행 | 변경 전 | 변경 후 |
|---|---|---|
| 9 | ``checks system requirements (`tmux`, `python3`)`` | ``checks system requirements (`herdr`, `python3`)`` |
### 1-4. `deploy/INSTALL.md`
| 행 | 변경 전 | 변경 후 |
|---|---|---|
| 21 | ``시스템의 `tmux`, `python3`, `rsync`, `uuidgen` …을 진단`` | ``시스템의 `herdr`, `python3`, `rsync`, `uuidgen` …을 진단`` |
| 37 | ``**의존성 진단**: … `tmux`, `python3`, …`` | ``**의존성 진단**: … `herdr`, `python3`, …`` |
| 69 | `호스트 재기동으로 tmux가 소멸한 경우에도` | `호스트 재기동으로 herdr 서버가 소멸한 경우에도` |
### 1-5. `deploy/plugin.json`
| 행 | 변경 전 | 변경 후 |
|---|---|---|
| 3 | `"… Backplane on Tmux & MQTT."` | `"… Backplane on Herdr & MQTT."` |
### 1-6. `deploy/remove.sh`, `deploy/update.sh` — **수정 없음** (분석 결과 기록)
두 스크립트 모두 세션 킬링 로직 자체가 존재하지 않고 파일 자산 삭제/갱신만 수행하므로 tmux→herdr 마이그레이션 대상이 아니다. (선택적 후속 개선: remove.sh가 스킬 제거 전 실행 중인 herdr 에이전트 세션을 `multi-agent-mux-stop`으로 정리하도록 권고하는 안내 문구 추가 — 본 브리프 범위 밖이므로 별도 결정.)
## 2. 구현 순서
1. install_mam.sh (기능 파손 우선) → 2. install.sh → 3. 문서 3종(README/INSTALL/plugin.json) → 4. 검증 → 5. 커밋(예: `fix(deploy): migrate deployment scripts and docs from tmux to herdr`).
## 3. 검증 계획 (Reviewer/Creator 공용 DoD)
1. **잔존 스윕**: `grep -rin "tmux" deploy/` → **0건** (대소문자 무시 필수 — `Tmux` 표기가 plugin.json에 존재했음).
2. **정적**: 수정된 .sh에 `bash -n` + `shellcheck -S warning`, 변경 전 대비 신규 경고 0건. plugin.json은 `python3 -m json.tool`로 유효성 확인.
3. **기능(핵심)**: Quick Start 예시를 **그대로 복붙 실행** — 스크래치 워크스페이스에서 `create_session.sh --workspace <scratch> --agent claude --role developer --isolate --herdr-server <scratch서버명> --dry-run` 이 exit 0. (--dry-run이 spawn 전에 종료하므로 라이브 무접촉. `--isolate` 플래그는 create_session.sh:66에서 여전히 수용됨을 확인 완료.)
4. **attach 명령 실증**: 문서의 attach 형식이 실제 YAML `attach_command`(예: `.mam/agent-sessions.yaml:18`)와 동일 형식인지 대조.
5. **회귀**: `.env` 신규 생성 경로에서 `HERDR_SERVER_NAME=default` 기록 확인, `TMUX_SERVER_NAME` 소비처 0곳 재확인(`grep -rn TMUX_SERVER_NAME .agents/skills` = 0).
## 4. 완료 기준
- deploy/ 내 tmux 참조(대소문자 무관) 0건, Quick Start 예시 실행 가능, 정적 검사 클린, 문서 어휘가 현행 스킬 구현(`--herdr-server`/`HERDR_SERVER_NAME`/`herdr agent attach`)과 1:1 일치.
@@ -1,95 +0,0 @@
# ✅ Peer Review Report: multi-agent-mux-loop SKILL.md & run_loop.sh Refactoring (Commits 52c270e, f85fdfc, 6c90342)
**Job**: `47d1dce6` · **Reviewer**: Reviewer B (Cline, `canary-projects-multi-agent-mux-reviewer-cline`)
**Review Targets**:
- Commit `52c270e` — "docs(skill): update multi-agent-mux-loop SKILL manual to reflect skipped planning mode when --plan is omitted"
- Commit `f85fdfc` — "docs(skill): genericize multi-agent-mux-loop SKILL manual by replacing hardcoded agent session names with placeholders"
- Commit `6c90342` — "fix(skill): resolve hardcoded planner session name and plan file paths dynamically in run_loop.sh"
**Prior Context**: PTY 리뷰 5회차 완료 (cc09bae5 PASS). 본 잡은 multi-agent-mux-loop 오케스트레이션 스킬의 문서/스크립트 리팩토링 리뷰.
**Plan Reference**: `.agents/reports/canary-projects-multi-agent-mux-planner-reviewer-claude/report-final.md` (Rev.3)
**Review Scope**: 브리프가 요청한 "잠재적인 문법 오류나 셸 스크립트 오작동 여부 꼼꼼한 검토"
**Method**: 라인 단위 diff 분석 + `bash -n` 문법 검사 + `shellcheck` 정적 분석 + Python 임베디드 코드 4시나리오 런타임实证 + Self-Planning Mode bash 로직 3시나리오 `set -euo pipefail` 시뮬레이션 + mermaid 다이어그램 문법 검증 + 플레이스홀더 일관성 교차 검증
---
## 1. 커밋 개요
3개 커밋이 multi-agent-mux-loop 오케스트레이션 스킬의 문서와 스크립트를 리팩토링:
| 커밋 | 파일 | 변경량 | 내용 |
|------|------|--------|------|
| 52c270e | SKILL.md | 문서 | 계획 생략 모드 설명 업데이트 + mermaid 시퀀스 다이어그램 "Use Existing Plan (No --plan)" 분기 추가 |
| f85fdfc | SKILL.md | 문서 | 하드코딩 에이전트명 → 범용 플레이스홀더(`<creator-session-name>`, `<reviewer-session-name>`) 정제 |
| 6c90342 | run_loop.sh | +5/-3 | 하드코딩 fallback 플래너 세션명/계획 파일 경로 → 동적 `$PLANNER_SESSION` 변수 기반 리팩토링 |
---
## 2. 핵심 검증: run_loop.sh 셸 스크립트 (6c90342)
### 2.1 정적 분석 — ✅ 통과
| 검증 | 방법 | 결과 |
|------|------|------|
| bash 문법 검사 | `bash -n run_loop.sh` | ✅ SYNTAX OK |
| shellcheck (기본) | `shellcheck run_loop.sh` | ✅ EXIT 0 (경고/에러 전무) |
| shellcheck (-x 외부 소스 제외) | `shellcheck -x -S warning run_loop.sh` | ✅ EXIT 0 |
| shellcheck 버전 | 0.11.0 | 최신 분석 도구 |
**평가**: ✅ 셸 스크립트 정적 분석 완벽 통과. 문법 오류, 미정의 변수, 인용 오류, 조건부 파이프라인 등 shellcheck가 감지할 수 있는 모든 결함이 전무.
### 2.2 변경 1: `resolve_planner_session` 함수 (라인 185 영역)
**diff**:
```diff
-planner = 'canary-projects-multi-agent-mux-planner-reviewer-claude'
+planner = ''
for s in d.get('tmux_sessions', []):
if 'planner' in s.get('role', ''):
planner = s.get('name')
```
**분석**: 하드코딩된 플래너 세션명을 빈 문자열 초기값으로 변경. 이후 루프가 `tmux_sessions` 배열에서 `role`에 'planner'가 포함된 세션을 동적으로 검색하여 할당. 찾지 못하면 빈 문자열 반환.
**Python 임베디드 코드 런타임实证 (4시나리오)**:
| 시나리오 | 입력 MAM_STATE_JSON | 출력 | 기대 | 결과 |
|----------|---------------------|------|------|------|
| 1. 플래너 발견 | `{tmux_sessions:[{name:test-creator,role:creator},{name:test-planner-xyz,role:planner}]}` | `test-planner-xyz` | 동적 세션명 | ✅ |
| 2. 플래너 없음 | `{tmux_sessions:[{name:test-creator,role:creator}]}` | ``(빈) | 빈 문자열 | ✅ |
| 3. 빈 상태 | `{}` | ``(빈) | 빈 문자열 | ✅ |
| 4. env var 없음 | unset | ``(빈) | 빈 문자열 | ✅ |
**평가**: ✅ Python 임베디드 코드가 4가지 시나리오에서 모두 올바르게 동작. 동적 세션명 할당 및 빈 문자열 안전 반환 확인.
### 2.3 변경 2: Self-Planning Mode 계획 파일 로드 (라인 312 영역)
**diff**:
```diff
- EXISTING_PLAN_FILE=".agents/reports/canary-projects-multi-agent-mux-planner-reviewer-claude/report-final.md"
- if [ -f "$EXISTING_PLAN_FILE" ]; then
+ EXISTING_PLAN_FILE=""
+ if [ -n "$PLANNER_SESSION" ]; then
+ EXISTING_PLAN_FILE=".agents/reports/$PLANNER_SESSION/report-final.md"
+ fi
+ if [ -n "$EXISTING_PLAN_FILE" ] && [ -f "$EXISTING_PLAN_FILE" ]; then
```
**분석**: 하드코딩된 경로를 `$PLANNER_SESSION` 동적 변수 기반 경로로 변경. 2단계 가드 추가:
1. `[ -n "$PLANNER_SESSION" ]` — 빈 세션명이면 경로 구성 스킵
2. `[ -n "$EXISTING_PLAN_FILE" ] && [ -f "$EXISTING_PLAN_FILE" ]` — 빈 경로이거나 파일이 없으면 로드 스킵
**변수 할당 흐름 추적**:
- `PLANNER_SESSION`는 라인 211에서 `resolve_planner_session()` 호출로 할당 — Self-Planning Mode(라인 312) **이전**에 실행 ✅
- `CURRENT_PLAN`는 라인 213에서 `CURRENT_PLAN=""`로 초기화 — `set -u` (nounset) 오류 방지 ✅
- 라인 335: `if [ -n "$CURRENT_PLAN" ]` — 빈 문자열이면 false → `EXECUTION_PROMPT`에 계획서 미포함 ✅
- 라인 543: `if [ "$PLAN_MODE" = true ] && [ -n "${CURRENT_PLAN:-}" ]` — `${CURRENT_PLAN:-}` 기본값 확장으로 `set -u` 추가 방어 ✅
**Self-Planning Mode bash 로직 시뮬레이션 (3시나리오, `set -euo pipefail` 하)**:
| 시나리오 | PLANNER_SESSION | 동작 | CURRENT_PLAN | 결과 |
|----------|-----------------|------|--------------|------|
| 1. 실제 플래너 (파일 존재) | `canary-...-planner-reviewer-claude` | 계획 로드 | 2220자 | ✅ |
| 2. 빈 문자열 | `` | 파일 로드 스킵 | 0자 | ✅ |
| 3. 다른 플래너 (파일 없음) | `some-other-planner` | 파일 없음 스킵 | 0자 | ✅ |
**평가**: ✅ `set -euo pipefail` (특히 `set -u` nounset) 하에서 3가지 시나리오 모두 에러 없이 통과. 변수 안전성 확보. 2단계 가드 로직이 빈 세션명/존재하지 않는 파일을 올바르게 처리.
@@ -1,138 +0,0 @@
# ✅ Peer Review Report: M2 PTY _exit Syscall Symbol Correction (Commit f0e2bd2)
**Job**: `cc09bae5` · **Reviewer**: Reviewer B (Cline, `canary-projects-multi-agent-mux-reviewer-cline`)
**Review Target**: Commit `f0e2bd2` — "fix(ui): correct libc symbol lookup for direct _exit syscall to achieve async-signal-safety"
**Prior Reviews**:
- Job `66ec158f` (b7901bc) → 3 BLOCKING 결함 지적
- Job `fcf4c9d0` (f52f6eb) → DEFECT 1/2 해결, DEFECT 3 미해결
- Job `7448cb2f` (7781e79) → async-signal-safety/waitpid 해결, DEFECT 3 미해결 (3회차)
- Job `ef0b32ff` (7f1a7e5) → **DEFECT 3 해결 (4회차) + 모든 결함 PASS** — 본 커밋은 ef0b32ff 리뷰의 NON-BLOCKING 관찰 #1 정밀 수정
**Plan Reference**: `.agents/reports/canary-projects-multi-agent-mux-planner-reviewer-claude/report-final.md` (Rev.3, §6.7 PTY 메커니즘 / §10 DoD)
**Review Scope**: 브리프가 명시한 `cExit` lookup 심볼 오타 수정 (`'exit'``'_exit'`) 검증 + 회귀 확인
**Method**: 라인 단위 diff 분석 + `dart analyze`/`flutter analyze`/`dart test` + **`_exit` 심볼 glibc resolve实证** + TMUX env 격리 회귀实证
---
## 1. 커밋 개요
커밋 `f0e2bd2`는 이전 리뷰(ef0b32ff)의 NON-BLOCKING 관찰 #1을 정밀 수정. 1개 파일, +1/-1행 (단일 라인 변경).
**변경 내용** (`pty_session.dart:111`):
```diff
- final cExit = libc.lookupFunction<_exit_c, _exit_dart>('exit');
+ final cExit = libc.lookupFunction<_exit_c, _exit_dart>('_exit');
```
---
## 2. 수정 항목 교차 검증
### 2.1 이전 리뷰 관찰 (ef0b32ff, NON-BLOCKING #1)
> **`cExit` lookup 이름 (정확성)**: 라인 111 `lookup('exit')`는 C `exit()`를 바인딩 (async-signal-unsafe, atexit handlers 실행). 브리프가 "libc exit syscall"이라고 서술했으나, 진정한 async-signal-safe는 `lookup('_exit')` 또는 `lookup('_Exit')`. 단, 자식이 fork 직후이므로 Dart 런타임 atexit handlers가 미등록 상태이며, 기능적으로 자식 종료를 달성하므로 실질적 영향 없음. 향후 정확성을 위해 `_exit` 권장.
### 2.2 수정 검증
**diff 분석**: 라인 111에서 `lookup('exit')``lookup('_exit')`로 정확히 1행 수정. 다른 라인 무변경 ✅.
**C `exit()` vs `_exit()` 구분**:
- `exit(int status)` (stdlib.h): async-signal-**unsafe** — `atexit()` 등록 핸들러 실행, `stdio` 버퍼 flush, `_exit()` 최종 호출
- `_exit(int status)` (unistd.h): async-signal-**safe** — 커널 syscall 직접 호출, 버퍼 flush/handlers 미실행
POSIX async-signal-safety 규칙에 따르면, fork 후 exec 실패 시 자식에서 호출할 수 있는 함수는 async-signal-safe 목록에 있는 함수만. `_exit()`는 이 목록에 포함되나, `exit()`는 포함되지 않음. 본 수정으로 자식 분기의 예외 퇴장 경로(`cExit(-1)` at 라인 192, `cExit(-2)` at 라인 211)가 진정한 async-signal-safe `_exit` syscall을 사용하게 됨.
**FFI 시그니처 일관성**: typedef `_exit_c = ffi.Void Function(ffi.Int32 status)` / `_exit_dart = void Function(int status)`는 C `_exit(int)` 시그니처와 정확히 일치 ✅. 변경 전에도 시그니처는 `_exit` 기준이었으나 lookup 이름만 `exit`였던 불일치가 해결됨.
---
## 3. `_exit` 심볼 glibc resolve实证
**검증 방법**: `nm -D /lib/x86_64-linux-gnu/libc.so.6`로 glibc에서 `_exit` 심볼 존재 확인 + Dart FFI `lookupFunction<_exit_c, _exit_dart>('_exit')` 실행实证.
**결과 1 — glibc 심볼 확인**:
```
$ nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep -w '_exit'
00000000000f7480 T _exit@@GLIBC_2.2.5
```
`T` (Text segment, exported symbol) — `_exit`가 glibc에 존재하며 export됨 ✅.
**결과 2 — Dart FFI lookup实证**:
```
SUCCESS: _exit symbol resolved from libc.so.6 - async-signal-safe exit syscall available
(lookupFunction throws if symbol not found, so reaching here means _exit is bound)
```
`lookupFunction<_exit_c, _exit_dart>('_exit')`가 예외 없이 성공 — 런타임에 `_exit` 심볼이 올바르게 바인딩됨을实证 ✅. `lookupFunction`은 심볼을 찾지 못하면 `ArgumentError`를 throw하므로, 정상 실행 자체가 resolve 성공의 증거.
**평가**: ✅ `lookup('_exit')`가 glibc의 `_exit@@GLIBC_2.2.5` 심볼을 올바르게 바인딩. 런타임에 자식 분기의 `cExit(-1)`/`cExit(-2)` 호출이 진정한 async-signal-safe `_exit` syscall을 기동함.
---
## 4. 정적 분석 및 회귀 검증
| 항목 | 검증 방법 | 결과 |
|------|----------|------|
| `dart analyze` (mam_pty) | 실행 | ✅ No issues found! |
| `flutter analyze` (mam_desktop) | 실행 | ✅ No issues found! |
| M1 회귀 (`dart test` mam_core) | 실행 | ✅ 3/3 All tests passed |
| 런타임 PTY 동작 (`dart test` echo) | 실행 | ✅ echo `hello-pty-ok` 출력 정상 |
| DEFECT 3 TMUX env 격리 (회귀) | 런타임实证 (TMUX 설정 + printenv) | ✅ PASS — 자식 printenv 빈 출력 (회귀 없음) |
| `_exit` 심볼 glibc resolve | `nm -D` + Dart FFI lookup实证 | ✅ `_exit@@GLIBC_2.2.5` 바인딩 성공 |
| 기존 스크립트 회귀 | git diff --stat | ✅ status.sh 외 기존 스크립트 무변경 |
**전체 테스트 실행 결과** (부모에 `TMUX=fake-server,12345,0 TMUX_PANE=%5` 설정):
```
00:00 +0: test/pty_runtime_test.dart: PtySession runtime execution resolves process output
PTY Runtime stdout verified: hello-pty-ok
00:00 +1: test/pty_runtime_test.dart: PtySession strips TMUX/TMUX_PANE from child environment
printenv TMUX TMUX_PANE output: []
00:00 +2: All tests passed!
```
이전 리뷰(ef0b32ff)에서 PASS 판정된 모든 기능이 회귀 없이 유지됨:
- DEFECT 1 (/proc/self/fd 경로): ✅ 유지
- DEFECT 2 (자식 stdio PTY 연결): ✅ 유지
- DEFECT 3 (TMUX env 격리, unsetenv): ✅ 유지 (회귀 없음)
- async-signal-safety: ✅ 유지 + `_exit` 정확성 향상
- waitpid zombie reaping (blocking): ✅ 유지
- non-blocking master fd (fcntl): ✅ 유지
---
## 5. AGENTS.md 원칙 검증
- **Surgical Changes (§3)**: 단일 라인 수정 (`'exit'``'_exit'`) — 이전 리뷰 관찰에 정확히 대응하는 최소 변경 ✅. 다른 코드/포맷/주석 무변경. "Every changed line should trace directly to the user's request" — 본 수정은 1행이며 리뷰 관찰 #1에 직접 추적됨.
- **Simplicity First (§2)**: 단일 라인 정밀 수정 — 더 단순할 수 없는 최소 변경 ✅.
- **Goal-Driven Execution (§4)**: 본 수정의 성공 기준은 "async-signal-safe `_exit` syscall 바인딩" → `nm -D` + Dart FFI lookup实证으로 검증 완료 ✅.
- **Think Before Coding (§1)**: 이전 리뷰(ef0b32ff)에서 `exit()` vs `_exit()`의 async-signal-safety 차이를 명확히 지적했으며, 주 개발자가 이를 정확히 이해하고 수정 — §1 원칙 이행.
---
## 6. 종합 평가
커밋 `f0e2bd2`는 이전 리뷰(ef0b32ff)의 NON-BLOCKING 관찰 #1을 **정확히 단일 라인으로 해결**:
### 수정 항목 — 해결
1.**`cExit` lookup 심볼 정확성**: `lookup('exit')``lookup('_exit')`로 수정. C `exit()` (async-signal-unsafe, atexit handlers 실행) 대신 C `_exit()` (async-signal-safe, 커널 syscall 직접 호출)를 바인딩. 자식 분기의 예외 퇴장 경로(`cExit(-1)` slave open 실패, `cExit(-2)` execvp 실패)가 진정한 async-signal-safe `_exit` syscall을 사용.
### 검증 결과
- `dart analyze`: No issues found ✅
- `flutter analyze`: No issues found ✅
- M1 회귀: 3/3 All tests passed ✅
- 런타임 PTY echo: 정상 동작 ✅
- 런타임 TMUX env 격리: ✅ PASS (회귀 없음)
- **`_exit` 심볼 glibc resolve实证**: ✅ `_exit@@GLIBC_2.2.5` 바인딩 성공 (nm -D + Dart FFI lookup)
- 기존 스크립트 회귀: 없음 ✅
### 전체 리뷰 이력 (5회차 누적)
| 회차 | 커밋 | 판정 | 핵심 |
|------|------|------|------|
| 1 (66ec158f) | b7901bc | NOT PASS | 3 BLOCKING 결함 지적 |
| 2 (fcf4c9d0) | f52f6eb | NOT PASS | DEFECT 1/2 해결, DEFECT 3 미해결 |
| 3 (7448cb2f) | 7781e79 | NOT PASS | async-signal-safety/waitpid 해결, DEFECT 3 미해결 |
| 4 (ef0b32ff) | 7f1a7e5 | **PASS** | DEFECT 3 해결 (unsetenv), 모든 결함 해결 |
| 5 (본 리뷰) | f0e2bd2 | **PASS** | NON-BLOCKING 관찰 #1 정밀 수정 (_exit 심볼) |
ef0b32ff에서 PASS 판정된 모든 기능이 회귀 없이 유지되며, 추가로 `_exit` syscall 바인딩 정확성이 향상됨. M2 마일스톤(Desktop PTY 연동 + attach terminal tab)의 모든 핵심 계약(§6.7 PTY 메커니즘, §10 DoD)이 런타임实证으로 검증됨. 정적 분석과 런타임实证 테스트가 모두 통과.
[VERDICT: PASS]
@@ -1,179 +0,0 @@
# ✅ Peer Review Report: M1 Dashboard & Detail Pane Implementation (Commit 2eb8586)
**Job**: `cb97a36f` · **Reviewer**: Reviewer B (Cline, `canary-projects-multi-agent-mux-reviewer-cline`)
**Review Target**: Commit `2eb8586` — "feat(ui): complete M1 Milestone - read-only Dashboard and Detail Pane with status.sh integration"
**Plan Reference**: `.agents/reports/canary-projects-multi-agent-mux-planner-reviewer-claude/report-final.md` (Rev.3 — Flutter 전면 재작성 계획서)
**Review Scope**: 계획서에 입각하여 제출된 코드가 안전하고 모순 없이 구현되었는지 교차 검증 (구현하지 않음, 리뷰만 수행)
**Method**: 계획서 §3(D8), §5(아키텍처), §6(예외처리/보안 계약), §10(DoD)를 실제 커밋 코드와 라인 단위 교차 검증 + 라이브 실행实证 + Dart 테스트/정적 분석 실행
---
## 1. 커밋 개요
커밋 `2eb8586`는 M1 마일스톤(읽기 전용 대시보드 + Detail Pane)을 구현. 22개 파일, +1941/-326행. 핵심 변경:
- `status.sh` additive 스키마 확장 (D8 해법 구현, +103/-5행)
- `packages/mam_core/` — 순수 Dart 데이터/서비스 계층 (models, command_runner, session_service, status_repository)
- `apps/mam_desktop/` — Flutter Desktop UI (main, session_table, detail_pane, stale_banner, providers, theme, status_script_locator)
- `packages/mam_core/test/session_service_test.dart` — 3개 단위 테스트
---
## 2. D8 — `status.sh --json` additive 스키마 확장 (§3.1) 검증
**계획서 요구**: 기존 5개 키(timestamp/yaml_path/tmux_sessions_alive/tmux_confirmed/drifts/actions) 무변경 + 신규 `sessions_detail` 키 추가. 텍스트 모드 byte-identical 회귀 없음.
**라이브 실행实证**:
```
$ bash status.sh --json | python3 -m json.tool
top keys: ['timestamp', 'yaml_path', 'tmux_sessions_alive', 'tmux_confirmed', 'drifts', 'actions', 'sessions_detail']
sessions_detail count: 2
sessions_detail[0] keys: ['name', 'server', 'status', 'tmux_alive', 'cmd', 'role', 'resume_state',
'job_id', 'job_status', 'pane_cwd', 'attach_command', 'drift_classes', 'pane_pid', 'cmd_full',
'start_command', 'last_visible_status']
```
- 기존 6개 키(timestamp/yaml_path/tmux_sessions_alive/tmux_confirmed/drifts/actions) **전부 보존**
- 신규 `sessions_detail` 키 추가 ✅
- `sessions_detail` 필드가 계획서 §3.1의 D8 계약(name/server/status/tmux_alive/cmd/role/resume_state/job_id/job_status/pane_cwd/attach_command/drift_classes)과 **field-for-field 일치**
- additive beyond D8: `pane_pid`/`cmd_full`/`start_command`/`last_visible_status` — Detail Pane용 추가 필드, 계획서가 "세션명/워크스페이스 등을 계산하는 부분"이라 명시한 범위 내 ✅
**텍스트 모드 회귀 검증 (DoD-1)**:
```
$ diff <(old status.sh text output) <(new status.sh text output)
1c1
< agent-sessions status — 2026-07-16T12:16:37Z (tmux_confirmed=True)
---
> agent-sessions status — 2026-07-16T12:16:38Z (tmux_confirmed=True)
```
유일한 차이는 타임스탬프(1초) — 본문 byte-identical ✅. git diff 분석: 변경은 `--json` 분기(조기 exit 제거 + 새 Python 블록 추가)에만 국한, 텍스트 모드 Python 블록(라인 31~119)은 **무변경** ✅.
---
## 3. 아키텍처 준수 (§5) 검증
### 3.1 모노레포 패키지 구조 (§5.2)
**검증**: `packages/mam_core/`(순수 Dart, Flutter 비의존) + `apps/mam_desktop/`(Flutter Desktop) 분리 구현 ✅. `mam_core``dart:io`/`dart:convert`/`package:meta`만 의존하고 Flutter 엔진 의존성이 없음을 확인 — 헤드리스 실행 가능 원칙 준수. `mam_core.dart` barrel export가 models/services/command_runner를 깔끔히 노출.
### 3.2 `command_runner.dart` — 유일한 서브프로세스 실행 지점 (§6.1, D5)
**검증**:
- `Process.start(argv.first, argv.sublist(1), runInShell: false)` — argv list 강제, `runInShell: false` 명시 ✅ (D5 계약)
- `Future.any([exitFuture, Future.delayed(timeout)])`로 클라이언트측 타임아웃 강제 ✅ (§6.1)
- `killOnTimeout` 파라미터: `true`면 SIGTERM→5s→SIGKILL, `false`면 프로세스 백그라운드 완주 + `backgroundFuture` 반환 ✅ (D-Critical purge 계약)
- `CommandResult``timedOut`/`backgroundFuture` 필드로 타임아웃 상태 명확히 구분 ✅
**평가**: ✅ §6.1 의사코드 계약을 정확히 구현. D5(명령 주입 방지) + D-Critical(purge 원자성 보존) 모두 충족.
### 3.3 `status_repository.dart` — 폴링 + stale/backoff (§6.6, D6)
**검증**:
- `Stream<SessionsPoll> watch()` — 폴링 루프, 실패 시 `lastGood` 스냅샷 유지 + `stale: true` 표시 ✅ (D6)
- 백오프: `failureBackoff = [3s, 6s, 15s]` — 계획서 §6.6 "3s→6s→최대 15s"와 일치 ✅
- `SessionsPoll` 모델: `snapshot`/`stale`/`lastOkAt`/`error` — stale 배너에 필요한 정보 전부 포함 ✅
- 기본 폴링 간격 4초(계획서는 3초 권장) — 경미한 차이이나 계획서가 "기본 3초, 설정 가능"이라 했으므로 구현 재량 범위 내
**평가**: ✅ D6 계약 정확히 구현. UI가 null/blank dashboard를 보지 않도록 보장.
### 3.4 `session_service.dart` — status.sh --json 래핑 (§2, Rev.1 §1)
**검증**:
- `runCommand(['bash', statusScriptPath, '--json'], timeout: 5s)` — 조회 5초 타임아웃(§6.1) ✅
- `timedOut`/`rc != 0`/`jsonDecode` 실패 시 `StatusFetchException` throw — `StatusRepository`가 이를 catch해 stale 처리 ✅
- `decoded is! Map<String, dynamic>` 타입 가드 ✅
- "이 코드는 YAML/SQLite/jsonl을 직접 읽지 않는다" — `status.sh --json` 출력만 소비, Rev.1 §1 원칙 준수 ✅
**평가**: ✅ 단일 진실 공급원 원칙 준수.
---
## 4. UI 계층 검증 (§7 화면 설계)
### 4.1 `main.dart` — DashboardScreen (§7 Sessions 대시보드)
**검증**:
- `ProviderScope` + `ConsumerWidget` — Riverpod 상태관리 (§5.1) ✅
- `sessionsPollProvider` StreamProvider 구독 → `pollAsync.when(data/loading/error)`
- Master-Detail 레이아웃: `SessionTable`(flex:3) + `DetailPane`(width:380) ✅ (§7)
- `_ErrorScreen` — 폴링 시작 실패 시 에러 화면 ✅
- `StaleBanner` — stale 상태 표시 ✅ (D6)
### 4.2 `session_table.dart` — DataTable2 (§7)
**검증**:
- `data_table_2` 사용 (§5.1 스택 선정) ✅
- 컬럼: `NAME/SERVER/YAML/TMUX/CMD/RESUME/JOB_ID/JOB_STATUS/DRIFT` — 계획서 §7 "Rev.1 §4.1과 동일 컬럼 셋" 정확히 일치 ✅
- 행 선택(`onTap``onSelect`) → `selectedSessionNameProvider` 업데이트 ✅
- `_StatusChip`/`_TmuxChip` — 상태별 색상 코딩(running=success, dead=danger) ✅
- 빈 상태 처리(`empty:` widget) ✅
### 4.3 `detail_pane.dart` — Detail Pane (§7)
**검증**:
- `SessionRow?` null 처리 → `_EmptyDetail`("Select a session") ✅
- PANE 섹션: pid/cwd/cmd/cmd_full ✅
- ATTACH 섹션: attach_command/start_command + 복사 버튼(`Clipboard.setData`) ✅ (§4 "복사 버튼" 요구사항)
- STATUS 섹션: last_visible_status/resume_state/job_id/job_status/drift_classes ✅
- `SelectableText` — 텍스트 선택 가능 ✅
- `_Header` — 세션명 + 상태 pill(status/tmux/role/server) ✅
### 4.4 `stale_banner.dart` — D6 stale 배너 (§6.6)
**검증**:
- `poll.stale` false → `SizedBox.shrink()` (숨김) ✅
- stale true → 경고 배너 "⚠ status snapshot stale (last ok: HH:MM:SS)" ✅
- `lastOkAt` 포맷팅(HH:MM:SS) ✅
### 4.5 `status_script_locator.dart` — 스크립트 경로 해석
**검증**: `.git` 마커로 repo root walk-up → 고정 경로 하강. 하드코딩 절대경로 없음. `flutter run` 실행 디렉터리 무관 robustness ✅. 계획서가 명시하지 않았으나 구현 품질 향상(Rev.1 §8 "no hardcoded absolute path" 원칙 계승).
### 4.6 `session_providers.dart` — Riverpod wiring
**검증**: `sessionServiceProvider``statusRepositoryProvider``sessionsPollProvider` 계층적 의존성 주입 ✅. `apps/mam_desktop`이 폴링/백오프 로직을 재구현하지 않고 `mam_core`에 위임 ✅ (§6.6 "Framework agnostic" 원칙).
---
## 5. DoD (§10) 실증 검증
계획서 §10의 12개 DoD 항목 중 M1 범위에서 검증 가능한 항목들을 실제로 실행 검증:
| DoD | 항목 | 검증 방법 | 결과 |
|-----|------|----------|------|
| 1 | `status.sh` 회귀 없음 (텍스트 모드 byte-identical) | old vs new text output diff | ✅ PASS (타임스탬프만 차이, 본문 동일) |
| 2 | 비파괴 검증 (mam_core/pty에 파일 쓰기/삭제 없음) | `grep -rn` | ✅ PASS (코드 전무) |
| 3 | 명령 주입 방어 (`runInShell: true` 금지) | `grep -rn 'runInShell'` | ✅ PASS (`runInShell: false`만 존재) |
| 9 | 정적 분석 (`dart analyze` clean) | `dart analyze` 실행 | ✅ PASS (No issues found!) |
| 11 | 회귀 없음 (stop/create/resume/monitor/lib.sh 무변경) | `git diff --stat` | ✅ PASS (status.sh만 변경) |
| — | Dart 단위 테스트 | `dart test` 실행 | ✅ PASS (3/3 All tests passed!) |
**DoD-1 상세 (jq diff 대체 검증)**: 기존 5개 키(timestamp/yaml_path/tmux_sessions_alive/tmux_confirmed/drifts) + actions 키가 신규 `sessions_detail` 추가 전후로 동일함을 라이브 실행으로 확인. `sessions_detail`은 순수 additive.
**테스트 커버리지** (`session_service_test.dart`):
1. `SessionsSnapshot.fromJson` well-formed payload 파싱 — drift 클래스, role, resume_state, pane.pid, attach_command 전부 정확히 매핑 ✅
2. 누락 필드 허용(`{"name": "bare"}`) — 기본값(`?`/`-`/null) 적용 ✅
3. 실제 `status.sh --json` 출력 파싱 — 라이브 연동 검증 ✅
**평가**: ✅ M1 범위 DoD 전부 충족. 테스트는 실제 `status.sh` 라이브 연동까지 검증하여 매우 견고함.
---
## 6. 코드 품질 관찰 (NON-BLOCKING — PASS에 영향 없음)
아래 항목들은 통과를 막는 결함이 아니며, 향후 마일스톤에서 고려하면 더 견고해지는 사항이다.
1. **폴링 간격 (선택)**: 계획서 §6.6/§7이 "기본 3초"를 권장했으나 `StatusRepository` 기본값이 4초(`pollInterval: Duration(seconds: 4)`). 경미한 차이이며 계획서가 "설정 가능"이라 명시했으므로 구현 재량 범위. 향후 사용자 피드백에 따라 조정 가능.
2. **`sessions_detail` additive 필드 (주의 권고)**: `pane_pid`/`cmd_full`/`start_command`/`last_visible_status` 4개 필드가 계획서 §3.1의 D8 예시 스키마를 초과해 추가됨. 코드 주석이 "Additive beyond the D8 example — needed by the M1 Detail Pane"이라 명시했으므로 의도적 확장이며, `SessionRow.fromJson`이 이를 안전히 파싱(누락 시 null). 회귀 위험 없음. 단, 향후 `status.sh` 출력 스키마를 문서화할 때 이 4개 필드도 계획서에 갱신하면 추적성 향상.
3. **`status_script_locator.dart` 예외 메시지 (선택)**: `.git` 디렉터리를 못 찾았을 때 "Run mam_desktop from within the multi-agent-mux repo checkout"이라는 안내가 명확. 다만 submodule/worktree 환경에서 `.git`이 파일인 경우(`.git` 디렉터리가 아님)를 고려하면 더 robust해짐. (현재 환경에서는 이슈 없음)
4. **`_StatusChip` switch 표현식 (선택)**: `case 'stopped': case 'terminated': case 'archived':` fallthrough가 의도한 대로 동작하나, Dart 3 switch 표현식에서 여러 case가 연속일 때 가독성이 약간 떨어질 수 있음. 기능적으로 정확하므로 스타일 선호 영역.
---
## 7. AGENTS.md 원칙 준수 검증
- **Surgical Changes (§3)**: 변경이 M1 대시보드/Detail Pane + D8 `status.sh` 확장에만 국한. 기존 스크립트(stop/create/resume/monitor/lib.sh) 무변경. `git diff --stat`로 확인 ✅
- **Simplicity First (§2)**: `mam_core`(순수 Dart) + `mam_desktop`(Flutter) 관심사 분리. `command_runner.dart` 유일 실행 지점으로 과잉 추상화 없음. 각 모델 클래스 단일 책임 ✅
- **Goal-Driven Execution (§4)**: §10 DoD 항목 전부 관측 가능(grep/diff/dart test/dart analyze). 라이브 실행实证으로 회귀 없음 입증 ✅
- **문서-코드 정합성**: 계획서 §3.1 D8 스키마 ↔ `status.sh` `sessions_detail` 출력 ↔ `SessionRow.fromJson` 매핑 — 3계층 전부 field-for-field 일치 ✅
---
## 8. 종합 평가
커밋 `2eb8586`는 계획서(Rev.3)의 M1 마일스톤(읽기 전용 대시보드 + Detail Pane)을 충실하게 구현했다. 핵심 성과:
1. **D8 additive 스키마 확장 정확 구현**: `status.sh --json`이 기존 6개 키를 무변경으로 보존하면서 `sessions_detail` 신규 키를 추가. 라이브 실행实证으로 기존 소비자 회귀 없음을 확인했으며, 텍스트 모드는 byte-identical(타임스탬프만 차이).
2. **불변 안전 계약 정확 이식**: `command_runner.dart`가 D5(명령 주입 방지, `runInShell: false`) + D-Critical(purge `killOnTimeout: false` + `backgroundFuture`) + §6.1 타임아웃(조회 5초)을 정확히 구현. `status_repository.dart`가 D6(stale 스냅샷 유지 + 백오프 3s→6s→15s)을 충족.
3. **모노레포 관심사 분리**: `mam_core`(순수 Dart, Flutter 비의존)가 데이터/서비스 계층을 담당하고 `mam_desktop`이 Riverpod으로 wiring — 계획서 §5.2 구조 정확히 반영.
4. **견고한 테스트**: 3개 단위 테스트(파싱 정확성 + 누락 필드 허용 + 실제 `status.sh` 라이브 연동) 전부 통과. `dart analyze` No issues found.
5. **회귀 없음**: 기존 셸 스크립트(stop/create/resume/monitor/lib.sh) 전부 무변경, `status.sh``--json` 분기 내부에만 additive 변경.
개선 권고 4건은 모두 NON-BLOCKING(구현 재량/스타일/향후 문서화)으로 통과 판정에 영향을 주지 않는다. 코드는 계획서에 입각해 안전하고 모순 없이 구현되었다.
[VERDICT: PASS]
@@ -1,41 +0,0 @@
# 리뷰 리포트 — Job dbab0e07
- **리뷰 대상**: 커밋 `36b3910` — (1) `create_session.sh` agy 인증 사전검증을 파일 기반으로 우회해 macOS 키체인 접근 Hang 방지, (2) `lib.sh provision_isolation()`에 Darwin 전용 `~/Library/Keychains` 심링크 시딩 추가로 격리 모드 인증 토큰 소실 해결
- **리뷰어**: claude (planner-reviewer)
- **리뷰 방식**: 정적 분석(bash -n, shellcheck 기준선 대비) + 계측 스텁/가짜 HOME/uname 오버라이드 기반 실행 검증
## 1. 설계 타당성
- **Hang 우회**: agy 격리 lever가 `HOME=<root>`이고(lib.sh 주석의 Phase 0 실측 매트릭스), macOS에서 `agy models`가 키체인 접근 프롬프트로 비대화식 환경에서 블로킹되는 문제를, 디스크상 토큰 파일(`~/.gemini/oauth_creds.json` 또는 `~/.gemini/antigravity-cli/antigravity-oauth-token`) 존재 시 CLI 호출 자체를 생략하는 방식으로 회피 — 검사 파일 경로 2개가 `provision_isolation()`이 agy 자격증명으로 시딩하는 파일 목록과 정확히 일치함(저장소 내부 지식과 정합).
- **토큰 소실 해결**: 격리 시 `HOME=<root>`로 바뀌면 macOS 키체인 경로(`$HOME/Library/Keychains`)가 빈 격리 홈을 가리켜 자격증명 조회가 실패하는 구조 — 실제 Keychains 디렉터리를 심링크로 시딩하는 것은 이 파일의 기존 철학("auth/config files are SYMLINKED ... never copied — token refresh must converge on the real files")과 일치하는 올바른 해법.
## 2. 실행 검증 (전부 실측)
- **사전검증 우회(Case A)**: 가짜 HOME에 토큰 파일 배치 + 호출 기록 스텁 `agy`를 PATH 선두에 두고 `create_session.sh --dry-run --agent agy` 실행 → **`agy` 바이너리가 단 한 번도 실행되지 않음**(Hang 원인 원천 제거 확인), exit 0.
- **폴백 보존(Case B)**: 토큰 파일 없는 빈 HOME → `agy models`가 정확히 1회 호출되고 스텁 실패 시 기존 오류 메시지("agy is not authenticated")와 exit 1이 그대로 동작 — 미인증 조기 차단 시맨틱 유실 없음.
- **Keychains 시딩**: lib.sh를 소싱한 격리 하네스에서 `uname`을 Darwin으로 오버라이드하고 가짜 HOME(`Library/Keychains/login.keychain-db` 포함)으로 `provision_isolation agy` 실행 →
- 심링크 정상 생성, 격리 홈 경유 read-through로 실제 키체인 데이터 접근 확인.
- `seeded` 출력에 `Library/Keychains`가 기존 포맷대로 병합됨.
- **재프로비저닝 멱등성**: 2회 실행에도 `ln -sfn``-n` 덕에 중첩 링크(`Keychains/Keychains`) 없이 동일 결과.
- **🔑 삭제 안전성(최중요)**: create rollback의 `rm -rf "$ISOLATION_ROOT"` 시뮬레이션 → **심링크만 제거되고 실제 키체인 파일은 온전히 생존**함을 실측 확인(rm -rf는 심링크를 따라 들어가지 않음). `seeded` 목록을 순회하며 삭제하는 소비자는 코드베이스에 존재하지 않음(생성·기록 전용)도 grep으로 확인.
## 3. 정적 분석
- `bash -n` 양 파일 통과. `shellcheck -S warning`: 변경 전 기준선(fc24af4) 대비 양 파일 모두 **경고 0건 → 0건, 신규 경고 없음**.
## 4. 유실 검사
- agy 외 에이전트(claude/cline/hermes)의 provision 분기·사전검증 분기는 바이트 단위로 무변경. Darwin 가드로 Linux에서 Keychains 시딩 완전 스킵(Linux 회귀 없음).
## 5. 비차단(Non-blocking) 지적 사항
1. **사전검증 약화** — 파일 존재가 토큰 유효성을 보증하지 않으므로, 만료/폐기된 토큰은 이제 preflight를 통과하고 TUI 기동 단계에서야 실패가 드러남. Hang 대비 합리적 트레이드오프이나 오류 표면화 시점이 늦어짐.
2. **Darwin 미게이팅** — 우회 분기가 OS 무관하게 적용되어, Hang이 없던 Linux에서도 엄격 검사가 생략됨(부수적으로 네트워크 호출 생략이라 빨라지는 이점은 있음). 엄격성이 중요해지면 `uname` 게이트 추가 고려.
3. **자격증명 격리 부재(의도된 설계)** — 격리 세션이 실제 키체인을 공유하게 되나, 시딩의 목적 자체가 인증 공유이므로 기존 심링크 시딩 철학과 일치. 기록 차원의 언급.
4. **macOS 실기기 미검증** — Security.framework가 심링크된 `$HOME/Library/Keychains`를 실제로 수용하는지는 Linux 환경에서 실측 불가. 메커니즘 수준(경로 해석·링크·멱등성·삭제 안전성)은 전부 검증 완료.
## 6. 결론
두 수정 모두 고장 메커니즘을 정확히 겨냥했고, 우회·폴백·시딩·멱등성·삭제 안전성이 전부 실행으로 입증되었으며 정적 분석 신규 경고와 기존 동작 유실이 없다. 비차단 4건은 후속 개선/기록 수준이다.
[VERDICT: PASS]
@@ -1,32 +0,0 @@
# Peer Review: `multi-agent-mux-loop` SKILL 문서 정비 + `run_loop.sh` 하드코딩 제거 (커밋 `52c270e`/`f85fdfc`/`6c90342`)
## Scope
세 커밋을 검토했다: (1) `52c270e` — SKILL.md의 "Self-Planning"(계획 완전 생략) 서술을 "Existing Plan Execution"(기존 승격 계획서 로드 후 즉시 구현)으로 정정, (2) `f85fdfc` — SKILL.md 내 하드코딩된 세션명(`canary-projects-multi-agent-mux-*`)을 플레이스홀더(`<planner-session-name>`/`<creator-session-name>`/`<reviewer-session-name-N>`)로 치환, (3) `6c90342``run_loop.sh``resolve_planner_session()` 폴백과 기존 계획 파일 경로를 실제로 동적화. 문서 변경(1, 2)은 렌더링/의미 정합성 위주로, 셸 스크립트 변경(3)은 문법·동작 검증 위주로 리뷰했다.
## 1, 2. SKILL.md 문서 변경 검토
- **`52c270e`**: `--plan` 미지정 시의 실제 동작(계획서 승격 파일을 로드해 즉시 구현 착수)과 서술("Self-Planning", "계획을 거치지 않고 직접 구현")이 이전엔 어긋나 있었다 — 실제로는 완전한 무계획 실행이 아니라 "기존 계획서가 있으면 그걸 쓴다"는 동작이므로, 이번 수정으로 프로즈/표/mermaid 다이어그램의 분기 라벨("Use Existing Plan (No --plan)")이 셋 다 일관되게 정정되었다. 세 위치(설명 불릿, 표, 다이어그램) 모두 누락 없이 반영됨을 확인.
- **`f85fdfc`**: 하드코딩된 세션명이 매뉴얼 예시 곳곳(다이어그램 참가자 라벨, `--target-agent`/`--reviewer` 예시 값)에 있었는데, 전부 제네릭 플레이스홀더로 치환됨. `grep -n "canary-projects-multi-agent-mux" SKILL.md` 기준으로 잔여 하드코딩이 없는지 확인했다(아래 §3 참고 — 실제로는 no-arg `--plan`을 하드코딩 언급 없이 완전히 정리했음을 확인).
두 커밋 모두 마크다운/mermaid 문법 오류 없이 코드펜스와 표 구조를 그대로 유지했다.
## 3. `run_loop.sh` 변경 검토 (실행 검증 포함)
### 변경 내용
- `resolve_planner_session()`의 폴백 값이 `'canary-projects-multi-agent-mux-planner-reviewer-claude'`(하드코딩)에서 `''`(빈 문자열)로 변경 — 이제 `role``'planner'`를 포함하는 tmux 세션을 찾지 못하면 특정 프로젝트 이름으로 잘못 추측하지 않고 정직하게 "찾지 못함"을 반환한다.
- 기존 계획서 로드 블록(`else` 분기, 314-322행)이 `EXISTING_PLAN_FILE=".agents/reports/canary-projects-multi-agent-mux-planner-reviewer-claude/report-final.md"`(하드코딩)에서 `PLANNER_SESSION` 기반 동적 경로로 변경되고, `[ -n "$PLANNER_SESSION" ]` 가드가 추가되어 세션을 못 찾은 경우 경로 조합 자체를 건너뛴다.
### 검증
- `bash -n run_loop.sh` → 문법 오류 없음.
- `shellcheck run_loop.sh` → 경고/오류 0건(종료 코드 0).
- **`resolve_planner_session()`을 실제로 발췌·소싱해 현재 라이브 상태에 대해 실행**: `canary-projects-multi-agent-mux-planner-reviewer-claude`를 정확히 반환함(현재 이 세션의 role이 `planner-reviewer`이므로 `'planner' in role` 매치) — 우연이 아니라 실제 동작 확인. 이어서 이 값으로 조합된 경로(`.agents/reports/canary-projects-multi-agent-mux-planner-reviewer-claude/report-final.md`)가 실제로 파일시스템에 존재함을 확인해, 이 프로젝트에서는 하드코딩 시절과 동일한 결과를 내면서도 이제는 진짜로 동적임을 증명했다.
- **빈 `PLANNER_SESSION` 엣지 케이스**(플래너 역할 세션이 아예 없는 워크스페이스를 시뮬레이션): 동일한 `set -euo pipefail` 하에서 새 로직 스니펫만 분리 실행 → `EXISTING_PLAN_FILE`이 빈 문자열로 남고 "계획 로드 건너뜀" 분기가 정상 작동, `set -u`(nounset)로 인한 미정의 변수 오류도 없음(`PLANNER_SESSION`은 항상 대입되므로 빈 문자열이어도 unset이 아님) — 하드코딩이 없어진 대신 도입될 수 있었던 "다른 워크스페이스에서 조용히 깨짐" 위험이 실제로는 없음을 확인.
- `--plan` 모드 경로(224-228, 285-286, 496-497, 543-548행)의 `$PLANNER_SESSION` 사용처는 이번 diff의 대상이 아니며, 플래너 세션이 비어 있을 경우 `multi-agent-mux-delegate-job submit`이 초반에 실패로 이어지는 fail-fast 구조라 이번 변경으로 인한 새로운 침묵 실패 경로는 없다.
- `git diff 7c94eef 6c90342 --stat` → 이 세 커밋이 건드린 파일은 `SKILL.md``run_loop.sh` 딱 둘뿐, 회귀 없음.
## 결론
문서 두 건은 실제 동작과 서술의 불일치를 바로잡고 하드코딩된 예시를 제네릭화한 정확한 수정이며, 셸 스크립트 변경은 실제로 실행해 정상 케이스(현재 세션 정확히 해석)와 엣지 케이스(플래너 세션 부재 시 안전한 스킵) 모두를 검증했다. 문법 오류, shellcheck 경고, 회귀 모두 없다.
[VERDICT: PASS]
@@ -1,40 +0,0 @@
# Peer Review (Round 5, 최종): `exit`→`_exit` 심볼 정정 (commit `f0e2bd2`) — `multi-agent-mux-ui` M2
## Scope
`604fdecf` 리뷰에서 지적한 마지막 1건 — `cExit``lookupFunction<...>('exit')`로 잘못된(async-signal-unsafe) libc 심볼에 바인딩되어 있던 문제 — 에 대한 수정 커밋 `f0e2bd2`("fix(ui): correct libc symbol lookup for direct _exit syscall to achieve async-signal-safety")를 검토했다.
## 변경 확인
`pty_session.dart:111`, 문자열 리터럴 한 글자(정확히는 언더스코어 하나) 수정:
```diff
- final cExit = libc.lookupFunction<_exit_c, _exit_dart>('exit');
+ final cExit = libc.lookupFunction<_exit_c, _exit_dart>('_exit');
```
이 파일에 대한 이번 커밋의 변경은 이 한 줄이 전부다(그 외 diff는 `.dart_tool` 빌드 캐시 바이너리뿐).
## 실행 검증
1. **심볼 재확인**: `libc.lookup('_exit').address`가 이제 실제로 `cExit`가 가리키는 주소와 일치함을 별도 스크립트로 재확인(이전 라운드에서 `'exit'`/`'_exit'`가 서로 다른 주소임을 이미 확정했던 것과 대조).
2. **실제 실패 경로 재현**: 존재하지 않는 실행파일(`this-binary-does-not-exist-xyz`)로 `PtySession.start()`를 호출해 `execvp()` 실패 → `cExit(-2)` 경로를 실제로 타게 만들었다. 결과: `start()`는 15ms 만에 정상 반환했고, 자식 프로세스는 **100ms 이내에 완전히 사라짐**(`ps`로 확인, 좀비도 아니고 행도 아님) — 이전 라운드에서 우려했던 "잘못된 심볼로 인한 잠재적 행/불안정 종료" 없이 자식이 즉시, 깨끗하게 종료됨을 확인.
3. **회귀 테스트**: `mam_pty``pty_runtime_test.dart`에 이번 리뷰 체인 동안 검증해온 항목에 대응하는 자동화 테스트가 추가되어 있음을 확인 — echo 케이스에 더해 **"PtySession strips TMUX/TMUX_PANE from child environment"** 테스트가 신규로 존재하며 통과한다. 이제 이전까지 매 라운드 내가 수작업 스크래치 스크립트로 검증해야 했던 env 격리가 저장소 자체의 회귀 테스트로 편입되었다.
4. **전체 회귀 스위트**: `dart analyze`(mam_pty/mam_core) + `flutter analyze`(mam_desktop) 전부 clean. `dart test`(mam_pty 2/2, mam_core 3/3) + `flutter test`(mam_desktop 3/3) 전부 통과.
5. **DoD**: `git show f0e2bd2 --stat -- '*.sh'` → 셸 스크립트 변경 없음. `grep -rn "runInShell: *true"` → 없음. `grep -rn "\.writeAsString\|\.writeAsBytes\|\.delete(\|openWrite("`(mam_core/mam_pty) → 없음.
## M2 전체 검증 이력 요약 (이번 라운드로 완결)
이 마일스톤은 5라운드에 걸쳐 검토되었고, 매 라운드 실제 실행으로 재현/반증했다:
| 라운드 | 커밋 | 발견 | 상태 |
| :-- | :-- | :-- | :-- |
| 1 (`c2503ed6`) | `b7901bc` | `/proc/self/fd/` 즉시 예외, PTY 슬레이브 미연결, env 격리 없음 | NOT PASS |
| 2 (`1fc02bc2`) | `f52f6eb` | 위 3건 해결(fork/exec 재작성) — 좀비 누수, fork-unsafe 호출 신규 발견 | NOT PASS |
| 3 (`a3f7449e`) | `7781e79` | fork-unsafe 부분개선 — 이벤트루프 정지(가장 심각), env 격리 죽은 코드 신규 발견 | NOT PASS |
| 4 (`604fdecf`) | `7f1a7e5`(+`a6e4dc9`) | 이벤트루프 정지/env 격리/좀비회수 전부 해결 — `exit``_exit` 심볼 오류 발견 | NOT PASS |
| 5 (본 리뷰) | `f0e2bd2` | 심볼 오류 정정, 실패 경로 실행 재현으로 정상 종료 확인 | **PASS** |
계획서 §5.2(구조)/§6.7(PTY 메커니즘, env 격리, TOCTOU, 리사이즈)/§10(DoD)의 요구사항이 모두 실제 실행 검증을 통과했고, 더 이상 미해결 항목이 없다.
[VERDICT: PASS]
@@ -1,34 +0,0 @@
# Peer Review (Round 3): 콜드스타트 에러 침묵 버그 수정 (commit `7e4cab6`) — `multi-agent-mux-ui`
## Scope
`50ed0559` 리뷰에서 지적한 잔여 결함 — "콜드스타트(한 번도 성공한 적 없는 폴링 실패)가 여전히 완전히 침묵됨, `stale_banner.dart:15``if (!poll.stale) return shrink` 게이트가 원인" — 에 대한 수정 커밋 `7e4cab6`("fix(ui): expose stale banner under cold-start failures when no successful snapshot exists")를 검토했다.
## 변경 내용 확인
`stale_banner.dart` 5줄 변경(그 외 파일은 무관한 dart_tool 캐시 바이너리 1개뿐):
```dart
final shouldShow = poll.stale || (poll.snapshot == null && poll.error != null);
if (!shouldShow) return const SizedBox.shrink();
...
final lastOkText = lastOk == null
? 'never'
: '...'
```
내가 `50ed0559`에서 제안한 수정안과 조건식이 정확히 일치한다 — `poll.stale`뿐 아니라 `poll.snapshot == null && poll.error != null`(콜드스타트: 한 번도 성공하지 못했지만 에러는 있는 상태)도 노출 조건에 포함시켰고, `lastOkAt == null`일 때 문구도 의미 없는 시각 대신 `'never'`로 분기했다.
## 검증
1. **경로 추적**: `main.dart``_DashboardBody``snapshot`이 null이어도 `StaleBanner(poll: poll)`를 항상 마운트한다(`sessions`/`count`는 각각 `?? const []`/`?? 0`로 안전 처리) — 배너 표시 조건이 고쳐지면 실제로 화면에 그려질 경로가 이미 존재함을 재확인.
2. **스트림 도달성**: `StatusRepository.watch()`는 모든 폴링 실패를 내부에서 흡수해 항상 `SessionsPoll`을 yield하므로(예외를 스트림 밖으로 던지지 않음), Riverpod `sessionsPollProvider`는 첫 실패 시에도 `AsyncError`가 아니라 `AsyncData(poll)`로 즉시 전이 — `DashboardScreen``_ErrorScreen`이 아니라 `_DashboardBody`(그리고 그 안의 `StaleBanner`)로 정상 도달함을 재확인.
3. **실제 렌더링 재현(직접 실행)**: `SessionsPoll(snapshot: null, stale: false, lastOkAt: null, error: 'StatusFetchException: preflight failed: tmux is missing or not executable')``StaleBanner`를 단독 렌더링하는 위젯 테스트를 임시 작성해 `flutter test`로 직접 실행 — `'never'` 텍스트와 에러 메시지(`'tmux is missing'`) 문자열이 모두 실제로 화면에 렌더링됨을 확인(테스트는 검증 후 삭제, 저장소에는 남기지 않음 — 리뷰 산출물 오염 방지). 이전 라운드(`50ed0559`)에서 재현했던 "배너가 전혀 뜨지 않는" 상황이 이제 재현되지 않는다.
4. **회귀 없음**: `dart analyze`(mam_core)/`flutter analyze`(mam_desktop) 모두 No issues found. 기존 6개 테스트(`mam_core` 3 + `mam_desktop` 3) 전부 통과.
5. **스코프 확인**: 이번 커밋은 `stale_banner.dart` 한 파일만 수정 — 이전 라운드에서 요청한 "좁은 범위 수정" 요구와 정확히 일치, 다른 파일에 부작용 없음.
## 결론
`765e2329`(pre-flight 체크 누락) → `50ed0559`(수정이 잘못된 조건 분기에 적용됨) → 이번 `7e4cab6`까지 이어진 콜드스타트 에러 침묵 버그가 정확한 근본 원인(단일 `if` 게이트)에 대한 정밀 수정으로 완전히 해소되었다. 실제 위젯 렌더링까지 직접 실행해 확인했고, 회귀도 없다. M1 스코프에서 더 이상 남은 이슈가 없다.
[VERDICT: PASS]
@@ -1,162 +0,0 @@
# ✅ Peer Review Report: macOS 키체인 Hang 우회 및 격리 모드 인증 토큰 소실 수정 (Job 14943484)
**Job**: `14943484` · **Reviewer**: Reviewer B (Cline, `canary-projects-multi-agent-mux-reviewer-cline`)
**Review Target**: 커밋 `36b3910` "fix(mac-compat): bypass keyring auth check hang and link macOS Library/Keychains to isolated home"
**Files Changed**: `lib.sh` (+8/-0), `create_session.sh` (+4/-1) — 2 files, 12 insertions, 1 deletion
**Review Scope**: 작업 목표 "create_session.sh 및 lib.sh에서 macOS 키체인(keyring) 접근 차단으로 인한 비대화식 Hang 현상과 격리 모드(--isolate) 시 인증 토큰 소실 문제를 각각 파일 기반 사전 검증 우회 및 Library/Keychains 폴더 링크 추가를 통해 해결" — 린트, 동작성, 유실 관점 교차 리뷰
**Method**: 커밋 diff 분석 + `bash -n`/`shellcheck` 정적 분석 + 인증 바이패스 로직 4케이스 검증 + Darwin 가드 검증 + 경로 일치성 확인 + seeded 패턴 일관성 확인 + 타 agent keychain 필요성 분석
---
## 1. 변경 사항 개요
### 1.1 파일 기반 사전 검증 우회 (create_session.sh 라인 92-98)
```diff
elif [ "$AGENT" = "agy" ]; then
- if ! agy models >/dev/null 2>&1; then
+ # Fast, non-blocking check: if token or credentials exist on disk, assume authenticated to prevent keyring hang
+ if [ -f "$HOME/.gemini/oauth_creds.json" ] || [ -f "$HOME/.gemini/antigravity-cli/antigravity-oauth-token" ]; then
+ true
+ elif ! agy models >/dev/null 2>&1; then
echo "ERROR: agy is not authenticated. Please log in first." >&2
exit 1
fi
```
**목적**: `agy models` 명령이 macOS에서 키체인 접근 시 비대화식 Hang 유발. 토큰/자격증명 파일 존재 시 파일 기반으로 인증 가정하여 Hang 우회.
### 1.2 Library/Keychains 폴더 링크 추가 (lib.sh 라인 841-848)
```diff
+ # On macOS, seed ~/Library/Keychains to allow isolated agy to query Keychain Access credentials
+ if [ "$(uname)" = "Darwin" ]; then
+ mkdir -p "$root/Library"
+ if [ -d "$HOME/Library/Keychains" ]; then
+ ln -sfn "$HOME/Library/Keychains" "$root/Library/Keychains"
+ seeded="${seeded:+$seeded,}Library/Keychains"
+ fi
+ fi
```
**목적**: `--isolate` 모드 시 격리된 홈 디렉토리에 `~/Library/Keychains` 심볼릭 링크 추가 → 격리 agy가 Keychain Access 자격증명 조회 가능.
---
## 2. 작업 목표 달성도
| 목표 | 상태 | 확인 |
|------|------|------|
| macOS 키체인 Hang 우회 (파일 기반 사전 검증) | ✅ | 토큰 파일 존재 시 `agy models` 스킵 |
| 격리 모드 인증 토큰 소실 해결 (Keychains 링크) | ✅ | Darwin 가드 + Library/Keychains 심볼릭 링크 |
| create_session.sh 적용 | ✅ | 라인 92-98 |
| lib.sh 적용 | ✅ | 라인 841-848 (agy case) |
---
## 3. 정적 분석
| 파일 | bash -n | shellcheck | 비고 |
|------|---------|------------|------|
| lib.sh | ✅ SYNTAX OK | ✅ 경고 없음 (clean) | 본 diff 새 경고 0건 |
| create_session.sh | ✅ SYNTAX OK | SC1091 (info, 기존 source) — **본 diff 새 경고 없음** | EXIT 1 (기존) |
---
## 4. 동작성 검증
### 4.1 ✅ 인증 바이패스 로직 4케이스 검증
| 케이스 | 조건 | 결과 | 판정 |
|--------|------|------|------|
| 1 | `antigravity-oauth-token` 파일 존재 | BYPASS (token found) | ✅ Hang 우회 |
| 2 | `oauth_creds.json` 파일 존재 | BYPASS (oauth_creds found) | ✅ Hang 우회 |
| 3 | 파일 없음 + agy models 실패 | ERROR (not authenticated) | ✅ 정상 에러 |
| 4 | 파일 없음 + agy models 성공 | PASS (agy models succeeded) | ✅ 정상 통과 |
**검증**: 파일 존재 시 `agy models` 호출 스킵 → macOS 키체인 Hang 방지. 파일 부재 시 기존 `agy models` 체크 유지 → 미인증 감지.
### 4.2 ✅ Darwin 가드 검증 (Keychains 링크)
| 조건 | 결과 | 판정 |
|------|------|------|
| `uname` = Linux | Darwin 체크 실패 → 블록 스킵 | ✅ Linux에서 Keychains 링크 미생성 |
| `uname` = Darwin + `~/Library/Keychains` 존재 | `mkdir -p $root/Library` + `ln -sfn` 실행 | ✅ macOS에서 심볼릭 링크 생성 |
| `uname` = Darwin + `~/Library/Keychains` 부재 | `[ -d ]` 실패 → 링크 미생성 | ✅ graceful (seeded 미추가) |
### 4.3 ✅ 경로 일치성 (auth check vs provisioning)
| 파일 | create_session.sh 체크 경로 | lib.sh provisioning 경로 | 일치 |
|------|---------------------------|-------------------------|------|
| oauth_creds.json | `$HOME/.gemini/oauth_creds.json` | `$HOME/.gemini/oauth_creds.json` (라인 835) | ✅ |
| antigravity-oauth-token | `$HOME/.gemini/antigravity-cli/antigravity-oauth-token` | `$HOME/.gemini/antigravity-cli/antigravity-oauth-token` (라인 838) | ✅ |
인증 체크 파일과 격리 provisioning 파일 경로가 완전 일치 → 일관성 확보.
### 4.4 ✅ seeded 패턴 일관성
`seeded="${seeded:+$seeded,}Library/Keychains"` (라인 846) — 기존 패턴(라인 836, 839, 853)과 동일한 `${seeded:+$seeded,}` 누적 패턴. 일관성 확보 ✅
### 4.5 ✅ 타 agent keychain 필요성 분석
| Agent | 인증 방식 | Keychain 필요 | Keychains 링크 적용 |
|-------|----------|---------------|---------------------|
| claude | `.credentials.json` 파일 기반 | 아니오 | 불필요 (맞음) |
| cline | 파일 기반 settings + DB | 아니오 | 불필요 (맞음) |
| agy | macOS Keychain Access | **예** | **적용됨** ✅ |
| hermes | `auth.json` 파일 기반 | 아니오 | 불필요 (맞음) |
Keychains 링크가 agy case에만 추가된 것은 **정확한 타겟팅** — agy만 macOS Keychain 사용, 타 agent는 파일 기반 인증.
### 4.6 ✅ true 문 유효성
`if` 블록 본문으로 `true` 사용 — bash에서 유효 (no-op). `if true; then true; fi` 검증 통과. 의도: 파일 존재 시 아무 동작 없이 통과(바이패스).
---
## 5. 잔여 결함 (LOW — INFORMATIONAL)
### 5.1 ⚠️ 만료된 토큰 시 false positive 가능성 (LOW, 설계 트레이드오프)
**위치**: create_session.sh 라인 93
**분석**: 토큰 파일이 존재하지만 **만료/무효**한 경우, 바이패스가 `agy models` 체크를 스킵하여 세션 시작 → agy 실행 시 인증 실패 가능.
**평가**: 의도적 트레이드오프 — 원 문제는 **Hang**(무한 대기)이며, 만료 토큰으로 인한 후속 실패는 Hang보다 나음(진단 가능). 주석(라인 92)이 의도 명시.
**심각도**: LOW — BLOCKING 아님. 설계 결정으로 수용 가능.
### 5.2 ️ 작업 트리 잔여 .tmp 파일 (INFO, unrelated)
**위치**: `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job.14657_36745.tmp` (untracked)
**분석**: 이전 delegate_job_safe 실행 잔여물. 본 diff와 무관. 무해하지만 정리 권장.
**심각도**: INFO — 본 리뷰 범위 외.
---
## 6. 종합 평가
### 작업 목표 달성도
"create_session.sh 및 lib.sh에서 macOS 키체인(keyring) 접근 차단으로 인한 비대화식 Hang 현상과 격리 모드(--isolate) 시 인증 토큰 소실 문제를 각각 파일 기반 사전 검증 우회 및 Library/Keychains 폴더 링크 추가를 통해 해결" — **달성**.
### 변경 품질
1.**파일 기반 Hang 우회**: 토큰/자격증명 파일 존재 시 `agy models` 스킵 — 4케이스 검증 모두 PASS
2.**Keychains 심볼릭 링크**: Darwin 가드 + `[ -d ]` 존재 확인 + `ln -sfn` — 안전한 조건부 생성
3.**경로 일치성**: auth check 파일과 provisioning 파일 경로 완전 일치
4.**타겟팅 정확**: agy case에만 Keychains 링크 추가 — 타 agent는 파일 기반 인증으로 불필요
5.**seeded 패턴 일관**: 기존 누적 패턴과 동일
6.**Darwin 가드**: Linux에서 미실행, macOS에서만 동작
### 검증 결과
- 정적 분석: `bash -n` 2/2 OK, `shellcheck` 본 diff 새 경고 없음 (lib.sh clean, create SC1091 기존만) ✅
- 인증 바이패스: 4케이스(토큰 존재/ oauth_creds 존재/ 파일 없음+실패/ 파일 없음+성공) 모두 PASS ✅
- Darwin 가드: Linux 스킵 확인 ✅
- 경로 일치성: auth check ↔ provisioning 완전 일치 ✅
- seeded 일관성: 기존 패턴과 동일 ✅
- 타 agent 분석: agy만 Keychain 사용, 타겟팅 정확 ✅
### 잔여 LOW 1건 + INFO 1건
- LOW 5.1: 만료 토큰 false positive — 의도적 트레이드오프 (Hang > 후속 실패), 주석 명시
- INFO 5.2: 잔여 .tmp 파일 (본 diff 무관)
### 판정 근거
작업 목표(키체인 Hang 우회 + 격리 토큰 소실 해결) 완전 달성. 파일 기반 바이패스 4케이스 검증 PASS, Darwin 가드 동작 확인, 경로 일치성 확보, agy 타겟팅 정확. 정적 분석 통과. 잔여 LOW 1건은 의도적 설계 트레이드오프(Hang 방지가 만료 토큰 후속 실패보다 우선). 주 개발자가 macOS 키체인 문제를 정확히 진단하고 파일 기반 우회 + Keychains 링크로 해결했으므로 PASS 판정이 타당.
[VERDICT: PASS]
@@ -1,221 +0,0 @@
# ✅ Peer Review Report: multi-agent-mux-loop Self-Planning Mode 수정 diff 교차 검증 (Job 417d0efd)
**Job**: `417d0efd` · **Reviewer**: Reviewer B (Cline, `canary-projects-multi-agent-mux-reviewer-cline`)
**Review Target**: 주 개발자(Antigravity)가 제출한 미커밋 git diff — SKILL.md, run_loop.sh, PLAN_LOOP.md 3개 파일 Self-Planning Mode 반영 수정 (3차 시도)
**Prior Context**:
-`71d5a6f2`: 4건 BLOCKING 결함 발견 (DEFECT A/B/C: mermaid `fi` 문법 오류, DEFECT D: PLAN_LOOP.md 하드코딩) → NOT PASS + ESCALATE
-`22e70ce2`: 동일 4건 결함 0/4 해결 (diff가 `fi` 유지) → NOT PASS + ESCALATE
- 본 잡 `417d0efd`: 주 개발자 3차 시도 — 4건 결함 해결 시도
**Review Scope**: 작업 목표 "multi-agent-mux-loop에서 --plan 옵션이 없을 때 계획과 개발을 모두 creator가 수행하는 수정사항(run_loop.sh, SKILL.md, PLAN_LOOP.md의 변경내역)이 올바르게 반영되었는지 확인" — 린트, 동작성, 유실 관점 교차 리뷰
**Method**: 라인 단위 diff 분석 + `bash -n`/`shellcheck` 정적 분석 + **mermaid CLI 11.16.0 렌더링实证** + 이전 결함 추적 비교 + `Loop` 예약어 충돌 근본 원인 분석
---
## 1. diff 개요 (5개 파일, +24/-13)
| 파일 | 변경 | 내용 |
|------|------|------|
| SKILL.md | +14/-6 | (1) "Existing Plan Execution" → "Creator Self-Planning & Development" 설명 (2) planning mermaid 블록 2단계 분기 추가 (3) **review mermaid 블록 `fi`→`end` 교체 (라인 117)** (4) Feedback Loop Cadence Self-Planning 설명 추가 |
| run_loop.sh | +2/-2 | (1) `wait_for_job` 잡 경로 `.mam/jobs/$job_id/job.json``.mam/jobs/$job_id.json` (2) EXECUTION_PROMPT Creator 자율 계획 지시로 변경 |
| PLAN_LOOP.md | +8/-4 | (1) `--target-agent` 하드코딩 → `<creator-session-name>` 플레이스홀더 (2) participant `Planner Claude`/`Creator Claude``Planner Agent`/`Creator Agent` (3) `--plan` 옵션 설명 Self-Planning 추가 (4) **planning mermaid `fi`→`end` 교체 (라인 66)** (5) **review mermaid `fi`→`end` 교체 (라인 84)** |
| dart_tool binary x2 | (무관) | 캐시 파일 — 리뷰 범위 외 |
---
## 2. 이전 4건 BLOCKING 결함 해결 추적 — 4/4 해결 ✅
### 2.1 ✅ DEFECT A (해결): SKILL.md 라인 117 `fi`→`end`
**이전 상태** (잡 71d5a6f2): SKILL.md mermaid review 블록 라인 117에 `fi` → mermaid CLI 파싱 에러
**본 diff**:
```diff
- fi
+ end
```
**현재 상태**: `grep -nc ' fi' SKILL.md` = **0**
**평가**: ✅ 해결. `fi``end`로 정확히 교체됨.
### 2.2 ✅ DEFECT B (해결): PLAN_LOOP.md 라인 66 `fi`→`end`
**이전 상태**: PLAN_LOOP.md mermaid 블록 라인 66에 `fi` → mermaid CLI 파싱 에러
**본 diff**:
```diff
- fi
+ end
+ end
```
**현재 상태**: `grep -nc ' fi' PLAN_LOOP.md` = **0**
**평가**: ✅ 해결. `fi``end`로 교체되고, 상위 `else --plan 미지정` 분기를 닫는 `end` 추가.
### 2.3 ✅ DEFECT C (해결): PLAN_LOOP.md 라인 84 `fi`→`end`
**이전 상태**: PLAN_LOOP.md mermaid review 블록 라인 84에 `fi`
**본 diff**:
```diff
- fi
+ end
```
**현재 상태**: 라인 84 `fi` 제거, `end`로 교체 ✅
**평가**: ✅ 해결.
### 2.4 ✅ DEFECT D (해결): PLAN_LOOP.md 하드코딩 3건 → 플레이스홀더/일반화
**이전 상태**: PLAN_LOOP.md 라인 19, 45, 46에 하드코딩 에이전트명
**본 diff**:
```diff
- --target-agent "canary-projects-multi-agent-mux-creator-claude" \
+ --target-agent "<creator-session-name>" \
- participant Plan as Planner Claude
- participant Dev as Creator Claude
+ participant Plan as Planner Agent
+ participant Dev as Creator Agent
```
**현재 상태**:
- `grep 'creator-claude\|Planner Claude\|Creator Claude' PLAN_LOOP.md` = **0건**
- `grep 'creator-session-name\|Planner Agent\|Creator Agent' PLAN_LOOP.md` = **3건** (플레이스홀더/일반화 확인) ✅
**평가**: ✅ 해결. SKILL.md(`f85fdfc`)와 일관성 확보. 3건 모두 정제.
### 2.5 이전 결함 추적 요약
| 결함 | 이전 상태 | 잡 22e70ce2 후 | 본 diff 후 | 해결? |
|------|-----------|----------------|------------|-------|
| DEFECT A: SKILL.md `fi` | 1개 | 1개 (미해결) | **0개** | ✅ 해결 |
| DEFECT B: PLAN_LOOP.md `fi` (66) | 1개 | 1개 (미해결) | **0개** | ✅ 해결 |
| DEFECT C: PLAN_LOOP.md `fi` (84) | 1개 | 1개 (미해결) | **0개** | ✅ 해결 |
| DEFECT D: PLAN_LOOP.md 하드코딩 | 3건 | 3건 (미해결) | **0건** | ✅ 해결 |
---
## 3. mermaid 렌더링实证 (CLI 11.16.0)
### 3.1 ✅ PLAN_LOOP.md — 렌더링 성공
```
$ npx @mermaid-js/mermaid-cli -i planloop2.mmd -o planloop2.svg
Generating single mermaid chart
→ SVG 생성: 41401 bytes ✅
```
**PLAN_LOOP.md mermaid 블록 구조 분석** (라인 41-92):
```
alt --plan 지정 시 → alt #1 open
loop ... → loop #1 open
end → loop #1 close ✅
else --plan 미지정 → alt #1 else
alt 기존 계획 존재 시 → alt #2 open
else 계획 미존재 시 → alt #2 else
end → alt #2 close ✅
end → alt #1 close ✅ (이전 fi, 이제 end)
loop 최대 --max-loop → loop #2 open
alt 리뷰어 옵션 지정 시 → alt #3 open
alt 100% PASS 충족 시 → alt #4 open
else NOT PASS 검출 시 → alt #4 else
end → alt #4 close ✅
else 리뷰어 미지정 → alt #3 else
end → alt #3 close ✅ (이전 fi, 이제 end)
end → loop #2 close ✅
alt --cleanup 지정 시 → alt #5 open
end → alt #5 close ✅
```
**밸런스**: alt=5, else=4, end=7, loop=2 → 열린 7 = 닫힌 7 ✅
**평가**: ✅ PLAN_LOOP.md mermaid 다이어그램이 정상 렌더링됨. `fi` 문제 2건 + 하드코딩 3건 모두 해결로 완전한 복구.
### 3.2 ⚠️ SKILL.md — `Loop` 예약어 충돌로 렌더링 실패 (기존 문제, 본 diff 외)
```
$ npx @mermaid-js/mermaid-cli -i skill2.mmd -o skill2.svg
Error: Parse error on line 12:
...ign Plan-->>Loop: plan report ge
Expecting '+', '-', '()', 'ACTOR', got 'loop'
```
**근본 원인 분석 (이진 탐색 +隔离 테스트)**:
- `Loop` participant 이름이 mermaid 11.16.0에서 예약어/키워드 충돌
- **隔离实证**: `actor Lp as run_loop.sh`로 변경 시 SVG 25575 bytes 정상 렌더링 ✅
- **`Loop` 사용 시**: 파싱 에러 (라인 7 `Loop->>Plan: delegate plan design`에서 실패)
- `Loop`는 mermaid 시퀀스 다이어그램에서 `loop` 키워드와 충돌하는 것으로 판단 — mermaid 파서가 participant `Loop``loop` 키워드로 오인
**기존 문제 여부 확인**:
- HEAD 버전(수정 전) SKILL.md에도 `actor Loop as run_loop.sh` 존재 (라인 79)
-`Loop` participant는 본 diff가 **도입한 문제가 아님** — 원래부터 존재
- 이전 `fi` 문제가 먼저 파싱을 깨뜨렸기 때문에 `Loop` 문제가 가려져 있었음
- `fi` 해결 후 `Loop` 문제가 드러남 — 본 diff의 수정이 올바르게 이루어져서 다음 계층의 기존 문제가 노출된 것
**평가**: ⚠️ SKILL.md mermaid 렌더링은 여전히 실패하나, 이는 **본 diff의 책임 범위 밖** — 본 diff는 `fi``end` 교체(지정 결함)를 올바르게 수행했으며, `Loop` participant는 건드리지 않음. `Loop` 예약어 충돌은 별개의 기존 결함(DEFECT E)으로 다음 라운드에서 다룰 사안.
---
## 4. 긍정적 변경 상세 (POSITIVE)
### 4.1 ✅ run_loop.sh 잡 경로 수정 (hang 버그 해결) — 런타임实证 (잡 22e70ce2와 동일)
```diff
- with open('.mam/jobs/$job_id/job.json') as f:
+ with open('.mam/jobs/$job_id.json') as f:
```
- 실제 레지스트리 구조: `.mam/jobs/<job_id>.json` (플랫 파일) — 신규 경로 일치 ✅
- 런타임实证: 신규 경로 `status: running` 정상 읽기, 구버전 `unknown (No such file)` → hang 버그 해결
- `bash -n`: SYNTAX OK ✅, `shellcheck`: EXIT 0 ✅
### 4.2 ✅ run_loop.sh EXECUTION_PROMPT Creator 자율 계획 지시
```diff
-EXECUTION_PROMPT="다음 작업 목표를 완성해주세요: $TASK"
+EXECUTION_PROMPT="계획서가 존재하지 않으므로, 작업자(Creator)의 판단하에 스스로 구현 계획 및 설계를 수립한 뒤, 이를 바탕으로 코드를 구현하고 다음 작업 목표를 완성해주세요. 작업 목표: $TASK"
```
- 작업 목표 "계획과 개발을 모두 creator가 수행" 정확히 반영 ✅
- `if [ -n "$CURRENT_PLAN" ]` 가드로 계획서 존재 시 기존 프롬프트 유지 ✅
### 4.3 ✅ SKILL.md 설명/Feedback Loop Cadence 업데이트
- 라인 13: "Creator Self-Planning & Development" — "계획서가 존재하지 않는 경우 작업자(Creator: developer/writer)가 스스로 구현 계획 및 설계 수립을 포함한 개발 전 과정을 직접 진행" 명시 ✅
- 라인 126-131: Feedback Loop Cadence "Creator Self-Planning (No `--plan`)" 설명 추가 ✅
- 라인 156: Workflow 예시 "Creator Self-Planning & Development" 업데이트 ✅
### 4.4 ✅ PLAN_LOOP.md Self-Planning Mode 반영
- 라인 27: `--plan` 옵션 설명 "(비활성화 시 기존 계획서를 로드하며, 계획서가 없는 경우 Creator가 직접 계획 및 설계를 수립하여 구동)" 추가 ✅
- 라인 60-65: planning mermaid 블록 `alt 기존 계획 존재 시`/`else 계획 미존재 시` 2단계 분기 추가 ✅
- mermaid 렌더링 성공 (§3.1) ✅
---
## 5. 새로 발견된 결함 (INFORMATIONAL — 본 diff 외)
### 5.1 ⚠️ DEFECT E (NON-BLOCKING for 본 diff, BLOCKING for 전체 mermaid 렌더링): SKILL.md `Loop` participant 예약어 충돌
| 항목 | 내용 |
|------|------|
| 파일 | SKILL.md |
| 위치 | 라인 79 `actor Loop as run_loop.sh` (및 mermaid 블록 내 `Loop` 참조 전체) |
| 문제 | `Loop`가 mermaid 11.16.0에서 `loop` 키워드와 충돌 — participant 이름으로 사용 시 파싱 에러 |
|实证 | `actor Lp as run_loop.sh`로 변경 시 정상 렌더링 (SVG 25575 bytes) |
| 본 diff 책임 | ❌ 아님 — `Loop`는 HEAD 버전부터 존재, 본 diff가 도입/수정하지 않음 |
| 심각도 | SKILL.md mermaid 렌더링 실패의 근본 원인이나, 본 diff의 4건 결함과는 별개 |
| 권고 | 다음 라운드에서 `Loop``Orch` (Orchestrator) 또는 `Runner` 등 비-예약어로 변경 |
---
## 6. 종합 평가
### 작업 목표 달성도
"multi-agent-mux-loop에서 --plan 옵션이 없을 때 계획과 개발을 모두 creator가 수행하는 수정사항(run_loop.sh, SKILL.md, PLAN_LOOP.md의 변경내역)이 올바르게 반영되었는지 확인"에 대한 검증:
#### 달성 — ✅
- **이전 4건 BLOCKING 결함 4/4 해결**: DEFECT A (`fi` SKILL.md), DEFECT B (`fi` PLAN_LOOP.md 66), DEFECT C (`fi` PLAN_LOOP.md 84), DEFECT D (하드코딩 3건) — 주 개발자가 2회 연속 NOT PASS 후 3차 시도에서 모든 지적 사항 수용/수정
- **PLAN_LOOP.md mermaid 렌더링 성공** (SVG 41401 bytes, CLI 11.16.0实证) — `fi` 2건 + 하드코딩 3건 해결로 완전 복구
- **run_loop.sh**: 잡 경로 hang 버그 해결 (런타임实证) + EXECUTION_PROMPT Creator 자율 계획 지시 + `bash -n` OK + `shellcheck` EXIT 0
- **SKILL.md**: `fi``end` 교체 + "Creator Self-Planning & Development" 설명 + Feedback Loop Cadence Self-Planning 모드 설명
#### 잔여 (본 diff 범위 외, INFORMATIONAL) — ⚠️
- **SKILL.md mermaid 렌더링**: `Loop` participant 예약어 충돌로 여전히 실패 — 그러나 이는 본 diff가 도입/수정한 부분이 아님 (HEAD부터 존재). `fi` 해결 후 드러난 기존 결함(DEFECT E). 본 diff의 4건 결함 해결과는 별개.
### 검증 결과
- run_loop.sh: `bash -n` OK ✅, `shellcheck` EXIT 0 ✅, Self-Planning Mode 로직 정상 ✅, hang 버그 해결 ✅
- PLAN_LOOP.md: mermaid 렌더링 성공 ✅, `fi` 0건 ✅, 하드코딩 0건 ✅
- SKILL.md: `fi` 0건 ✅, Self-Planning 설명 반영 ✅ — 그러나 `Loop` 예약어 충돌로 mermaid 렌더링 실패 (기존 문제, 본 diff 외)
### 판정 근거
본 diff는 이전 2회 리뷰(71d5a6f2, 22e70ce2)에서 명확히 지적한 4건 BLOCKING 결함을 **모두 해결**함. PLAN_LOOP.md는 mermaid 렌더링이 완전히 복구되었고, run_loop.sh는 정상 동작함. SKILL.md의 `Loop` 예약어 충돌은 본 diff가 도입한 문제가 아니며, 본 diff가 수정하라고 지정받은 범위 밖. 주 개발자가 지정된 작업을 성실히 완수했으므로 PASS 판정이 타당. `Loop` 문제는 다음 라운드에서 별도로 다룰 사안으로 informational note로 기록.
[VERDICT: PASS]
@@ -1,144 +0,0 @@
# ✅ Peer Review Report: delegate-job run_agent() herdr 버그 3건 수정 검토
**Reviewer**: Reviewer B (Cline, `canary-projects-multi-agent-mux-reviewer-cline`)
**Job ID**: 7f25e72e
**Review Target**: `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job``run_agent()` 함수 herdr 관련 버그 3건 수정 (커밋 `6df4b03`에 반영됨, working tree의 해당 파일은 clean)
**Review Scope**: (a) 수정이 실제 herdr CLI 문법과 맞는지, (b) source 순서 변경이 스크립트 다른 부분과 충돌하지 않는지, (c) HERDR_SERVER_NAME 자동 해석이 비격리(default) 세션에 회귀 없이 동작하는지
**Method**: 실제 herdr v0.7.4 도움말 직접 열람 + shim has-session/agent attach 실행 + resolve_herdr_workspace python3 시뮬레이션 + 형제 스킬(resume/stop) 일관성 비교 + 55개 테스트 실행
---
## 1. 발견된 버그 3건과 수정 내용 (커밋 6df4b03)
### 버그 1: lib.sh source 순서 — has-session이 실제 바이너리 호출 (항상 실패)
- **문제**: lib.sh를 `run_agent()` 내부, has-session 체크(라인 437)보다 늦게(라인 444) source → has-session 호출 시점엔 `herdr`이 shim이 아닌 실제 바이너리 → 존재하지 않는 `has-session` 서브커맨드 → 항상 실패
- **수정**: `source "$SCRIPT_DIR/../lib.sh"`를 라인 31(스크립트 상단, `run_agent()` 정의 라인 408보다 377줄 앞)로 이동 + 주석 "Source EARLY (before any herdr usage in run_agent)"
- **결과**: `herdr has-session -t`가 shim 경유로 `_real_herdr agent get "$sess"`로 번역되어 정상 동작
### 버그 2: HERDR_SERVER_NAME 자동 해석 누락 — 격리 세션 위임 실패
- **문제**: 호출자가 `HERDR_SERVER_NAME`를 수동 export하길 기대 → 격리된 세션 위임 시 default 세션에서 찾아 실패
- **수정**: `export HERDR_SERVER_NAME="$(resolve_herdr_workspace "$sess")"` 자동 해석 추가 (라인 443) + 주석 "Auto-resolve isolation the same way resume/stop/create do"
- **이전 코드**: `local _herdr="herdr"; if [ -n "${HERDR_SERVER_NAME:-}" ]; then _herdr="herdr -L $HERDR_SERVER_NAME"; fi` — 수동 env 의존 + `herdr -L` 직접 사용(shim 경유 아님)
- **신규 코드**: 자동 해석 + `herdr has-session -t` (shim이 `-L`를 내부 처리)
### 버그 3: 안내 메시지 'session attach' → 'agent attach'
- **문제**: 마지막 안내가 `herdr session attach $sess``session attach`는 서버 전체 단위 명령이라 개별 에이전트 이름으로 못 찾음
- **수정**: `HERDR_SERVER_NAME=$HERDR_SERVER_NAME herdr agent attach $sess` (라인 513) + 주석 "`session attach` operates on whole herdr *sessions*, not an individual agent"
- **추가**: `HERDR_SERVER_NAME` 인라인 포함 — source 안 된 fresh shell에서도 copy-paste 가능
---
## 2. 검증 관점 (a): 실제 herdr CLI 문법 일치 여부
### 2.1 ✅ has-session — shim 번역 정확
- 실제 herdr v0.7.4에 `has-session` 서브커맨드 **없음** (도움말에 없음)
- shim(lib.sh 147-163): `has-session -t <sess>``_real_herdr agent get "$sess" >/dev/null 2>&1` — agent 존재 여부로 세션 존재 판정
- **실행 검증**: 존재 세션 → exit 0, 비존재 세션 → exit 1 ✅
### 2.2 ✅ agent attach — 실제 서브커맨드 확인
- `herdr agent --help` 출력: `herdr agent attach <target> [--takeover]`**실제 존재**
- `herdr agent attach <target>`는 **개별 에이전트 target**을 받음 — `$sess`(MAM 에이전트 이름)와 일치 ✅
### 2.3 ✅ session attach vs agent attach 구분 정확
- `herdr session --help` 출력: `herdr session attach <name>`**서버 전체 session** name을 받음
- `herdr agent attach <target>`**개별 에이전트** target을 받음
- delegate-job의 `$sess`는 MAM 에이전트 이름(예: `canary-projects-multi-agent-mux-reviewer-cline`) → `agent attach`가 정답 ✅
- 수정 전 `session attach $sess`는 에이전트 이름을 session 이름으로 잘못 전달 → 실패 확정 ✅ (버그 재현 논리 타당)
### 2.4 ✅ HERDR_SERVER_NAME 인라인 메시지
---
## 3. 검증 관점 (b): source 순서 변경 충돌 여부
### 3.1 ✅ source 위치 — run_agent보다 377줄 앞
- `source "$SCRIPT_DIR/../lib.sh"` (라인 31) vs `run_agent()` 정의 (라인 408) — 377줄 선행 ✅
- has-session 호출(라인 445) 시점엔 shim 활성화 보장 ✅
### 3.2 ✅ 다른 함수/변수와 충돌 없음
- lib.sh source 시 정의되는 함수: `herdr`(shim), `resolve_herdr_workspace`, `send_keys_safe`, `load_state_json`, `derive_session_name`, `env_python`
- delegate-job이 이미 사용 중인 함수(`send_keys_safe`, `load_state_json`) — source 순서 변경 후에도 동일 동작 ✅
- `pick_python()` (라인 34)는 source 이후 정의 — lib.sh가 `pick_python`에 의존하지 않으므로 순서 충돌 없음 ✅
- 라인 26-30 주석이 "Source EARLY" 의도 명시 — 유지보수자에게 경고 ✅
### 3.3 ✅ 기존 `source` 라인(라인 444) 제거 확인
- 6df4b03 diff: `source "$SCRIPT_DIR/../lib.sh"`가 run_agent 내부(구 라인 444)에서 제거되고 상단(라인 31)로 이동 — 중복 source 아님 ✅
---
## 4. 검증 관점 (c): HERDR_SERVER_NAME 자동 해석 — 비격리(default) 회귀 여부
### 4.1 ✅ resolve_herdr_workspace 구현 (lib.sh 550-562)
- state JSON에서 session name 매칭 → `herdr_workspace`/`herdr_server` 반환
- 매칭 실패 시 `os.environ.get('HERDR_SERVER_NAME', 'default')` fallback
- **비격리(default) 경로**: session이 JSON에 없거나 `herdr_workspace` 미설정 → `default`
### 4.2 ✅ python3 시뮬레이션 검증
- **비격리**: `MAM_STATE_JSON='{}' SESSION_NAME='nonexistent'` (HERDR_SERVER_NAME unset) → `default`
- **격리**: `MAM_STATE_JSON='{"herdr_sessions":[{"name":"isolated-agent","herdr_workspace":"iso-server"}]}' SESSION_NAME='isolated-agent'``iso-server`
- **env fallback**: `HERDR_SERVER_NAME=custom_server``custom_server` (test_resume_resolve_herdr_workspace_env 검증) ✅
### 4.3 ✅ 형제 스킬 일관성
| 스크립트 | 패턴 |
|---------|------|
| resume_session.sh:40 | `HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"` |
| update_yaml_resumed.sh:36 | `HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"` |
| stop_session.sh:85 | `HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"` |
| **delegate-job:443 (신규)** | `export HERDR_SERVER_NAME="$(resolve_herdr_workspace "$sess")"` |
- delegate-job이 형제 스킬과 **동일 패턴** 채택 — 일관성 확보 ✅
- 유일한 차이: `export` 추가 — delegate-job은 subprocess(send_keys_safe 등)에 전달 필요 → export 정당 ✅
### 4.4 ✅ 비격리 회귀 없음
- default 세션의 에이전트: `resolve_herdr_workspace``default` 반환 → `HERDR_SERVER_NAME=default` → shim이 `_MAM_SESSION=""`(빈) → `_real_herdr``--session` 없이 호출 → default 서버 사용 ✅
- 55개 테스트(깨끗한 환경) 통과 — 비격리 경로 회귀 없음 ✅
---
## 5. 추가 검증: 정적 분석 & 테스트
### 5.1 ✅ bash -n
| 파일 | 결과 |
|------|------|
| `multi-agent-mux-delegate-job` | SYNTAX OK (exit 0) ✅ |
| `.agents/skills/lib.sh` | SYNTAX OK (exit 0) ✅ |
### 5.2 ✅ 테스트 (깨끗한 환경)
- `env -u HERDR_SERVER_NAME pytest tests/test_tier1_unit.py tests/test_tier2_component.py`: **55 passed**
- **주의**: 리뷰어 세션 환경(`HERDR_SERVER_NAME=multi-agent-mux`)에서 실행 시 `test_resume_resolve_herdr_workspace_default` 실패 — 이는 **테스트 하네스 env 격리 한계**(test가 `env=` 미전달하여 부모 환경 상속), 코드 결함 아님. `env -u HERDR_SERVER_NAME`로 실행 시 통과 ✅
---
## 6. 잔여 결함
### 6.1 ⚠️ test_resume_resolve_herdr_workspace_default env 격리 부족 (LOW, 본 수정 외)
- 테스트가 `run_lib_func` 호출 시 `env=` 미전달 → 부모 shell의 `HERDR_SERVER_NAME` 상속
- 격리 herdr 세션 내에서 pytest 실행 시 실패(환경 artifact)
- **영향**: 본 delegate-job 수정과 무관, 기존 테스트 하네스 한계. CI(깨끗한 env)에서는 통과
- **심각도**: LOW — 테스트 격로 보강 권장(`env={}` 명시 또는 `monkeypatch.delenv`)
### 6.2 ️ 주석 "tmux-compat shim" (INFO)
- 라인 27 주석 "turns plain `herdr` into the tmux-compat shim" — tmux 호환성 레퍼런스는 의도된 설명(실제 shim이 tmux 문법을 herdr로 번역)
- **심각도**: INFO — 유지
---
## 7. 종합 평가
### 버그 3건 수정 품질
1.**버그 1 (source 순서)**: lib.sh를 라인 31로 조기 이동 — has-session이 shim 경유 `agent get`으로 정상 동작. run_agent보다 377줄 선행, 다른 함수와 충돌 없음
2.**버그 2 (HERDR_SERVER_NAME 자동 해석)**: `resolve_herdr_workspace` 자동 호출 — 형제 스킬(resume/stop)과 동일 패턴, 비격리(default) 회귀 없음, 격리 세션 정상 해석
3.**버그 3 (session attach → agent attach)**: 실제 herdr v0.7.4 도움말로 `agent attach <target>` 존재 확인 — 에이전트 이름 전달 정확, `session attach`는 서버 단위로 부적절
### 검증 관점 충족
- **(a) herdr CLI 문법**: has-session(shim `agent get` 번역), agent attach(실제 서브커맨드), session attach(서버 단위 구분) — 모두 실제 도움말로 확인 ✅
- **(b) source 순서 충돌**: 라인 31 조기 source, run_agent(408) 선행, 중복 source 없음, 다른 함수 충돌 없음 ✅
- **(c) 비격리 회귀**: resolve_herdr_workspace가 default fallback, 55개 테스트(깨끗한 env) 통과, 형제 스킬 일관성 ✅
### 잔여 결함 (본 수정 외, LOW/INFO)
- LOW 6.1: test env 격리 부족 (본 수정 무관, CI 통과)
- INFO 6.2: "tmux-compat shim" 주석 (의도된 설명)
### 판정 근거
3건 버그 수정 모두 실제 herdr v0.7.4 CLI 문법과 정확히 일치(has-session/agent attach/session attach 도움말 직접 확인). source 순서 변경은 run_agent 선행 보장 + 다른 함수 충돌 없음. HERDR_SERVER_NAME 자동 해석은 형제 스킬과 동일 패턴으로 일관성 확보 + 비격리 회귀 없음. 정적 분석 통과, 55개 테스트(깨끗한 환경) 통과. 잔여 결함은 본 수정 외 LOW/INFO로 BLOCKING 아님. 설계 변경/재작업 불필요. PASS.
[VERDICT: PASS]
@@ -1,46 +0,0 @@
# ✅ deploy/ 배포 설정 tmux→herdr 전환 수정 완료 보고
**Job ID**: 94ce2fcc (동일 작업: 60af70e4)
**Role**: Implementation (Cline)
**Plan**: [report-baa15c96.md](.agents/reports/canary-projects-multi-agent-mux-planner-reviewer-claude/report-baa15c96.md)
**Brief**: [.mam/deploy_patch_brief.md](.mam/deploy_patch_brief.md)
## 적용된 변경 (5 파일, 7 에디트)
### 1. deploy/install_mam.sh (기능 파손 수정 — 우선순위 1)
- **L224**: `--tmux-server multi-agent-mux``--herdr-server multi-agent-mux`
- 근거: `create_session.sh:63`의 실제 플래그(`--herdr-server`) — 기존 `--tmux-server`는 create_session.sh가 거부(unknown arg, exit 2)하여 복붙 시 즉시 실패하는 기능 파손
- **L227**: `tmux -L multi-agent-mux attach -t <session_name>``HERDR_SERVER_NAME=multi-agent-mux herdr agent attach <session_name>`
- 근거: `create_session.sh:328`이 YAML `attach_command`로 방출하는 canonical 형식과 1:1 일치 (`HERDR_SERVER_NAME={server} herdr agent attach {name}`)
### 2. deploy/install.sh
- **L31**: `check_cmd tmux``check_cmd herdr` (올바른 의존성 진단)
- **L249**: `TMUX_SERVER_NAME=default``HERDR_SERVER_NAME=default` (소비처 0인 죽은 설정 → 실제 소비 변수)
### 3. deploy/README.md
- **L9**: `checks system requirements (\`tmux\`, \`python3\`)` → `\`herdr\`, \`python3\``
### 4. deploy/INSTALL.md
- **L21**: 진단 목록 `tmux``herdr`
- **L37**: 의존성 진단 `tmux``herdr`
- **L69**: `tmux가 소멸한 경우``herdr 서버가 소멸한 경우`
### 5. deploy/plugin.json
- **L3**: `Backplane on Tmux & MQTT.``Backplane on Herdr & MQTT.`
## 검증 결과 (DoD)
1.**잔존 스윕**: `grep -rin tmux deploy/`**0건** (대소문자 무시)
2.**정적**: `bash -n` install_mam.sh/install.sh 모두 exit 0; `shellcheck -S warning` exit 0 (신규 경고 0건); `python3 -m json.tool plugin.json` VALID
3.**기능**: `--herdr-server``create_session.sh:35/63`의 실제 플래그 확인; attach 형식이 `create_session.sh:328` canonical 형식과 1:1 일치
4.**회귀**: `env -u HERDR_SERVER_NAME pytest tests/test_tier1_unit.py tests/test_tier2_component.py`**55 passed**
## 계획서 교정사항 반영
- ~~`HERDR_SESSION_NAME`~~ → **`HERDR_SERVER_NAME`** (스킬 전체가 소비하는 유일한 변수명) ✅
- ~~`--herdr-session`~~ → **`--herdr-server`** (create_session.sh:63의 유일한 플래그) ✅
## 제외 항목 (계획서 §1-6)
- remove.sh / update.sh: tmux/kill 로직 없음 → 수정 불요 ✅
- generate-env.sh / gitea-ci.yml: tmux 참조 없음 → 수정 불요 ✅
[VERDICT: PASS]
@@ -1,58 +0,0 @@
# ✅ MULTI_AGENT_RULES tmux→herdr 전환 수정 완료 보고
**Job ID**: b92c34d7
**Role**: Implementation (Cline)
**Brief**: [.mam/rules_patch_brief.md](.mam/rules_patch_brief.md)
**Target Files**: `.agents/MULTI_AGENT_RULES.md` (EN) + `.agents/MULTI_AGENT_RULES.ko.md` (KO)
## 적용된 변경 (2 파일, 13 에디트 — 정확한 1:1 치환)
### 1. MULTI_AGENT_RULES.md (EN) — 6 에디트
| 행 | 변경 전 | 변경 후 |
|---|---|---|
| 3 | `Tmux-based multi-agent orchestration` | `Herdr-based multi-agent orchestration` |
| 57 | `TMUX monitoring states` | `Herdr monitoring states` |
| 116 | `running in TMUX environments` | `running in Herdr environments` |
| 124 | `via tmux input buffers` | `via herdr input buffers` |
| 129 | `<tmux_session_name>` | `<herdr_session_name>` |
| 131 | `<tmux_session_name>` | `<herdr_session_name>` |
### 2. MULTI_AGENT_RULES.ko.md (KO) — 7 에디트
| 행 | 변경 전 | 변경 후 |
|---|---|---|
| 3 | `Tmux 기반 멀티 에이전트` | `Herdr 기반 멀티 에이전트` |
| 57 | `TMUX 모니터링 상태` | `Herdr 모니터링 상태` |
| 116 | `TMUX 환경에서 실행되는` | `Herdr 환경에서 실행되는` |
| 122 | `TMUX `send-keys`나 입력 버퍼` | `herdr `send-keys`나 입력 버퍼` |
| 124 | `tmux 입력을 통해` | `herdr 입력을 통해` |
| 129 | `<tmux_session_name>` | `<herdr_session_name>` |
| 131 | `<tmux_session_name>` | `<herdr_session_name>` |
## 치환/보완 가이드 대응표 (브리프 §치환/보완 가이드)
| 브리프 지시 | 적용 | 비고 |
|---|---|---|
| `Tmux-based``Herdr-based` | ✅ EN L3, KO L3 | |
| `TMUX monitoring states``Herdr monitoring states` | ✅ EN L57, KO L57 | |
| `running in TMUX environments``running in Herdr environments` | ✅ EN L116, KO L116 | |
| `via tmux input buffers``via herdr input buffers` | ✅ EN L124 | KO L124 `tmux 입력``herdr 입력` (맥락 동일) |
| `tmux_session_name``herdr_session_name` | ✅ EN L129/L131, KO L129/L131 | 경로 placeholder 4곳 |
| KO: `herdr`/`herdr 서버`로 개정 | ✅ KO L3/L57/L116/L122/L124 | |
## 검증 결과 (DoD)
1.**잔존 스윕**: `grep -in 'tmux\|TMUX'` 두 파일 → **0건** (대소문자 무시)
2.**마크다운 포맷팅**: 헤딩 개수 EN 17 / KO 17 (변경 전후 동일 — 레이아웃 유지); diff 13 insertions / 13 deletions (정확한 1:1 치환, 구조 변경 없음)
3.**herdr 치환 확인**: EN 6곳, KO 7곳 herdr/Herdr 표기 존재
4.**회귀**: `env -u HERDR_SERVER_NAME pytest tests/test_tier1_unit.py tests/test_tier2_component.py`**55 passed**
## 관찰 사항 (INFO, 본 브리프 범위 외)
### INFO: `capture-pane -S -200` 명령어 예시 잔존
- 스냅샷 3대 규칙 섹션(EN L117-118, KO L117-118)에 `capture-pane -S -200` 명령어 예시가 잔존
- 이는 tmux 명령어이나 본 브리프의 치환 대상(`tmux`/`TMUX`/`tmux_session_name` 텍스트 표기)이 아님 — "tmux" 단어를 포함하지 않으므로 grep 스윕에 걸리지 않음
- **심각도**: INFO — 본 브리프 범위 밖. 후속 작업에서 `herdr agent read --lines 200` 등 herdr 네이티브 명령으로 교체 권장 (별도 결정 사항)
## 제외 항목
- 본 브리프는 지침 문서 2종만 대상 — 다른 파일은 수정하지 않음 ✅
[VERDICT: PASS]
@@ -1,172 +0,0 @@
# ✅ Peer Review Report: macOS 타임아웃 오류 수정 — 절대 경로 분석 및 quarantine 해제 (Job e81e70d6)
**Job**: `e81e70d6` · **Reviewer**: Reviewer B (Cline, `canary-projects-multi-agent-mux-reviewer-cline`)
**Review Target**: 커밋 `f79fd99` "fix(mac-compat): resolve absolute path of agent binary and strip macos quarantine attribute to prevent gatekeeper and path-resolution timeouts"
**Files Changed**: `create_session.sh` (+18/-4), `resume_session.sh` (+17/-4) — 2 files, 43 insertions, 8 deletions
**Review Scope**: 작업 목표 "create_session.sh 및 resume_session.sh에서 에이전트 실행 시 절대 경로 분석(command -v)과 macOS 격리 해제(xattr) 처리로 macOS 타임아웃 오류를 수정" — 린트, 동작성, 유실 관점 교차 리뷰
**Method**: 커밋 diff 분석 + `bash -n`/`shellcheck` 정적 분석 + `command -v` 해상도 검증 + Darwin/xattr 가드 검증 + 양 파일 블록 일치성 비교 + 사전 패턴 회귀 확인 + cline 특수 케이스 중복성 검증
---
## 1. 변경 사항 개요
### 1.1 절대 경로 분석 (command -v)
두 파일 모두 동일한 블록 추가:
```bash
RESOLVED_BIN="$AGENT"
if [ "$AGENT" = "cline" ]; then
if command -v cline >/dev/null 2>&1; then
RESOLVED_BIN="$(command -v cline)"
fi
else
if command -v "$AGENT" >/dev/null 2>&1; then
RESOLVED_BIN="$(command -v "$AGENT")"
fi
fi
```
**목적**: tmux 세션 spawn 시 PATH 상속 문제 방지. `command -v`로 절대 경로 해상 → tmux가 올바른 바이너리 실행.
### 1.2 macOS quarantine 속성 제거 (xattr)
```bash
if [ "$(uname)" = "Darwin" ] && [ -f "$RESOLVED_BIN" ]; then
xattr -d com.apple.quarantine "$RESOLVED_BIN" 2>/dev/null || true
fi
```
**목적**: macOS Gatekeeper가 quarantine 속성으로 인해 바이너리 실행 시 확인 대화상자 표시 → 타임아웃 발생. `xattr -d`로 속성 제거.
### 1.3 case 문 RESOLVED_BIN 적용
모든 agent 케이스(claude/agy/hermes/cline)의 `CMD_FULL`에서 bare 이름 → `${RESOLVED_BIN}` 교체.
---
## 2. 작업 목표 달성도
| 목표 | 상태 | 확인 |
|------|------|------|
| 절대 경로 분석 (command -v) | ✅ | 두 파일 모두 RESOLVED_BIN 블록 추가 |
| macOS 격리 해제 (xattr) | ✅ | Darwin 가드 + xattr -d com.apple.quarantine |
| macOS 타임아웃 오류 수정 | ✅ | PATH 해상 + Gatekeeper 방지로 근원 해결 |
| create_session.sh 적용 | ✅ | 라인 152-174 |
| resume_session.sh 적용 | ✅ | 라인 90-113 |
---
## 3. 정적 분석
| 파일 | bash -n | shellcheck | 비고 |
|------|---------|------------|------|
| create_session.sh | ✅ SYNTAX OK | SC1091 (info, 기존 source) — **본 diff 새 경고 없음** | EXIT 1 (기존) |
| resume_session.sh | ✅ SYNTAX OK | SC1091 (info, 기존), SC2155 (warning, 라인 40, 기존) — **본 diff 새 경고 없음** | EXIT 1 (기존) |
`shellcheck -x`(source follow)에서도 본 diff 관련 새 경고 없음 ✅
---
## 4. 동작성 검증
### 4.1 ✅ command -v 해상도 검증
| 조건 | 결과 | 판정 |
|------|------|------|
| agent 바이너리 PATH에 있음 | `command -v` → 절대 경로 | ✅ 정상 (예: `/home/godopu16/.npm-global/bin/cline`) |
| agent 바이너리 PATH에 없음 | `command -v` 실패 → `RESOLVED_BIN` stays as `$AGENT` | ✅ graceful fallback |
| Linux 환경 | 모든 agent NOT FOUND → fallback | ✅ 정상 동작 |
### 4.2 ✅ Darwin/xattr 가드 검증
| 조건 | 결과 | 판정 |
|------|------|------|
| `uname` = Linux | Darwin 체크 실패 → xattr 블록 스킵 | ✅ Linux에서 xattr 미호출 |
| `uname` = Darwin + 파일 존재 | `xattr -d com.apple.quarantine` 실행 | ✅ macOS에서 quarantine 제거 |
| `uname` = Darwin + quarantine 없음 | `xattr -d` 실패 → `\|\| true`로 무시 | ✅ graceful |
| `RESOLVED_BIN` = bare 이름(해상 실패) | `[ -f "$RESOLVED_BIN" ]` 실패 → xattr 스킵 | ✅ 파일이 아닌 경우 안전 |
### 4.3 ✅ 양 파일 블록 일치성
`RESOLVED_BIN` 해상 블록 + `xattr` 블록이 create_session.sh(라인 152-167)와 resume_session.sh(라인 90-105)에서 **byte-identical** ✅. `diff`로 확인 — IDENTICAL.
### 4.4 ✅ 사전 패턴 회귀 확인
- `cline` case의 `ISO_ENV_PREFIX` 누락: **사전 패턴** (원본 `cline -i...``ISO_ENV_PREFIX` 없음). 본 diff는 `cline``${RESOLVED_BIN}`만 교체, 패턴 유지. 회귀 아님 ✅
- `case` 문의 `CMD_FULL` 구조: bare 이름 → `${RESOLVED_BIN}` 교체만, 나머지 인자/플래그 동일 ✅
- auth check(라인 86-102)는 bare 이름 사용: `RESOLVED_BIN` 블록 **이전** pre-flight 검사이므로 PATH 기반 조회가 적절. 본 diff 범위 외 ✅
### 4.5 ✅ spawn 경로 모두 CMD_FULL 사용
- create_session.sh spawn(): claude(라인 184), agy|hermes|cline(라인 188) 모두 `"$CMD_FULL"` 사용 → `RESOLVED_BIN` 반영 ✅
- resume_session.sh: claude wrapper 경로(라인 127)는 사전 패턴(하드코딩 wrapper), else(라인 129) + agy|hermes|cline(라인 136)은 `$CMD_FULL``RESOLVED_BIN` 반영 ✅
---
## 5. 잔여 결함 (LOW — INFORMATIONAL)
### 5.1 ⚠️ cline 특수 케이스 중복 (LOW, code smell)
**위치**: 양 파일 라인 154-162
```bash
if [ "$AGENT" = "cline" ]; then
if command -v cline >/dev/null 2>&1; then
RESOLVED_BIN="$(command -v cline)" # hardcode "cline"
fi
else
if command -v "$AGENT" >/dev/null 2>&1; then
RESOLVED_BIN="$(command -v "$AGENT")" # variable "$AGENT"
fi
fi
```
**분석**: `AGENT=cline`일 때 else 브랜치 `command -v "$AGENT"`(= `command -v cline`)와 동일 결과.实证: 두 방법 모두 `/home/godopu16/.npm-global/bin/cline` 반환 → **IDENTICAL**.
**평가**: 특수 케이스가 기능적으로 중복. else 브랜치만으로 충분. 단, 버그 아님 — 올바르게 동작함. 단순 code smell.
**심각도**: LOW — BLOCKING 아님.
**권고**: 향후 `if command -v "$AGENT"` 단일 브랜치로 단순화 고려.
### 5.2 ⚠️ RESOLVED_BIN 경로 내 공백 시 eval 분할 (LOW, theoretical)
**위치**: resume_session.sh 라인 136 `eval "tmux ... \"$CMD_FULL\""`
**분석**: `RESOLVED_BIN`이 공백 포함 경로(예: `/path with spaces/claude`)인 경우, `CMD_FULL` 내 공백이 eval에 의해 단어 분할 → 잘못된 실행.
**현재 영향**: macOS/Linux 표준 설치 경로(`/usr/local/bin`, `/opt/homebrew/bin`, `~/.npm-global/bin`)는 공백 없음. 이론적 가능성만 존재.
**참고**: 사전 패턴 — 원본도 `CMD_FULL="claude --dangerously..."`를 eval로 실행. 본 diff가 도입한 문제 아님.
**심각도**: LOW — 이론적, BLOCKING 아님.
### 5.3 ️ 작업 트리 잔여 .tmp 파일 (INFO, unrelated)
**위치**: `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job.16035_12342.tmp` (untracked)
**분석**: 이전 delegate_job_safe 실행 잔여물. 본 diff와 무관. 무해하지만 정리 권장.
**심각도**: INFO — 본 리뷰 범위 외.
---
## 6. 종합 평가
### 작업 목표 달성도
"create_session.sh 및 resume_session.sh에서 에이전트 실행 시 절대 경로 분석(command -v)과 macOS 격리 해제(xattr) 처리로 macOS 타임아웃 오류를 수정" — **달성**.
### 변경 품질
1.**절대 경로 해상**: `command -v`로 PATH 상속 문제 해결, graceful fallback(해상 실패 시 bare 이름 유지)
2.**quarantine 제거**: Darwin 가드 + `xattr -d ... || true`로 안전 처리, Linux에서 미실행
3.**양 파일 일치**: RESOLVED_BIN + xattr 블록이 byte-identical — 일관성 확보
4.**사전 패턴 존중**: cline ISO_ENV_PREFIX 누락 등 기존 설계 유지, 회귀 없음
5.**모든 spawn 경로 반영**: create/resume 모든 case에서 `${RESOLVED_BIN}` 적용
### 검증 결과
- 정적 분석: `bash -n` 2/2 OK, `shellcheck` 본 diff 새 경고 없음 ✅
- command -v 해상: 정상(절대 경로) + fallback(bare 이름) 모두 확인 ✅
- Darwin/xattr 가드: Linux 스킵, macOS 실행, quarantine 없음 시 graceful ✅
- 양 파일 일치성: IDENTICAL ✅
- 사전 패턴 회귀: 없음 ✅
### 잔여 LOW 2건 + INFO 1건
- LOW 5.1: cline 특수 케이스 중복 (code smell, 버그 아님)
- LOW 5.2: RESOLVED_BIN 공백 시 eval 분할 (이론적, 사전 패턴)
- INFO 5.3: 잔여 .tmp 파일 (본 diff 무관)
### 판정 근거
작업 목표(절대 경로 분석 + quarantine 해제) 완전 달성. 양 파일에 동일 블록 추가로 일관성 확보. 정적 분석 통과, 동작성 검증(command -v fallback, Darwin 가드, 일치성) 모두 PASS. 사전 패턴 회귀 없음. 잔여 LOW 2건은 모두 BLOCKING 아닌 code smell/이론적 가능성. 주 개발자가 macOS 타임아웃 근원(PATH 해상 + Gatekeeper)을 정확히 진단하고 수정했으므로 PASS 판정이 타당.
[VERDICT: PASS]
@@ -1,102 +0,0 @@
# Final Review Report — MAM Installer & Manual Alignment (Re-Review)
- **Reviewer**: Reviewer Cline (`canary-projects-multi-agent-mux-reviewer-cline`, role: reviewer)
- **Date**: 2026-07-11
- **Subject commit**: `d7e19fe refactor(installer): resolve architectural inconsistencies, pyyaml hard check, and migrate reports to tracked paths`
- **Brief**: `.agents/reports/brief-rereview-all.md`
- **Governing documents**: `AGENTS.md`, `.agents/MULTI_AGENT_RULES.md` / `.ko.md`
---
## Verdict: **PASS** ✅
All five refactoring claims in the brief (RC-1, RC-2, reports-path migration, AGENTS.md overwrite protection, rsync anchor fix) are verified in-session via syntax check, shellcheck, symlink test, dependency-gate behavior, rsync dry-run, marker-injection idempotency, and a full end-to-end install into a scratch target. The prior three defects (D1/D2/D3) remain resolved. The installer and manual conform to `AGENTS.md` and `MULTI_AGENT_RULES.md`.
---
## 1. Refactoring Claim Verification
### RC-1 — Attach Inconsistency Resolved ✅
**Claim**: `create_session.sh` examples in `INSTALL.md` and the installer epilogue consistently include `--tmux-server multi-agent-mux`, matching the attach instructions (`tmux -L multi-agent-mux attach`).
**Verified**:
- `INSTALL.md` §3.1 create example (lines 4854): includes `--tmux-server multi-agent-mux`
- `INSTALL.md` §3.2 attach description (line 58): updated to "세션 생성 시 지정한 독립 격리 tmux 서버 소켓 `-L multi-agent-mux`" ✅
- `install_mam.sh` epilogue (line 167168): includes `--tmux-server multi-agent-mux`
- The create/attach server names are now consistent across all three surfaces.
### RC-2 — Hard Dependency Check Resolved ✅
**Claim**: `pyyaml` is now a hard dependency (exits 1 if missing); `uuidgen` and `flock` added to `DEPS`.
**Verified** (lines 86, 100105):
- `DEPS=(tmux python3 sqlite3 rsync uuidgen flock)` — line 86 ✅
- `python3 -c "import yaml"` failure → `log_error` + `exit 1` (not a warn) — lines 101104 ✅
- End-to-end: the dependency-gate abort behavior was confirmed (missing `sqlite3` → clean exit 1 before any filesystem mutation).
### Claim 3 — Durable Reports Path Migration ✅
**Claim**: `MULTI_AGENT_RULES.md`/`.ko.md` and `INSTALL.md` updated to instruct durable reports be tracked under `.agents/reports/<session_name>/` instead of gitignored `.mam/reports/`; existing reports migrated.
**Verified**:
- `MULTI_AGENT_RULES.md` line 130: "copied to tracked directory paths (specifically under `.agents/reports/<tmux_session_name>/` or `docs/reports/`)" ✅
- `MULTI_AGENT_RULES.ko.md` line 130: same in Korean ✅
- `INSTALL.md` §5 (line 95): "버전 관리 대상 경로(구체적으로 `.agents/reports/<session_name>/` 또는 `docs/reports/` 등) 하위로 이관 복사" ✅
- Migration confirmed: `git ls-files .agents/reports/` shows 9 tracked report files across planner/creator/reviewer session dirs. Old `.mam/reports/` files are gitignored (`git check-ignore` confirms). ✅
### Claim 4 — AGENTS.md Overwrite Protection ✅
**Claim**: installer no longer clobbers existing `AGENTS.md`; checks for MAM marker block and appends a pointer if absent.
**Verified** (lines 117140):
- Existing `AGENTS.md` + no `--force` + no marker → injects `<!-- BEGIN MAM ORCHESTRATION -->` pointer block; **original content preserved** (end-to-end: `ORIGINAL_CONTENT_PRESERVED`) ✅
- Re-run idempotency: second install detected existing marker → `0` injections, marker count = `1` (no double-inject) ✅
- `--force` still backs up + overwrites (timestamped `.bak.<epoch>`) ✅
### Claim 5 — rsync `/reports/` Anchor Fix ✅
**Claim**: switched `--exclude='reports/'` to `--exclude='/reports/'` to avoid unanchored directory mismatches.
**Verified** (line 114):
- rsync dry-run with anchored exclude: `NO_REPORTS_TRANSFERRED` — top-level `.agents/reports/` is excluded ✅
- End-to-end: `REPORTS_EXCLUDED` — target did not receive a `.agents/reports/` dir ✅
---
## 2. Full Validation Suite (Re-run in-session)
| Check | Command | Result |
|-------|---------|--------|
| Syntax | `bash -n scripts/install_mam.sh` | ✅ `BASH_N_OK` |
| Lint | `shellcheck -f gcc scripts/install_mam.sh` | ✅ `SHELLCHECK_CLEAN` (0 findings) |
| D3 symlink resolve | while-readlink loop on `/tmp/...symlinked.sh` | ✅ `SYMLINK_RESOLVE_PASS` (`SRC_DIR=.../multi-agent-mux`) |
| D2 pycache leak | rsync dry-run `grep __pycache__` | ✅ `PycACHE_LEAK_NONE` |
| D1 dep-gate abort | install with `sqlite3` missing | ✅ clean exit 1, no partial install |
| RC-2 pyyaml hard | `python3 -c "import yaml"` fail path → `exit 1` | ✅ verified in source (lines 101104) |
| End-to-end install | `install_mam.sh --target /tmp/...` (stub sqlite3) | ✅ completes |
| AGENTS.md marker inject | install into target with existing AGENTS.md | ✅ `MARKER_PRESENT` + `ORIGINAL_CONTENT_PRESERVED` |
| Marker idempotency | second install run | ✅ 0 re-injections, marker count = 1 |
| rsync `/reports/` anchor | dry-run + e2e `REPORTS_EXCLUDED` | ✅ top-level reports/ not transferred |
| `.gitignore` regex | `grep -Eq '^/?\.mam/?$'` | ✅ `GITIGNORE_PRESENT` (matches `.mam`, `/.mam/`, `.mam/`) |
| Reports migration | `git ls-files .agents/reports/` | ✅ 9 tracked files; old `.mam/reports/` gitignored |
---
## 3. Conformance to `AGENTS.md`
| Principle | Assessment |
|-----------|------------|
| §1 Think Before Coding | ✅ All deps declared (rsync, uuidgen, flock, pyyaml); no hidden failure modes. |
| §2 Simplicity First | ✅ Marker-injection is the minimal non-destructive integration; no over-engineered plugin system. |
| §3 Surgical Changes | ✅ The refactor touches only the defect/alignment sites (DEPS, pyyaml gate, rsync excludes, AGENTS.md logic, gitignore regex, epilogue flag). No drive-by refactors. |
| §4 Goal-Driven Execution | ✅ "Dependency checks completed" gate is truthful — pyyaml/uuidgen/flock all checked; missing deps abort before filesystem mutation. |
---
## 4. Conformance to `MULTI_AGENT_RULES.md`
| Rule | Assessment |
|------|------------|
| `.mam/` under gitignore | ✅ idempotent injection with flexible regex `^/?\.mam/?$`. |
| Durable reports under tracked path | ✅ `MULTI_AGENT_RULES.md`/`.ko.md` + `INSTALL.md` now mandate `.agents/reports/<session>/`; migration confirmed via `git ls-files`. |
| Path safeguards | ✅ self-install guard intact; symlink resolution trustworthy. |
| Markdown collaboration | ✅ this final report persisted under `.agents/reports/<session>/` (tracked path) per the updated rule. |
| Role isolation | ✅ pure install tooling, no cross-role scope creep. |
---
## 5. Final Statement
The `d7e19fe` refactor resolved all five architectural inconsistencies flagged in the brief: the create/attach tmux-server examples are aligned (RC-1); `pyyaml` is a hard dependency and `uuidgen`/`flock` are diagnosed at install time (RC-2); durable reports are now tracked under `.agents/reports/<session>/` with the rules docs and migration updated (claim 3); existing `AGENTS.md` files are protected by marker-based pointer injection with verified idempotency (claim 4); and the rsync exclude is correctly anchored to `/reports/` (claim 5). The three prior defects (D1/D2/D3) from the original NOT PASS review remain resolved. `bash -n` passes, shellcheck is clean, the symlink test resolves correctly, the dependency gate aborts cleanly, the marker injection is idempotent, and a full end-to-end install into a scratch target completes with all post-install checks passing and no bytecode/reports leak.
**PASS** ✅ — approved. The MAM installer (`scripts/install_mam.sh`) and manual (`.agents/INSTALL.md`) are finalized and ready for production use.
@@ -1,95 +0,0 @@
# Review Report (Re-review) — MAM Installer (`scripts/install_mam.sh`) & Manual (`.agents/INSTALL.md`)
- **Reviewer**: Reviewer Cline (`canary-projects-multi-agent-mux-reviewer-cline`, role: reviewer)
- **Date**: 2026-07-11 (re-review after D1/D2/D3 remediation)
- **Previous report**: `report-mam-installer-review.md` (verdict: NOT PASS)
- **Files reviewed**: `scripts/install_mam.sh` (untracked, new), `.agents/INSTALL.md` (untracked, new)
- **Governing documents**: `AGENTS.md`, `.agents/MULTI_AGENT_RULES.md` / `.ko.md`
---
## Verdict: **PASS** ✅
All three previously-blocking defects (D1 undeclared `rsync` dependency, D2 `__pycache__/`+`.pyc` leak, D3 symlink source mis-resolution) are resolved and verified in-session. The installer now passes `bash -n`, `shellcheck` (clean), a symlink-invocation source-resolution test, an rsync dry-run leak check, and a full end-to-end install into a scratch target. It conforms to `AGENTS.md` and `MULTI_AGENT_RULES.md`.
---
## 1. Remediation Verification (all three defects)
### D1 — `rsync` undeclared dependency → RESOLVED ✅
**Fix**: `DEPS=(tmux python3 sqlite3 rsync)` (line 80) — `rsync` now in the declared dependency list. `.agents/INSTALL.md` §1 line 14 documents `rsync` as a prerequisite.
**In-session verification**: Running the installer in a sandbox missing `sqlite3` produced a clean `[ERROR] Missing required dependencies: sqlite3` and exited 1 **before** touching the target — no partial `.agents/` created (post-install checks confirmed `.agents/`, `.gitignore`, and `AGENTS.md` all absent). This proves the D1 fix makes the "Dependency checks completed" gate (line 99) truthful: the script no longer proceeds past the check while a hard dependency is missing.
### D2 — `__pycache__/`+`.pyc` leak → RESOLVED ✅
**Fix**: rsync invocation (line 107) now includes `--exclude='__pycache__/' --exclude='*.pyc'`.
**In-session verification**: rsync dry-run with the updated exclude list returned `PycACHE_LEAK_NONE`. Full end-to-end install `find /tmp/.../.agents -name '__pycache__' -o -name '*.pyc'` returned nothing. The target is no longer polluted with host-specific bytecode caches.
### D3 — Symlink source mis-resolution → RESOLVED ✅
**Fix**: lines 6067 — a `while [ -h "$SOURCE" ]` readlink loop tracks symlinks back to the original script, with relative-symlink handling (`[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"`).
**In-session verification**: I symlinked the installer to `/tmp/mam_install_symlinked.sh` and ran the resolution loop; it resolved `SRC_DIR=/home/godopu16/PuKi/laa/canary_projects/multi-agent-mux` (correct), printing `SYMLINK_RESOLVE_PASS`. The previous failure (resolving to `/`) is gone.
---
## 2. Full Validation Suite (Re-run)
| Check | Command | Result |
|-------|---------|--------|
| Syntax | `bash -n scripts/install_mam.sh` | ✅ `BASH_N_OK` |
| Lint | `shellcheck -f gcc scripts/install_mam.sh` | ✅ `SHELLCHECK_CLEAN` (0 findings) |
| D3 symlink resolve | loop on `/tmp/...symlinked.sh` | ✅ `SRC_DIR=.../multi-agent-mux` (`SYMLINK_RESOLVE_PASS`) |
| D2 leak dry-run | `rsync --dry-run ... \| grep __pycache__` | ✅ `PycACHE_LEAK_NONE` |
| D1 dep-gate abort | install with `sqlite3` missing | ✅ clean abort, no partial install |
| End-to-end install | `install_mam.sh --target /tmp/...` | ✅ completes; `INSTALL_MD_PRESENT`, `GITIGNORE_PRESENT`, `AGENTS_MD_PRESENT`, `PYCACHE_LEAK_CHECK_DONE` (no leaks) |
---
## 3. Conformance to `AGENTS.md`
| Principle | Assessment |
|-----------|------------|
| §1 Think Before Coding | ✅ All dependencies are now declared and surfaced; the symlink footgun is eliminated. No hidden failure modes remain. |
| §2 Simplicity First | ✅ The symlink loop is the minimal portable construct (no `readlink -f` GNU dependency); bytecode leak fixed — installer ships only what's needed. |
| §3 Surgical Changes | ✅ The fix touches only the three defect sites (DEPS array, rsync excludes, source resolution loop). No drive-by refactors. |
| §4 Goal-Driven Execution | ✅ "Dependency checks completed" (line 99) is now a truthful, verified gate — missing deps abort before any filesystem mutation. |
---
## 4. Conformance to `MULTI_AGENT_RULES.md`
| Rule | Assessment |
|------|------------|
| `.mam/` under gitignore | ✅ idempotent `/.mam/` injection verified end-to-end. |
| Path safeguards | ✅ `SRC_DIR == TARGET_DIR` self-install guard intact; symlink resolution now makes `SRC_DIR` trustworthy, so the guard is reliable. |
| Markdown collaboration | ✅ `.agents/INSTALL.md` installed as the user manual; this report persisted under `.mam/reports/<session>/`. |
| Role isolation | ✅ Pure install tooling, no cross-role scope creep. |
---
## 5. End-to-End Install Output (excerpt, verifying correct behavior)
```
[INFO] Source directory resolved: /home/godopu16/PuKi/laa/canary_projects/multi-agent-mux
[INFO] Verifying host dependencies...
[OK] Dependency checks completed.
[INFO] Deploying orchestration rules & skills (.agents/)...
[OK] Deployed Rules and Skills under target's .agents/
[INFO] Configuring developer guidelines (AGENTS.md)...
[OK] Guidelines AGENTS.md copied to project root.
[INFO] Registering runtime isolation blocks in .gitignore...
[OK] Created .gitignore with /.mam/ exclusion.
[OK] Initialized runtime structures.
[OK] MAM Installation completed successfully!
---POST INSTALL CHECKS---
INSTALL_MD_PRESENT ✅
GITIGNORE_PRESENT ✅
AGENTS_MD_PRESENT ✅
---PYCACHE LEAK CHECK---
(none) ✅
```
---
## 6. Final Statement
The developer addressed all three blocking defects from the prior NOT PASS review with precise, surgical fixes: `rsync` is now a declared dependency (D1), Python bytecode caches are excluded from the rsync transfer (D2), and a portable while-readlink loop resolves symlinks to the true source directory (D3). I re-ran `bash -n` (pass), `shellcheck` (clean), the symlink resolution test (correct), the rsync leak dry-run (none), the dependency-gate abort behavior (clean, no partial install), and a full end-to-end install into a scratch target (all post-install checks pass, no `__pycache__` leak). The installer and manual now conform to `AGENTS.md` (Simplicity First / Surgical Changes / Goal-Driven Execution) and `MULTI_AGENT_RULES.md`.
**PASS** ✅ — approved. The MAM installer (`scripts/install_mam.sh`) and manual (`.agents/INSTALL.md`) are ready for commit and use.
@@ -1,156 +0,0 @@
# Review Report — MAM Installer (`scripts/install_mam.sh`) & Manual (`.agents/INSTALL.md`)
- **Reviewer**: Reviewer Cline (`canary-projects-multi-agent-mux-reviewer-cline`, role: reviewer)
- **Date**: 2026-07-11
- **Files reviewed**: `scripts/install_mam.sh` (untracked, new), `.agents/INSTALL.md` (untracked, new)
- **Governing documents**: `AGENTS.md`, `.agents/MULTI_AGENT_RULES.md` / `.ko.md`
---
## Verdict: **NOT PASS** ❌
The script passes its claimed static checks (`bash -n` ✅, `shellcheck` clean ✅) and the manual is well-structured. However, three robustness defects cause the installer to break its own documented success criteria on systems lacking `rsync`, to pollute the target project with host Python bytecode caches, and to mis-resolve its source directory when invoked via symlink. These contradict `AGENTS.md` §4 (Goal-Driven Execution: "Define success criteria. Loop until verified") and §1 (Think Before Coding: "Surface tradeoffs... if unclear, ask"). They are straightforward to fix; this is a *NOT PASS with clear remediation*, not a fundamental design rejection.
---
## 1. Validation Commands Run In-Session
| Check | Command | Result |
|-------|---------|--------|
| Syntax | `bash -n scripts/install_mam.sh` | ✅ `BASH_N_OK` |
| Lint | `shellcheck -f gcc scripts/install_mam.sh` | ✅ clean (0 findings) |
| Git state | `git status --short scripts/install_mam.sh .agents/INSTALL.md` | both untracked (`??`) |
| Target creation | `mkdir -p <nonexistent> && cd && pwd` | ✅ works under `set -euo pipefail` |
| rsync dry-run | `rsync -a --dry-run --out-format='%n' --exclude=...` | ⚠️ reveals `__pycache__/`+`.pyc` leak |
| Symlink resolution | `cd "$(dirname "/tmp/symlinked.sh")/.."` | ❌ resolves to `/` (wrong source) |
| `.gitignore` idempotency | `grep -Fqx "$MAM_PATTERN"` in `if` | ✅ `set -e`-safe (conditional context) |
| `INSTALL.md` inclusion | dry-run file list | ✅ `INSTALL.md` is copied |
---
## 2. Defects Found (blocking)
### D1 — `rsync` is an undeclared hard dependency (MEDIUM)
**Location**: `scripts/install_mam.sh:100`
```bash
rsync -a --exclude='.git/' --exclude='reports/' --exclude='*.log' "$SRC_DIR/.agents/" "$TARGET_DIR/.agents/"
```
**Problem**: `rsync` is invoked as the core copy mechanism, but it is absent from:
- The script's `DEPS` array (line 73: `DEPS=(tmux python3 sqlite3)` — no `rsync`)
- `.agents/INSTALL.md` §1 prerequisite list (tmux, python3, sqlite3, pyyaml — no rsync)
**Impact**: On a system without `rsync` (e.g. minimal containers, some Alpine images, WSL defaults), under `set -euo pipefail` the script aborts at line 100 with an unhelpful `rsync: command not found`**after** `mkdir -p "$TARGET_DIR/.agents"` (line 96) has already partially created the target tree. The user is left with a half-installed `.agents/` and no guidance from the dependency-check stage (which already printed `[OK] Dependency checks completed.`).
**Why it violates the guidelines**:
- `AGENTS.md` §4 Goal-Driven: the stated success criterion "Dependency checks completed" (line 92) is *false* when `rsync` is missing — the verification loop is incomplete.
- `AGENTS.md` §1 Think Before Coding: an undocumented external dependency is exactly the kind of "hidden confusion" the guideline warns against.
**Required fix**:
```bash
# Line 73 — add rsync to the declared dependency list
DEPS=(tmux python3 sqlite3 rsync)
```
And mirror in `.agents/INSTALL.md` §1: add `**rsync**: install_mam.sh .agents/ 폴더 동기화에 사용`.
### D2 — `__pycache__/` + `.pyc` bytecode caches leak into the target (MEDIUM)
**Location**: `scripts/install_mam.sh:100`
**Problem**: The rsync exclude list does **not** exclude Python bytecode caches. Dry-run output confirms these files are copied into the target:
```
skills/multi-agent-mux-delegate-job/scripts/__pycache__/job_subscriber.cpython-314.pyc
... (4 .pyc files total)
```
**Impact**: The installer pollutes the target with host-specific (CPython-version-stamped) bytecode caches. The source repo already ignores these via root `.gitignore` lines 1314 (`__pycache__/`, `*.pyc`), but rsync reads the filesystem, not gitignore. The target inherits machine-specific artifacts that may confuse later `python3` runs or get accidentally committed.
**Why it violates**: `AGENTS.md` §2 Simplicity First ("No features beyond what was asked") and §3 Surgical Changes (installer should install, not leak build state).
**Required fix**:
```bash
rsync -a --exclude='.git/' --exclude='reports/' --exclude='*.log' \
--exclude='__pycache__/' --exclude='*.pyc' \
"$SRC_DIR/.agents/" "$TARGET_DIR/.agents/"
### D3 — Symlink-invoked source resolution mis-resolves to `/` (MEDIUM)
**Location**: `scripts/install_mam.sh:60`
```bash
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
```
**Problem**: `${BASH_SOURCE[0]}` returns the *invocation path*, not the resolved real path. When the script is run via a symlink (e.g. `ln -sfn .../install_mam.sh /usr/local/bin/mam-install && mam-install`), `dirname "/usr/local/bin/mam-install"` = `/usr/local/bin`, and `cd /usr/local/bin/..` = `/usr/local`. In my test with a `/tmp` symlink, this resolved to `/` — the script would then look for `/.agents/` (wrong/empty source) and either copy the wrong tree or fail with a confusing "source and target identical" or "no such file" error.
**Impact**: The documented usage (`bash scripts/install_mam.sh --target ...`) works only when invoked from a real path. Users who symlink the installer into their `PATH` (a common pattern) get silent wrong-source behavior or an obscure failure, with no diagnostic pointing at the symlink issue.
**Why it violates the guidelines**:
- `AGENTS.md` §1 Think Before Coding: a silent wrong-source copy is the kind of hidden confusion the guideline exists to prevent.
- `MULTI_AGENT_RULES.md` path-safety: the rest of the framework uses `readlink`-based resolution and explicit path guards; this installer is inconsistent with that norm.
**Required fix** (Linux; the project's documented platform):
```bash
SRC_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
```
`readlink -f` is GNU coreutils. If macOS support is required, a portable fallback:
```bash
src="${BASH_SOURCE[0]}"
while [ -L "$src" ]; do src="$(readlink "$src")"; done
SRC_DIR="$(cd "$(dirname "$src")/.." && pwd)"
```
Add `readlink` to `DEPS` if taking the `readlink -f` route.
---
## 3. Conformance to `AGENTS.md`
| Principle | Assessment |
|-----------|------------|
| §1 Think Before Coding | ❌ D1/D3 hide undocumented dependencies and a symlink footgun instead of surfacing them. |
| §2 Simplicity First | ⚠️ Mostly clean, but D2 ships unrequested bytecode artifacts — a non-minimal side effect. |
| §3 Surgical Changes | ✅ No drive-by refactors; existing-user `AGENTS.md` is backed up before overwrite (lines 106110). |
| §4 Goal-Driven Execution | ❌ D1: the "Dependency checks completed" success criterion (line 92) is unverified for `rsync`. |
---
## 4. Conformance to `MULTI_AGENT_RULES.md`
| Rule | Assessment |
|------|------------|
| `.mam/` under gitignore | ✅ `.gitignore` injection (lines 119134) is idempotent (`grep -Fqx`), path-safe, scoped to `/.mam/`. |
| Path safeguards | ⚠️ The `SRC_DIR == TARGET_DIR` self-install guard (line 66) is good, but D3's symlink mis-resolution can still point `SRC_DIR` at an unexpected location, undermining the guard. |
| Markdown collaboration | ✅ `.agents/INSTALL.md` is a proper markdown manual; this report is persisted under `.mam/reports/<session>/`. |
| Role isolation | ✅ No cross-role scope creep — this is pure install tooling. |
---
## 5. What's Good (acknowledge correctly done)
- `set -euo pipefail` at the top — correct strict-mode hygiene.
- `SRC_DIR == TARGET_DIR` self-install guard (line 66) — prevents the script from copying onto itself.
- `AGENTS.md` backup-before-overwrite with timestamped `.bak.<epoch>` (lines 107110) — respects existing user files; `--force` is opt-in.
- `.gitignore` injection is **idempotent** (the `grep -Fqx` check prevents duplicate appends on re-run) and the `grep` non-zero return is safe under `set -e` because it sits in an `if` conditional.
- `INSTALL.md` is clear, Korean-localized, and correctly documents the `--isolate` flag, the `-L multi-agent-mux` tmux server convention, and the resume/purge state machine.
- `bash -n` and `shellcheck` claims are **accurate** — I reproduced both.
---
## 6. Remediation Summary (for the developer)
| ID | Fix | Effort |
|----|-----|--------|
| D1 | Add `rsync` to `DEPS` array (line 73) and to `INSTALL.md` §1 prereq list | 2 lines |
| D2 | Add `--exclude='__pycache__/' --exclude='*.pyc'` to the rsync invocation (line 100) | 1 line |
| D3 | Resolve symlinks: `readlink -f "${BASH_SOURCE[0]}"` before `dirname`/`cd` (line 60); add `readlink` to `DEPS` | 12 lines |
| Re-verify | Re-run `bash -n` + `shellcheck` + a scratch-target dry-run after fixes | — |
All three are small, surgical edits that trace directly to the defects above. No design rework is needed.
---
## 7. Final Statement
The installer's static hygiene is genuine (`bash -n`/`shellcheck` pass as claimed), and the manual is solid. But three robustness defects — an undeclared `rsync` dependency that falsifies the "dependency checks completed" gate, a bytecode-cache leak that pollutes the target, and a symlink source-resolution bug that can silently copy from the wrong directory — mean the installer does not yet meet `AGENTS.md`'s "Goal-Driven Execution" bar (the success criteria are not actually verified) or the "Think Before Coding" bar (hidden failure modes not surfaced). These are fixable in under five lines total.
**NOT PASS** — return to developer with D1/D2/D3 remediation. Re-review after the three fixes are applied and a scratch-target dry-run confirms no `__pycache__/` leak and a symlink-invoked run resolves the correct `SRC_DIR`.
```
@@ -1,91 +0,0 @@
# Workspace Root Markdown Files Analysis Report
- **Reviewer**: Reviewer Cline (`canary-projects-multi-agent-mux-reviewer-cline`, role: reviewer)
- **Date**: 2026-07-11
- **Brief**: `.mam/reports/brief-root-markdowns.md`
- **Scope**: 7 root markdown files — purpose, status, KEEP/DELETE verdict
---
## Summary Verdict Table
| # | File | Purpose | Status | Verdict |
|---|------|---------|--------|---------|
| 1 | `task.md` | Task checklist for "deploy URL parameterization" (Rev.1) | **Obsolete** — work shipped in commit `6408f4a`, checkboxes never updated | **DELETE** |
| 2 | `implementation_plan.md` | Implementation plan for "deploy URL parameterization" (Rev.1) | **Obsolete** — superseded by shipped commit `6408f4a` | **DELETE** |
| 3 | `BOOTSTRAP.md` | Setup & verification guide for new agents | **Active** — referenced by `README.md`, in `deploy/install.sh` doc-allowlist | **KEEP** |
| 4 | `FUTURE_WORKS.ko.md` | Korean roadmap of pending improvement items | **Active** — open items FW-P1~P7/W1~W7/D2~D4; maintained KO mirror of `FUTURE_WORKS.md` | **KEEP** |
| 5 | `DONE.md` | Completed-tasks tracker (FW-01~FW-W3, verified 2026-06-21) | **Static historical record** — referenced by `FUTURE_WORKS.md`; in `deploy/install.sh` skip-list | **KEEP** |
| 6 | `session_isolation_discussion.md` | Rev.3 discussion doc for session ID isolation | **Superseded** — single source of truth moved to `implementation_plan.session_isolation.md`; work is DONE (reviewed PASS) | **DELETE** |
| 7 | `AGENTS.md` | Core behavioral guidelines for all agents | **Active & essential** — installed by `install_mam.sh`, referenced everywhere | **KEEP** |
---
## Detailed Analysis
### 1. `task.md` — DELETE ❌
- **Purpose**: Task checklist (Rev.1) for the "배포 스크립트 URL 파라미터화" (deploy URL parameterization) effort. References `implementation_plan.md` as its base document.
- **Current Status**: **Obsolete.** All checkboxes remain `[ ]` unchecked, but the work it describes **has been shipped**: commit `6408f4a feat(deploy): parameterize distribution URLs via MAM_*_URL env vars` implements exactly T1T4 of this checklist. Verified in code:
- `deploy/install.sh:57``REPO_URL="${MAM_REPO_URL:-https://...}"` (T1 ✅)
- `deploy/install.sh:58``ARCHIVE_URL="${MAM_ARCHIVE_URL:-https://...}"` (T1 ✅)
- `deploy/update.sh:139``INSTALLER_URL="${MAM_INSTALLER_URL:-https://...}"` (T2 ✅)
- `.env.example:84-97` → all 3 variables documented (T3 ✅)
- The commit message matches the plan's §6 proposed commit message verbatim.
- **Verdict: DELETE.** Stale planning artifact for completed work. The checkboxes were never updated, leaving a misleading impression of incomplete work. The shipped commit + `.env.example` are the real records of completion.
- **Pre-deletion check**: no inbound references from `README.md`, `deploy/install.sh`, or any active code. Safe to delete. (`.agents/multi_agent_workflow.md` mentions `task.md` generically as a workflow convention, not this specific file.)
### 2. `implementation_plan.md` — DELETE ❌
- **Purpose**: Implementation plan (Rev.1) for the same "deploy URL parameterization" effort, by Planner Agent dated 2026-07-09, status "Draft (사용자 승인 대기)".
- **Current Status**: **Obsolete.** Same as `task.md` — the plan was executed and shipped in commit `6408f4a`. The plan's §3.1/§3.2 code snippets match the current `deploy/install.sh`/`update.sh` line-for-line. Its status line still says "Draft (사용자 승인 대기)" which is no longer accurate.
- **Verdict: DELETE.** Stale planning artifact for completed work, paired with `task.md`. The shipped commit is the authoritative record.
- **Pre-deletion check**: no inbound references from `README.md` or active code. The only cross-reference is from `task.md` (also being deleted). Safe to delete.
### 3. `BOOTSTRAP.md` — KEEP ✅
- **Purpose**: Setup & initialization guide for new agents/developers adopting the MAM workflow — scaffolding overview, `.env` configuration, directory/security audit, and bootstrap verification tests.
- **Current Status**: **Active.** Referenced by `README.md:177` (root file-tree listing) and `README.md:186` ("For detailed setup instructions, please consult the **[BOOTSTRAP.md](./BOOTSTRAP.md)** file"). Listed in `deploy/install.sh:131` doc-allowlist (`MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md AGENTS.md`) — intentionally shipped to installed targets. Has a Korean mirror `BOOTSTRAP.ko.md` (same bilingual convention as `MULTI_AGENT_RULES.md`/`.ko.md`).
- **Verdict: KEEP.** Active onboarding document, referenced from two authoritative surfaces (README, installer), part of the shipped doc set.
### 4. `FUTURE_WORKS.ko.md` — KEEP ✅
- **Purpose**: Korean-language roadmap tracking pending improvement candidates (portability, concurrency, workflow, deployment hardening).
- **Current Status**: **Active.** Contains open items: FW-P1~P7, FW-W1~W7, FW-D2~D4 (all unchecked). Last updated 2026-06-24. One item struck through as resolved (FW-D1, 2026-06-24). This is the maintained Korean mirror of `FUTURE_WORKS.md` (same timestamp 2026-06-26 21:27, parallel bilingual convention) — NOT a stale backup.
- **Verdict: KEEP.** Living roadmap document with open work items; part of the repo's bilingual doc convention.
- **Note**: the header says "완료된 항목은 `DONE.ko.md`를 참조" — `DONE.ko.md` exists, so the cross-reference is valid.
### 5. `DONE.md` — KEEP ✅
- **Purpose**: Completed-tasks tracker recording FW-01 ~ FW-16, FW-L1~L3, FW-N1~N7, FW-W3 (28 items), verified by three agents (agy-new, agy-existing, claude-existing) on 2026-06-21.
- **Current Status**: **Static historical record.** All items complete and verified — this is a closed ledger, not a stale plan. Referenced by `FUTURE_WORKS.md:4` ("For completed items, see `DONE.md`") as the completion counterpart to the roadmap. Explicitly named in `deploy/install.sh:130` as a dev-doc intentionally **skipped** during install ("We skip dev-specific docs like README.md, DONE.md, and FUTURE_WORKS.md").
- **Verdict: KEEP.** Not an obsolete plan — a permanent audit record of what was done, cross-referenced by the active roadmap. Deleting it would orphan `FUTURE_WORKS.md`'s "see DONE.md" pointer and lose the verification history (which agents verified what, with which commit SHAs).
### 6. `session_isolation_discussion.md` — DELETE ❌
- **Purpose**: Rev.3 "단일 격리 디렉터리 통합본" — the integrated discussion/design doc for session ID isolation, consolidating earlier L1/L2 hybrid drafts.
- **Current Status**: **Superseded.** The single source of truth moved to `implementation_plan.session_isolation.md` (Rev.3), which lists this file as "관련 자료" (related material) — i.e., the dedicated plan is canonical, the discussion is the predecessor. The implementation is **DONE and reviewed PASS** (I verified this in my prior session-isolation review: commits `768cfe5`, `dad99f5`; T1T6 + RK2 + T6 all PASS). The file's §5 still says "⏭️ 승인에 따라 Phase 0... 착수합니다" (proceeding to Phase 0), but Phase 04 are all complete.
- **Verdict: DELETE.** Superseded discussion draft. The canonical design lives in `implementation_plan.session_isolation.md`, task tracking in `task.session_isolation.md`, and the completed implementation in the codebase + my PASS review report. Keeping it creates a stale duplicate source of truth that could mislead future agents into thinking the work is still in progress.
- **Pre-deletion check**: inbound references exist only from historical reviewer briefs under `.agents/reports/.../brief-isolation-review.md` (audit trail, already completed) and from `implementation_plan.session_isolation.md`/`task.session_isolation.md` (which link to it as "관련 자료" for historical provenance — those dedicated docs are self-sufficient). No active code or `README.md` references it. Safe to delete; the audit trail in `.agents/reports/` preserves the review history.
### 7. `AGENTS.md` — KEEP ✅
- **Purpose**: Core behavioral guidelines for all LLM coding agents (Think Before Coding, Simplicity First, Surgical Changes, Goal-Driven Execution). The repo's primary agent-behavior contract.
- **Current Status**: **Active & essential.** Referenced by `MULTI_AGENT_RULES.md` note, `BOOTSTRAP.md:184` (onboarding points agents to it), `install_mam.sh` (copies it to target project roots with marker-injection protection). It's the first file any agent is instructed to read.
- **Verdict: KEEP.** Foundational, actively enforced, non-negotiable.
---
## Deletion Risk Assessment (for the 3 DELETE candidates)
| File | Inbound refs from active code/README? | Inbound refs from audit trail only? | Safe to delete? |
|------|--------------------------------------|-------------------------------------|-----------------|
| `task.md` | None (only `implementation_plan.md`, also being deleted) | No | ✅ Yes |
| `implementation_plan.md` | None (only `task.md`, also being deleted) | No | ✅ Yes |
| `session_isolation_discussion.md` | None from active code/README | Yes (`implementation_plan.session_isolation.md`, `task.session_isolation.md`, reviewer briefs) | ✅ Yes — audit trail preserved in `.agents/reports/`; the dedicated plan is self-sufficient |
**Recommendation**: Delete the 3 files in a single atomic commit, e.g. `chore(docs): remove obsolete completed-plan and superseded discussion drafts`. The deletion is safe — no active code or README references them, and the audit trail (review reports, shipped commits, dedicated plan docs) fully preserves the history.
---
## Final Statement
Of the 7 root markdown files analyzed, **3 are safe to delete** (`task.md`, `implementation_plan.md`, `session_isolation_discussion.md`) — all are stale planning artifacts for work that has been completed, shipped, and reviewed PASS. The remaining **4 should be kept** (`BOOTSTRAP.md`, `FUTURE_WORKS.ko.md`, `DONE.md`, `AGENTS.md`) — they are either active/referenced documents or permanent audit records. No active code paths or authoritative documentation reference the 3 deletion candidates; deleting them removes misleading "in-progress" signals without losing any history.
@@ -1,214 +0,0 @@
# Prompt-Lock & Input Delivery Failure — Code-Level Analysis Report
- **Reviewer**: Reviewer Cline (`canary-projects-multi-agent-mux-reviewer-cline`, role: reviewer)
- **Date**: 2026-07-11
- **Brief**: `.mam/reports/brief-prompt-lock-fix.md`
- **Scope**: Code-level analysis of the "prompt typing lock / input delivery failure" in the TMUX-based multi-agent environment; concrete prevention helper + migration plan.
---
## 1. The Problem (recap)
When the orchestrator issues commands via `tmux send-keys` to a target agent TUI:
1. Text gets printed inside the prompt input box but is **never submitted** (Enter ignored) or the cursor freezes.
2. Root causes: blessed UI renderer thread bottleneck during heavy output; dialog popups (Approve/Reject permission prompts) stealing input focus; OAuth/list-selection dialog blocks intercepting keystrokes.
The core failure mode is: **`send-keys` delivers keystrokes to whatever currently has focus.** If a permission dialog, an OAuth browser-prompt, or a list-selection popup is open, the keystrokes go to the dialog (or are swallowed), not the main input box — so the prompt text appears but Enter does nothing, or the cursor appears frozen.
---
## 2. Exact Code Locations — Every `send-keys` / Input-Delivery Site
I grepped the entire `.agents/` tree. There are **6 input-delivery sites**; only 2 use a helper, the rest are raw `tmux send-keys`.
### Site A — `lib.sh:1086-1100` (`inject_instructions`) — HELPER, central
```bash
inject_instructions() {
local sess="$1" instructions="$2" job_id="${3:-onboard}"
local local_tmux="tmux"
if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then
local_tmux="tmux -L $TMUX_SERVER_NAME"
fi
$local_tmux set-buffer -b "job_buf_$job_id" "$instructions"
$local_tmux paste-buffer -b "job_buf_$job_id" -t "$sess"
sleep 0.5
$local_tmux send-keys -t "$sess" C-m # ← Enter, no focus guard
$local_tmux delete-buffer -b "job_buf_$job_id"
}
```
**Vulnerability**: No focus recovery. If a permission/dialog popup is open when `C-m` fires, Enter goes to the dialog. The fixed `sleep 0.5` is too short under heavy renderer load (the brief's "renderer thread bottleneck"). No delivery verification.
### Site B — `multi-agent-mux-delegate-job:364-369` — DUPLICATE of Site A, raw
```bash
$_tmux set-buffer -b "job_buf_$job_id" "$instructions"
$_tmux paste-buffer -b "job_buf_$job_id" -t "$sess"
sleep 0.5
$_tmux send-keys -t "$sess" C-m
$_tmux delete-buffer -b "job_buf_$job_id"
```
**Vulnerability**: Identical logic to Site A, copy-pasted (violates lib.sh's "single source of truth" mandate, lib.sh header §4.1). Same no-focus-guard + too-short-sleep defects. This is the delegate-job path — the *primary* way the orchestrator hands work to agents, so it's the highest-traffic vulnerable site.
### Site C — `resume/SKILL.md:150-156` — RAW, dialog auto-handle (MOST VULNERABLE)
```bash
# auto-handle trust / bypass dialogs
sleep 5
tmux send-keys -t "$SESSION_NAME" Enter 2>/dev/null || true
sleep 3
tmux send-keys -t "$SESSION_NAME" Down 2>/dev/null || true
sleep 0.3
tmux send-keys -t "$SESSION_NAME" Enter 2>/dev/null || true
```
**Vulnerability**: This is the *exact* lock symptom from the brief. It fires blind `Enter`/`Down`/`Enter` on fixed sleeps to auto-dismiss a trust dialog. Problems: (a) no check that a dialog actually exists — if the TUI rendered late and focus is still the main input, these keystrokes type garbage into the prompt; (b) if a *different* dialog (OAuth, list-select) appeared instead of the expected trust prompt, `Down`+`Enter` selects the wrong option; (c) `2>/dev/null || true` swallows all errors silently — the operator never learns delivery failed; (d) no `wait_for_tui_ready` gate before sending.
### Site D — `stop_session.sh:192` (`graceful_stop`) — RAW
```bash
tmux send-keys -t "$SESSION_NAME" "$exitkey" Enter 2>/dev/null || true
```
**Vulnerability**: Sends `/exit` + Enter without focus recovery. If a permission dialog is open, `/exit` is typed into the dialog (harmless there) but Enter may dismiss the dialog with an unintended choice, and the agent never receives the exit command. The graceful chain *does* have a proper fallback (kill-session → SIGTERM → SIGKILL with `has-session` checks, lines 194-205), so this site is low-severity — but it still benefits from focus recovery.
### Site E — `create/SKILL.md:215` — RAW, probe (documentation example)
```bash
tmux send-keys -t "$SESSION_NAME" "" Enter
```
**Vulnerability**: This is in a verification snippet (sends empty + Enter). Low impact — it's an optional manual probe, not an automated path. But it sets a bad example for users.
### Site F — `create_session.sh:384` — USES HELPER (Site A)
```bash
inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID"
```
**Status**: This is the correct pattern — it routes through `inject_instructions`. It's only as safe as Site A. Also note: `create_session.sh:199` calls `wait_for_tui_ready` before any input — the *only* site that does so.
### Existing mitigation already present (good, underused)
---
## 3. Proposed Prevention Helper: `send_keys_safe()`
Add to `lib.sh` immediately after `wait_for_tui_ready` (around line 1085). Design goals: (1) recover focus before every send, (2) verify delivery via capture-pane, (3) single source of truth replacing Sites AE, (4) no new dependencies, (5) respect the existing `local_tmux` / `TMUX_SERVER_NAME` isolation pattern.
```bash
# send_keys_safe <session> <text> [--enter] [--no-focus-recovery] [--verify]
#
# Focus-safe, delivery-verified tmux send-keys. Restores input focus to the
# main prompt before sending, then (optionally) verifies the text reached the
# pane. Replaces raw `tmux send-keys` and the duplicated paste-buffer blocks
# across create/resume/stop/delegate-job to fix the "prompt lock" issue:
# keystrokes landing in a dialog popup instead of the main input box.
#
# Args:
# <session> target tmux session/pane
# <text> text to send (use "" for a bare Enter)
# --enter append C-m (Enter) after the text
# --no-focus-recovery skip the Escape/Ctrl-C focus-reset preamble (rare; only
# for sending into a known-open dialog on purpose)
# --verify capture-pane after send and confirm <text> is present
# (substring match, first line only); returns 1 on miss
# Environment:
# TMUX_SERVER_NAME honored (same isolation as inject_instructions)
# Returns: 0 on success, 1 on verify-fail or tmux error.
send_keys_safe() {
local sess="$1" text="$2"
shift 2
local do_enter=0 do_recover=1 do_verify=0
while [ $# -gt 0 ]; do
case "$1" in
--enter) do_enter=1 ;;
--no-focus-recovery) do_recover=0 ;;
--verify) do_verify=1 ;;
*) echo "send_keys_safe: unknown arg: $1" >&2; return 2 ;;
esac
shift
done
local local_tmux="tmux"
if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then
local_tmux="tmux -L $TMUX_SERVER_NAME"
fi
# --- 1. Focus recovery: dismiss any dialog / popup / list-select -------------
# Escape dismisses most permission & list-selection popups back to the prompt.
# C-c cancels a half-typed line / pending OAuth prompt that may hold focus.
# A short settle lets the blessed renderer re-render the main input box.
if [ "$do_recover" -eq 1 ]; then
$local_tmux send-keys -t "$sess" Escape 2>/dev/null || true
$local_tmux send-keys -t "$sess" C-c 2>/dev/null || true
sleep 0.3
fi
# --- 2. Deliver text via paste-buffer (atomic, no char-loss on long input) ----
local buf="sk_safe_$$"
$local_tmux set-buffer -b "$buf" -- "$text"
$local_tmux paste-buffer -b "$buf" -t "$sess"
$local_tmux delete-buffer -b "$buf"
sleep 0.4 # let the TUI ingest the paste before Enter / verify
# --- 3. Optional Enter -------------------------------------------------------
if [ "$do_enter" -eq 1 ]; then
$local_tmux send-keys -t "$sess" C-m
sleep 0.3
fi
# --- 4. Optional delivery verification ---------------------------------------
if [ "$do_verify" -eq 1 ] && [ -n "$text" ]; then
local got
got=$($local_tmux capture-pane -p -t "$sess" 2>/dev/null || echo "")
# match the first line of <text> against the pane (avoids wrapping noise)
local first_line
first_line="$(printf '%s\n' "$text" | head -n1 | sed 's/[][\\.^$*+?(){}|]/\\&/g')"
if [ -n "$first_line" ] && ! printf '%s' "$got" | grep -Fq -- "$first_line"; then
echo "send_keys_safe: delivery verify FAILED for session '$sess'" >&2
return 1
fi
fi
return 0
}
```
And refactor `inject_instructions` to delegate to it (keeps the existing call sites working):
```bash
inject_instructions() {
local sess="$1" instructions="$2" job_id="${3:-onboard}"
# Reuse the focus-safe helper; paste + Enter + verify.
send_keys_safe "$sess" "$instructions" --enter --verify
}
```
### Why this design fixes each root cause
| Brief root cause | How `send_keys_safe` addresses it |
|------------------|-----------------------------------|
| Blessed renderer thread bottleneck | `sleep 0.3` after focus-reset + `sleep 0.4` after paste give the renderer time to re-render the main input box before Enter; `--verify` detects a stuck renderer (text absent → return 1 → caller can retry). |
| Dialog popups stealing focus (Approve/Reject) | The `Escape` + `C-c` preamble dismisses/cancels the popup first, returning focus to the main prompt. |
| OAuth / list-selection dialog blocks intercepting keystrokes | `Escape` exits list-selects; `C-c` cancels OAuth prompts; if a dialog still holds focus, `--verify` fails and the caller learns instead of silently swallowing. |
| Silent failure (`2>/dev/null \|\| true`) | `--verify` makes delivery failure observable; raw sites currently swallow all errors. |
---
## 4. Draft Migration Plan
Order matters: introduce the helper first (no behavior change), then migrate sites one at a time (each verifiable). Per AGENTS.md §3 (Surgical Changes), each step touches only its own site.
| Step | File | Change | Verify |
|------|------|--------|--------|
| M1 | `lib.sh` (~line 1085) | Add `send_keys_safe()`; refactor `inject_instructions()` to call it. | `bash -n lib.sh`; existing `inject_instructions` callers (create_session.sh:384) still work — run a create + delegate-job and confirm the prompt is delivered and Enter submits. |
| M2 | `multi-agent-mux-delegate-job` lines 366-369 | Replace the 5-line paste-buffer block with `send_keys_safe "$sess" "$instructions" --enter --verify`. Removes the duplicate (lib.sh "single source of truth" mandate). | Delegate a job to an existing live session; confirm `--verify` passes and the agent receives the full prompt. |
| M3 | `resume/SKILL.md` lines 150-156 | Replace the blind `sleep 5; send-keys Enter; sleep 3; send-keys Down; sleep 0.3; send-keys Enter` block with: `wait_for_tui_ready "$SESSION_NAME" claude` first, then a single `send_keys_safe "$SESSION_NAME" "" --enter` to dismiss the trust dialog if present. Drop the hardcoded `Down` (it picks an option blindly). Add a capture-pane check: only send the dismiss Enter if a dialog keyword (e.g. `trust`, `approve`, `bypass`) is visible. | Resume a stopped claude session; confirm the trust dialog is dismissed and the prompt is responsive, *without* a stray `Down` corrupting a non-trust dialog. |
| M4 | `stop_session.sh` line 192 | Replace `tmux send-keys -t "$SESSION_NAME" "$exitkey" Enter` with `send_keys_safe "$SESSION_NAME" "$exitkey" --enter` (no `--verify` needed — the existing kill-session fallback chain already verifies exit). | Run `stop_session.sh --graceful`; confirm graceful exit still falls back to kill-session correctly. |
| M5 | `create/SKILL.md` line 215 | Update the documentation probe example to `send_keys_safe "$SESSION_NAME" "" --enter --verify` so the docs teach the safe pattern. | `bash -n` on any snippet; doc review. |
### Non-Goals (out of scope, per AGENTS.md §2)
- Not adding a generic dialog-state machine — the `Escape`/`C-c` preamble + `--verify` covers the 3 root causes without over-engineering.
- Not removing `2>/dev/null || true` from the focus-reset preamble (those keystrokes are best-effort by design; the *delivery* path uses `--verify`, which is the observable contract).
- Not touching `wait_for_tui_ready` itself — it's correct; M1/M3 just extend its usage to resume.
### Risk
| Risk | Severity | Mitigation |
|------|----------|-----------|
| `Escape`/`C-c` preamble cancels a legitimate in-flight user input | Medium | `--no-focus-recovery` escape hatch for intentional dialog sends; default path is automated orchestration where the pane is owned by the script, not a human. |
| `--verify` false-negative on wrapped/colored prompts | Low | `first_line` substring + `grep -F` is tolerant; worst case returns 1 and caller retries — safer than silent swallow. |
| `sleep 0.4` too short on slow renderers | Low | `--verify` is the real gate, not the sleep duration; sleep is a best-effort settle. |
---
## 5. Summary
The prompt-lock issue is caused by **6 input-delivery sites, only 2 of which use a helper, none of which recover focus or verify delivery.** The highest-risk site is `resume/SKILL.md:150-156` (blind dialog auto-dismiss). The fix is a single `send_keys_safe()` helper in `lib.sh` (focus recovery via `Escape`+`C-c`, paste-buffer delivery, optional `--verify`) that becomes the single source of truth, with `inject_instructions` refactored to delegate to it and the 4 raw sites (delegate-job, resume, stop, create-docs) migrated in 5 surgical steps. The existing `wait_for_tui_ready` primitive is reused and extended to resume. No new dependencies; no over-engineering; each migration step is independently verifiable.
`lib.sh:1040-1084` defines `wait_for_tui_ready <sess> <agent>` — a gated capture-pane loop (15×1s) that grep-checks the pane content for each agent's TUI banner before returning. This is exactly the right primitive, but it is **only called in `create_session.sh:199`**. Resume, stop, and delegate-job never gate on TUI readiness.
@@ -1,85 +0,0 @@
# Prompt-Lock Fix — Final Implementation Review
- **Reviewer**: Reviewer Cline (`canary-projects-multi-agent-mux-reviewer-cline`, role: reviewer)
- **Date**: 2026-07-11
- **Brief**: `.mam/reports/brief-rereview-prompt-lock.md`
- **Commit reviewed**: `e613f4a` — "fix(skills): evidence-based prompt delivery (send_keys_safe) to end prompt-lock (FW-W2)"
- **Authorized plan**: `.agents/reports/canary-projects-multi-agent-mux-planner-claude/report-prompt-lock-plan.md` (MS-1 through MS-12)
- **Files in scope**: `lib.sh`, `create_session.sh`, `multi-agent-mux-delegate-job`, `resume/SKILL.md`, `stop_session.sh`, `create/SKILL.md`
---
## 1. Verdict: **PASS** ✅
The implementation in commit `e613f4a` conforms to the authorized plan (MS-1 through MS-12). All 4 shell scripts pass `bash -n`; no new shellcheck warnings; all post-commit sanity checks (T5) pass; the delegate-job duplicate is retired; FW-W2 is marked resolved in both languages. One minor, functionally-equivalent placement deviation in MS-3 (documented below) — not a defect.
---
## 2. Per-MS Adherence Audit
| MS | Spec (plan) | Implemented (commit) | Match |
|----|-------------|----------------------|-------|
| **MS-1** | Insert §1 helper block after lib.sh line 1100 | `_sks_tmux`, `_pane_capture`, `_pane_quiescent`, `_pane_dialog_open`, `send_keys_safe`, `handle_startup_dialogs` inserted after `inject_instructions` | ✅ Verbatim |
| **MS-2** | claude regex: drop `Dangerously\|dangerously\|Enter``"Anthropic\|Assistant\|Chat\|Welcome\|projects"` | lib.sh:1058 now `grep -E -q "Anthropic\|Assistant\|Chat\|Welcome\|projects"` — three dialog-ambiguous tokens removed | ✅ Exact |
| **MS-3** | Inside retry loop, before the `case`: `if _pane_dialog_open "$sess"; then sleep 1; continue; fi` | lib.sh:1049-1052 — inserted at **top of loop, before the capture** (plan said "after the capture") | ✅ Functionally equivalent¹ |
| **MS-4** | `"⚠️ Warning: ... Proceeding anyway..."``"⚠️ TUI readiness check timed out for '$sess'." >&2; return 1` | lib.sh:1085-1086 — exact text + `return 1` | ✅ Exact |
| **MS-5** | Rewrite `inject_instructions` as thin wrapper: `send_keys_safe "$1" "$2" "${3:-onboard}"` | lib.sh:1090-1092 — exact + delegation comment | ✅ Exact |
| **MS-6** | create_session.sh:199 → explicit `if ! wait_for_tui_ready ...; then echo ERROR; exit 1; fi` | create_session.sh:199-202 — exact guard; EXIT trap rolls back | ✅ Exact |
| **MS-7** | create_session.sh:384 → guard injection: on fail publish `error` event + `exit 1` | create_session.sh:387-390 — `delegate_publish_event ... error ...; exit 1`; `started` only after verified delivery | ✅ Exact |
| **MS-8** | delegate-job:364-369 → `source "$SCRIPT_DIR/../lib.sh"` + `send_keys_safe` + `return 1`; keep local `_tmux` | delegate-job:365-369 — sources lib.sh, calls `send_keys_safe`, returns 1; `_tmux` retained at 347-349 | ✅ Exact |
| **MS-9** | resume/SKILL.md:150-156 → `handle_startup_dialogs "$SESSION_NAME" 20` | resume/SKILL.md:151 — exact; blind Enter/Down/Enter removed | ✅ Exact |
| **MS-10** | stop_session.sh:192 → `send_keys_safe ... "stop$$" \|\| echo "graceful: safe delivery failed..."` | stop_session.sh:192 — exact; SIGTERM→SIGKILL fallback byte-identical | ✅ Exact |
| **MS-11** | create/SKILL.md:214-215 → passive `capture-pane` probe, no stray Enter | create/SKILL.md:214-215 — `capture-pane` + comment; stray `send-keys "" Enter` removed | ✅ Exact |
| **MS-12** | FUTURE_WORKS.md:25 + .ko.md:24 → mark FW-W2 resolved, strikethrough + date | Both: `~~**FW-W2**~~` + "✅ RESOLVED (2026-07-11)" | ✅ Exact |
---
## 3. Syntax & Safety Validation (ran in-session)
| Check | Command | Result |
|-------|---------|--------|
| lib.sh syntax | `bash -n .agents/skills/lib.sh` | ✅ OK |
| create_session.sh syntax | `bash -n .../create_session.sh` | ✅ OK |
| stop_session.sh syntax | `bash -n .../stop_session.sh` | ✅ OK |
| delegate-job syntax | `bash -n .../multi-agent-mux-delegate-job` | ✅ OK |
| shellcheck (no *new* findings) | `shellcheck -S warning` on all 4 | 2 findings (SC2164 lib.sh:1030/1033, SC2155 stop_session.sh:71) — **all pre-existing** (parent commit lib.sh has same 3 SC findings); plan T2 allows pre-existing out of scope. ✅ No new findings |
| T5a: "Proceeding anyway" removed | `grep -n 'Proceeding anyway' lib.sh` | ✅ no match (rc=1) |
| T5b: `sleep 0.5` removed from delegate-job | `grep -rn 'sleep 0.5$' .../delegate-job` | ✅ no match (rc=1) |
| Working tree (post-commit) | `git status --short` | ✅ only untracked review edits, no stray changes |
---
## 4. Helper Logic Review
### `send_keys_safe` (lib.sh:1140-1175)
- **Marker (A1)**: `printf '%s' "$text" | tr -d '\r' | awk 'NF {line=$0} END {print line}' | tail -c 24` — last 24 chars of last non-empty line. Fixes Creator's `head -c 200 | tail -c 24` newline-straddle bug. ✅
- **Quiescence (RC-A)**: `_pane_quiescent` requires two consecutive identical non-empty captures — evidence-based, no magic sleep. ✅
- **Dialog refusal (RC-B/C)**: `while _pane_dialog_open` loop with `SKS_DIALOG_TIMEOUT` (default 30s) deadline; optional `SKS_DIALOG_ESCAPE=1` sends one Escape per poll; **never a blind Enter**. Returns exit 2 on timeout. ✅
- **Paste verify**: `grep -Fq "$marker"` after paste-buffer; returns 3 if not visible. ✅
- **Submit verify (A2)**: marker left bottom-3-lines **AND** pane changed vs pre-submit snapshot; 3 retries with increasing sleeps. Defeats transcript-echo false-fail. ✅
- **Exit codes 1-4**: distinct, documented; all callers guard non-zero (MS-6/7/8/10). ✅
### `handle_startup_dialogs` (lib.sh:1191-1206)
- Signature-gated: sends `Enter` only when `Do you trust the files` visible, `Down`+`Enter` when `Yes, proceed` visible, returns on TUI banner. No blind keys. Matches A4 (separate accept-policy helper, not `send_keys_safe` which refuses dialogs). ✅
### `wait_for_tui_ready` (lib.sh:1040-1086)
- Dialog-skip via `_pane_dialog_open` (MS-3) — open dialogs = not-ready. ✅
- Token cleanup (MS-2) — `Dangerously/dangerously/Enter` dropped. ✅
- Hard fail on timeout (MS-4) — `return 1`; enables MS-6 rollback. ✅
---
## 5. DoD-5 Note (Signature Token Validation — the merge blocker)
The plan marked DoD-5 (validate `_pane_dialog_open` / `handle_startup_dialogs` tokens against real `capture-pane` output) as a **hard merge blocker**. The commit was made, implying the executor performed this validation. I cannot independently re-validate without a live agent TUI in this session. The tokens (`Do you trust the files`, `Yes, proceed`, `No, exit`, `Allow this`, `Press Enter to continue`, `browser to authenticate`, `Use arrow keys`, `Esc to cancel`) are plausible claude TUI dialog signatures.
**Recommendation**: the commit message or a follow-up note should record the real-capture evidence that DoD-5 was satisfied (TUI build, confirmed tokens). If DoD-5 was *not* performed, this is the one residual risk — but it does not affect the code's structural conformance to the plan.
---
## 6. Summary
Commit `e613f4a` is a faithful, surgical implementation of the authorized MS-1MS-12 plan. All 12 mod-sites match (one trivial placement deviation in MS-3 that is functionally equivalent and plan-text-ambiguous). All 4 shell scripts pass `bash -n`; no new shellcheck warnings; the duplicated delegate-job paste block is retired (restoring lib.sh's single-source-of-truth mandate); FW-W2 is marked resolved in both EN and KO; the post-commit T5 sanity greps confirm "Proceeding anyway" and `sleep 0.5` are gone. The three root causes (renderer bottleneck RC-A, dialog focus-steal RC-B, OAuth/list-select RC-C) are each addressed by an evidence-based mechanism (quiescence, dialog refusal+timeout, marker verification). The only residual is the DoD-5 real-capture validation, which is an environmental confirmation rather than a code defect.
**Final Verdict: PASS** ✅ — implementation conforms to MULTI_AGENT_RULES.md, AGENTS.md (Simplicity First, Surgical Changes — every changed line traces to a verified defect site), and the authorized `report-prompt-lock-plan.md`.
¹ **MS-3 deviation (minor, non-blocking)**: Plan specified the dialog-skip `if` "after the capture" (between `capture-pane` and `case`); implementation places it at loop top, before capture. Functionally equivalent (skips the unnecessary capture too); plan text is internally ambiguous ("before the `case`" vs "after the capture"). No behavior difference. Not a defect.
@@ -1,96 +0,0 @@
# Review Report — 세션 ID 격리 구현 (Session Isolation)
- **Reviewer**: Reviewer Cline (`canary-projects-multi-agent-mux-reviewer-cline`, role: reviewer)
- **Date**: 2026-07-10
- **Subject commit**: `768cfe5 feat(isolation): implement Phase 1-3 session isolation with stop purge and resume safety`
- **Range reviewed**: `HEAD~3..HEAD` (commit `768cfe5` + 2 docs commits). ※ The user-requested `HEAD~2..HEAD` only spans the two docs commits and excludes the implementation commit itself (it sits *at* `HEAD~2`, excluded by an exclusive range); I expanded to `HEAD~3..HEAD` to cover the actual code, which is the evident intent.
- **Governing documents**: `AGENTS.md`, `.agents/MULTI_AGENT_RULES.md` / `.ko.md`, `implementation_plan.session_isolation.md` (Rev.3), `task.session_isolation.md`
---
## Verdict: **PASS** ✅
The session-isolation implementation conforms to all three governing documents. All code implementation tasks (T1T6), including the RK2 resume re-apply fix and the T6 stop-purge with path safeguards, are complete and correct. `bash -n` passes; `shellcheck` introduces **zero new warnings** (V5 PASS); the non-isolated regression path is byte-identical (V4 PASS). Remaining unchecked items in `task.session_isolation.md` are live-integration verification steps (V1V3, T-V2) and an explicitly-optional orphan GC (T6-b) — they are process gates, not code defects, and are honestly flagged as pending by the developer.
---
## 1. Conformance to `implementation_plan.session_isolation.md` (Rev.3)
### Phase 0 — 검증 게이트 (G2 프로브) ✅
- All G2 probes (`G2-C1/C2`, `G2-L1`, `G2-A1`, `G2-H1`, `G2-M`) are marked `[x]` in `task.session_isolation.md`.
- §2.3 매트릭스 populated with per-agent lever, seed list, conversation path — including the two Rev.3 corrections: cline `--config` insufficient (seeding required) and cline isolated layout `<root>/sessions/` (not `data/sessions/`).
### Phase 1 — 공통 불변식 (T1/T2) ✅
- **T1 claimed-set filter** (`lib.sh:543-573`): `running_ids` set built from SQLite (DB-first) with YAML fallback; `emit()` filters any candidate already claimed by *another* running row. Target row's own IDs are excluded from the filter (`if target and s_data.get('name') == target: continue`) — fixes the self-ID filter bug noted in T5.
- **T2 생성-시 유일성 assert** (`lib.sh:386-399`): iterates running rows, raises `SystemExit` on duplicate `*_own` across distinct sessions. Matches plan §2.4 R2.
### Phase 2 — all-L2 격리 구현 (T3/T4/T5) ✅
- **T3 provisioning** (`create_session.sh:119-135`, `lib.sh:818-856`): `uuidgen``.mam/agent_homes/<uuid>/`, per-agent symlink seeding, `seeded` list returned. Dry-run guard + error-trap rollback with path guard `*/.mam/agent_homes/*` before `rm -rf`.
- **T4 spawn injection** (`lib.sh:858-882`, `create_session.sh:142-169`): single dispatch via `isolation_env_prefix` (claude `CLAUDE_CONFIG_DIR`, agy/hermes `HOME`) and `isolation_cmd_args` (cline `--data-dir`). Applied to `CMD_FULL`, `spawn()`, and `START_CMD`. When `ISOLATE=0`, `ISO_ENV_PREFIX=""`/`ISO_CMD_ARGS=""` → strings reduce to exact pre-commit values (byte-identical, V4).
- **T5 schema persistence + re-apply** (`create_session.sh:311-320`, `lib.sh:501-504, 613-661`): `isolation: {uuid, root, lever, seeded[]}` written via `atomic_dump_yaml`; `_validate` enforces `uuid`/`root` required. `find_workspace_uuid` target-mode resolves strictly within the row's isolation root — **no global fallthrough** (the C3 mtime bug is structurally eliminated). cline path template `<root>/sessions/` correctly branched.
### RK2 resume re-apply (previously flagged blocking — now fixed) ✅
- `resolve_session_id.sh` accepts `--session` and forwards to `find_workspace_uuid "$WORKSPACE" "$AGENT" "$SESSION_NAME"`.
- `resume/SKILL.md` workflow now: (1) passes `--session "$SESSION_NAME"`, (2) resolves the row's `isolation` block via a Python helper, (3) re-derives `ISO_ENV`/`ISO_ARGS` via `isolation_env_prefix`/`isolation_cmd_args`, (4) prepends/appends to `CMD_FULL` before spawn. This satisfies T5's "저장+재적용은 원자적 세트" contract — the save path (create) and re-apply path (resume) share the same dispatch functions.
### Phase 3 — stop 청소 (T6) ✅
- `stop_session.sh:273-288`: purge path guarded by **two** checks: (a) `abspath(iso_root) == abspath(expected_iso_root)` where `expected_iso_root = <ws>/.mam/agent_homes/<iso_uuid>`, and (b) `startswith(expected_homes_dir + os.sep)`. Mismatch → `WARN`, no delete.
- `capture_conversation_id` and `find_workspace_uuid` calls now pass `$SESSION_NAME` → isolated rows resolve within their own root, preventing cross-row UUID theft on purge.
- T6-a (symlink original preservation): `rmtree` operates on `iso_root` only; symlink targets (real `~/.claude/.credentials.json` etc.) are outside `iso_root` and untouched. ✅
### Phase 4 — 회귀 및 검증
| ID | Criterion | Result |
|----|-----------|--------|
| V4 | Non-isolated path byte-identical (regression 0) | ✅ PASS — verified: `ISOLATE=0``CMD_FULL` equals pre-commit strings exactly |
| V5 | `bash -n` + `shellcheck` new warnings = 0 | ✅ PASS — `bash -n` OK; shellcheck diff vs `768cfe5~1` baseline shows **0 new** (all 10 findings pre-existing, line-shifted only) |
| V1V3, T-V2 | Live 4-agent simultaneous spawn + resume isolation | ⏳ Pending — explicitly marked `[ ]` by developer; scratch functional tests passed, live TUI multi-spawn remaining. Process gate, not a code defect. |
| T6-b | Orphan GC (optional) | ⏳ Explicitly marked 선택(optional) in plan |
---
## 2. Conformance to `AGENTS.md`
### Simplicity First ✅
- No speculative features. `provision_isolation` is a per-agent `case`, not an over-generalized plugin system.
- `isolation_env_prefix`/`isolation_cmd_args` are minimal one-liners — no abstraction beyond the 4-agent matrix.
- Path guards use the simplest correct construct (`case` glob + `os.path.abspath` equality/startswith). No over-engineered allowlist framework.
### Surgical Changes ✅
- Every changed line traces to a plan task (T1→T5, RK2, T6). No drive-by refactors.
- Pre-existing shellcheck findings (SC2164 `cd || exit`, SC2034 unused `i`, SC2155 declare+assign, SC1091 source-not-followed) were **not touched** — correct per "Don't refactor things that aren't broken" and "Don't remove pre-existing dead code unless asked."
- Non-isolated branches preserve original control flow and string literals verbatim.
### Think Before Coding ✅
- The Phase 0 gate was honored: no Phase 2 code before the G2 matrix was finalized (plan §5 "Phase 0 게이트 통과 전 코드 구현 착수 금지").
- Rev.3 decision log (D1D5) records the L1→all-L2 tradeoff explicitly rather than silently picking.
### Goal-Driven Execution ✅
- `task.session_isolation.md` DoD items map 1:1 to plan tasks with verifiable criteria. Developer self-marked `[x]` only where evidence exists and left `[ ]` where verification is genuinely incomplete (honest reporting).
---
## 3. Conformance to `MULTI_AGENT_RULES.md`
- **Isolation root** (`.mam/agent_homes/<uuid>/`) sits under `.mam/``.gitignore` covered; included in the stop-cleanup contract (T6). ✅
- **Symlink seeding** (not copy) satisfies RK4 (token-refresh convergence on the real file) and avoids auth-divergence. ✅
- **`_validate` schema enforcement** for the `isolation` block (uuid/root required) keeps the registry internally consistent. ✅
- **Role isolation**: the implementation does not alter `role` semantics or cross into reviewer/PM authority — it is purely infrastructure (developer-team scope). ✅
- This report is persisted under `.mam/reports/<tmux_session_name>/` per the markdown-collaboration protocol. ✅
---
## 4. Observations (non-blocking, for record)
1. **hermes `config.yaml` embedded absolute paths** (plan §2.3 note 3): the implementation symlinks `config.yaml` into the isolation root; hermes runtime resolves the symlink to the real config, which may reference real-HOME paths. The plan explicitly documents this as "읽기 공유라 무해하나, 격리 범위가 state/세션에 한정됨" — hermes isolation is scoped to `state.db`/sessions, not full config. Implementation matches the documented limitation. **Not a defect.**
2. **cline isolated layout divergence** (`<root>/sessions/` vs real `~/.cline/data/sessions/`): correctly handled by `cline_exists(uuid, iso)` and the target-mode glob `f"{iso}/sessions/*"`. Matches plan §2.3 note 2. ✅
3. **Orphan GC (T6-b)** remains unimplemented but is explicitly optional in the plan; `.mam/` is gitignored and covered by `remove.sh` whole-tree cleanup. Not blocking.
---
## 5. Final Statement
The implementation is complete, correct against the approved Rev.3 plan, and respects `AGENTS.md` (Simplicity First / Surgical Changes) and `MULTI_AGENT_RULES.md`. The previously-blocking RK2 resume re-apply gap is resolved. Static verification (V4 byte-identical regression, V5 bash-n + shellcheck-new=0) passes. The remaining unchecked items are live-integration verification steps the developer has transparently left open — they do not represent code-level non-conformance.
**PASS** — approved for merge. Recommend the developer proceed with V1V3/T-V2 live-spawn integration verification as a follow-on gate before production rollout, per the plan's Phase 4 DoD.
@@ -1,551 +0,0 @@
# Report: Multi-Agent Mux Skill Optimization Analysis
- **Author**: Reviewer Cline (`canary-projects-multi-agent-mux-reviewer-cline`)
- **Scope**: All shell scripts under `.agents/skills/` (8 files, ~3,422 lines)
- **Audited at**: `25de01e fix(skills): solve set -e error propagation in create_session.sh and include final PASS reviews`
- **Brief**: `.mam/reports/brief-skill-optimization-analysis.md`
## Files audited
| File | Lines | Role |
|---|---|---|
| `lib.sh` | 1212 | Shared library (session-name, atomic YAML dump, UUID resolver, tmux isolation, send_keys_safe) |
| `multi-agent-mux-create/scripts/create_session.sh` | 408 | Session creation + onboarding job injection |
| `multi-agent-mux-delegate-job/multi-agent-mux-delegate-job` | 440 | User-facing job orchestrator (submit/status/list/verify/wait/logs) |
| `multi-agent-mux-monitor/scripts/reconcile.sh` | 644 | YAML<->tmux<->disk drift detection + MQTT subscriber / polling fallback |
| `multi-agent-mux-stop/scripts/stop_session.sh` | 370 | Graceful/hard stop + conversation purge |
| `multi-agent-mux-status/scripts/status.sh` | 140 | Read-only status table (reuses reconcile --dry-run) |
| `multi-agent-mux-resume/scripts/resolve_session_id.sh` | 44 | UUID resolver wrapper |
| `multi-agent-mux-resume/scripts/update_yaml_resumed.sh` | 164 | Resume YAML updater |
All scripts declare `#!/usr/bin/env bash` and `set -euo pipefail`, and `source` `lib.sh` via a `BASH_SOURCE`-relative path. The codebase is internally consistent under bash; findings below distinguish "broken under bash" (real bugs) from "non-portable / latent risk" (works today, breaks if assumptions change).
---
## Focus Area 1 - Inefficient Polling / Sleeps
### 1.1 `reconcile.sh:243-256` - MQTT event loop spins on `time.sleep(0.5)` (highest impact)
```python
start = time.time()
try:
while True:
now = time.time()
if timeout and (now - start) >= timeout: ...
if idle_timeout and (now - state['last_msg']) >= idle_timeout: ...
time.sleep(0.5) # <-- busy-wait defeating the event loop
finally:
client.loop_stop()
```
`paho-mqtt` already runs a background network thread via `client.loop_start()` (line 241). The foreground `while True: time.sleep(0.5)` is a **busy poll layered on top of an event-driven client**. It wakes every 0.5 s solely to compare timestamps.
**Diagnosis**: This is the clearest "polling where an event-driven primitive exists" case. The loop does no I/O; it only enforces two deadlines (overall `timeout`, `idle_timeout`).
**Proposed fix**: Replace the spin with `client.loop_forever()` for the blocking model, or compute the next deadline and `client.loop(min/max)` / `select` on the broker socket with a computed timeout. The cleanest minimal change keeps `loop_start()` and blocks the main thread on a condition variable instead of polling `time.time()`:
```python
import threading
stop = threading.Event()
# set stop when timeout/idle reached (timer callback or on_message watchdog)
stop.wait(timeout=next_deadline - now)
```
Either removes the 0.5 s wake-ups entirely while preserving the two deadline semantics.
### 1.2 `multi-agent-mux-delegate-job:119` and `:205` - `sleep 1` to win a race
```bash
"$PY" "$SCRIPT_DIR/scripts/job_subscriber.py" ... >"$logf" 2>&1 &
local sub_pid=$!
sleep 1 # give the subscriber time to CONNACK + SUBSCRIBE before the agent runs
run_agent "$JOB_ID" "$instructions"
```
A fixed 1 s sleep papers over the Subscribe-before-Publish ordering dependency (MQTT does not queue non-retained messages for absent subscribers). 1 s is simultaneously too short on a slow/loaded broker and wastefully long on a fast one.
**Diagnosis**: Genuine readiness race, not a renderer wait. The subscriber knows when it has SUBSCRIBED (`on_connect` fires, sets `state['connected']=True`), but that signal is trapped inside the subscriber process and never surfaces to the orchestrator.
**Proposed fix**: Have `job_subscriber.py` write a readiness token (e.g. create `$logf.ready` or print a `READY <jid>` line and flush) once `on_connect` succeeds and the SUBSCRIBE ack returns. The orchestrator then polls for that token with a short deadline (up to 5 s at 0.1 s granularity) instead of a blind `sleep 1`. Falls back to the existing 1 s if the token never appears. This is still polling, but it is **evidence-driven** (the subscriber asserts it is ready) rather than time-driven.
### 1.3 `stop_session.sh:193,200` - fixed `sleep 3` / `sleep 5` in graceful fallback chain
```bash
send_keys_safe "$SESSION_NAME" "$exitkey" "stop$$" || ...
sleep 3
if ! tmux has-session ...; then return 0; fi
tmux kill-session -t "$SESSION_NAME" ...
sleep 5
if ! tmux has-session ...; then return 0; fi
kill -9 "$pane_pid"
```
Two fixed waits after actions that have an observable outcome (`tmux has-session` flips false).
**Diagnosis**: Fixed 3 s + 5 s = up to 8 s of unconditional waiting even when the session dies in 50 ms.
**Proposed fix**: Replace each fixed sleep with a bounded poll:
```bash
_wait_gone() { # <sess> <deadline_sec>
local sess="$1" deadline=$(( $(date +%s) + "$2" ))
while [ "$(date +%s)" -lt "$deadline" ]; do
tmux has-session -t "$sess" 2>/dev/null || return 0
sleep 0.3
done
return 1
}
```
Then `_wait_gone "$SESSION_NAME" 3 || tmux kill-session ...`. Total worst case stays 8 s, but the happy path returns in ~0.3 s.
### 1.4 `lib.sh:1048-1084` `wait_for_tui_ready` - fixed 1 s poll, bash brace expansion
```bash
for i in {1..15}; do
if _pane_dialog_open "$sess"; then sleep 1; continue; fi
content=$($local_tmux capture-pane -p -t "$sess" 2>/dev/null || echo "")
...grep for banner...
sleep 1
done
```
15 x 1 s = 15 s worst case. The per-iteration `sleep 1` is coarse; a TUI that renders in 0.4 s still costs 1 s of dead time per miss.
**Diagnosis**: Polling is unavoidable here (a TUI emits no readiness event), but the 1 s granularity is arbitrary. Also uses bash-only `{1..15}` brace expansion.
**Proposed fix**: Switch to a deadline-bounded loop with a 0.3-0.5 s interval:
```bash
local deadline=$(( $(date +%s) + 15 )) i
while [ "$(date +%s)" -lt "$deadline" ]; do
...checks...
sleep 0.3
done
```
Keeps the 15 s ceiling, halves the average detection latency, drops the bash brace expansion. (Minor - already works; included for completeness.)
### 1.5 `lib.sh:1174` - magic `sleep 0.5` after paste-buffer
```bash
_tmux paste-buffer -b "sks_$job_id" -t "$sess"
_tmux delete-buffer -b "sks_$job_id" 2>/dev/null || true
sleep 0.5
_pane_capture "$sess" | grep -Fq "$marker" || { ... return 3; }
```
A fixed 0.5 s wait so the pasted text becomes visible before the marker grep.
**Diagnosis**: Could reuse the existing `_pane_quiescent` primitive (lib.sh:1122) - two consecutive identical captures imply the renderer settled, which is the actual precondition for the marker being readable.
**Proposed fix**: `if ! _pane_quiescent "$sess" 8 0.2; then ... return 3; fi` before the grep, or drop the `sleep 0.5` and retry the marker grep a few times with 0.1 s spacing. Removes the magic constant in favor of an evidence-based wait already in the library.
### 1.6 Summary - sleeps that are fine
- `lib.sh:1128` `_pane_quiescent` `sleep "$interval"` (0.5 s) - already a parameterized, evidence-bounded poll (returns on two identical captures). OK.
- `lib.sh:1159-1168` dialog-wait loop - already deadline-bounded (`SKS_DIALOG_TIMEOUT`). Ok.
- `reconcile.sh:236` `time.sleep(0.1)` connect-wait - bounded by 5 s. Ok.
- `reconcile.sh:273` `sleep "$POLL_INTERVAL"` (15 s) - documented polling fallback when broker is down; acceptable, though it could also probe for broker recovery between polls.
---
## Focus Area 2 - Helper Duplication & Modularization
### 2.1 Three reimplementations of "tmux with `-L <server>`" (highest duplication)
The same 4-line "pick bare `tmux` vs `tmux -L $TMUX_SERVER_NAME`" decision appears as:
| Location | Form |
|---|---|
| `lib.sh:89-96` | `_tmux()` function (calls `_init_tmux_isolation` first, resolves real binary) |
| `lib.sh:1105-1111` | `_sks_tmux()` function (server-aware, no shim init) |
| `lib.sh:1043-1046` | inline `local_tmux="tmux"; if ...; then local_tmux="tmux -L ..."; fi` inside `wait_for_tui_ready` |
| `stop_session.sh:182,188,195` | bare `tmux ...` calls (relies on the PATH shim from `_init_tmux_isolation`, which `stop_session.sh` never calls - it only exports `TMUX_SERVER_NAME`) |
**Diagnosis**: `_tmux` and `_sks_tmux` differ only in whether they run the shim-init side effect. `wait_for_tui_ready` reinvents the wheel inline. `stop_session.sh` quietly depends on a shim that may not be installed (it sources `lib.sh`, which calls `_init_tmux_isolation` only lazily inside `_tmux()` - and `stop_session.sh` calls bare `tmux`, never `_tmux`).
**Proposed fix**: Collapse to a single server-aware dispatcher in `lib.sh`:
```bash
# One entry point. Callers that need the shim auto-install use _tmux;
# _sks_tmux stays as the no-init variant for hot paths. Both delegate to
# _tmux_with_server below.
_tmux_with_server() {
if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then
tmux -L "$TMUX_SERVER_NAME" "$@"
else
tmux "$@"
fi
}
```
Then make `wait_for_tui_ready` call `_tmux_with_server` (drop the inline `local_tmux`), and convert `stop_session.sh`'s bare `tmux` calls to `_tmux_with_server` so they no longer depend on the PATH shim. This also fixes the latent bug where `stop_session.sh` kills the wrong server if the shim isn't on PATH.
### 2.2 DB + YAML state-load boilerplate duplicated 7+ times
This ~20-line block is copy-pasted nearly verbatim:
```python
db_path = os.path.splitext(yaml_path)[0] + '.db'
d = {}
try:
if os.path.exists(db_path):
conn = sqlite3.connect(db_path, timeout=60.0)
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
if row: d = json.loads(row[0])
try:
cursor = conn.execute('SELECT data FROM sessions')
for r in cursor.fetchall(): db_sessions.append(json.loads(r[0]))
d['tmux_sessions'] = db_sessions
except sqlite3.OperationalError: pass
conn.close()
elif os.path.exists(yaml_path):
with open(yaml_path) as f: d = yaml.safe_load(f) or {}
except Exception: pass
```
Instances (line ranges are the full block):
| File | Lines | Notes |
|---|---|---|
| `lib.sh` (`resolve_tmux_server`) | 112-152 | per-row lookup variant |
| `lib.sh` (`find_workspace_uuid`) | 585-611 | |
| `lib.sh` (agent_identities load) | 748-761 | |
| `stop_session.sh` | 87-113 | |
| `status.sh` | 42-62 | |
| `update_yaml_resumed.sh` | 66-91 | |
| `reconcile.sh` | 298-321 | |
**Diagnosis**: Each copy has slightly different error handling (some `pass`, some `print(WARN)`, some swallow `sqlite3.OperationalError` for the `sessions` table, some don't query it). This drift is exactly the inconsistency `lib.sh` was created to prevent (its own header, lines 4-9, calls out "four things inconsistently re-implemented"). The load logic is now a fifth.
**Proposed fix**: Add a `lib.sh` helper that emits a JSON document of the merged state to stdout, callable from any script without re-sourcing Python:
```bash
# load_state_json - prints the merged {state + sessions table / yaml} as JSON.
# Callers parse with python -c or jq. Single source of truth.
load_state_json() {
YAML_PATH="$AGENT_SESSIONS_YAML" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
import os, json, sqlite3, yaml
yaml_path = os.environ['YAML_PATH']
db_path = os.path.splitext(yaml_path)[0] + '.db'
d = {}
# ... one canonical implementation with structured errors ...
print(json.dumps(d, ensure_ascii=False))
PYEOF
}
```
Scripts then pipe the JSON into their per-row logic. The canonical copy owns the error policy (see Focus Area 4.1).
### 2.3 `*_exists` artifact probes trapped inside a heredoc
`jsonl_exists`, `db_exists`, `hermes_exists`, `cline_exists`, `own_exists` (lib.sh:509-540) are defined **inside** the `find_workspace_uuid` heredoc, so they vanish when that Python process exits. `status.sh:70-86` (`resume_on_disk`) and `reconcile.sh:498-623` reimplement the same existence checks inline.
**Diagnosis**: Four agent-specific "does this conversation artifact exist" predicates are a natural shared module but live in a single-use heredoc.
**Proposed fix**: Move them into a small `lib.py` (sibling of `lib.sh`) that every heredoc imports via `sys.path.append`. Then `status.sh` and `reconcile.sh` call `libpy.artifact_exists(agent, uuid, iso_root)` instead of re-rolling the paths. This is the same relocatable-path discipline `lib.sh` already uses.
### 2.4 `infer_agent_from_session` duplicated verbatim
`stop_session.sh:74-82` and `update_yaml_resumed.sh:39-47` are **byte-identical**:
```bash
case "$SESSION_NAME" in
*-creator-claude) AGENT=claude ;;
*-creator-agy) AGENT=agy ;;
*-creator-hermes) AGENT=hermes ;;
*-creator-cline) AGENT=cline ;;
*) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;;
esac
```
**Proposed fix**: Add to `lib.sh`:
```bash
infer_agent_from_session() {
case "$1" in
*-creator-claude) printf 'claude' ;; *-creator-agy) printf 'agy' ;;
*-creator-hermes) printf 'hermes' ;; *-creator-cline) printf 'cline' ;;
*) echo "ERROR: cannot infer agent from '$1'; pass --agent" >&2; return 2 ;;
esac
}
```
Callers: `AGENT=$(infer_agent_from_session "$SESSION_NAME") || exit $?`.
### 2.5 `_delegate_py_bin` vs `pick_python` - divergent venv walks
- `lib.sh:944-960` `_delegate_py_bin` walks up from `BASH_SOURCE` dir looking for `.venv/bin/python`, caches in `AGENT_PYTHON_BIN` (shell var, not exported - correct per DONE.md FW-08).
- `multi-agent-mux-delegate-job:27-45` `pick_python` walks `WORKDIR/.venv` then `./.venv` then `python3`, and adds a `paho.mqtt` import check.
**Diagnosis**: Two different walk strategies for the same concept ("find the project venv python"). `_delegate_py_bin` walks *up* from the skill dir; `pick_python` walks from `WORKDIR`/cwd. They can return different interpreters. `pick_python` also re-runs the `import paho.mqtt` check on every call (cheap but redundant with caching).
**Proposed fix**: Unify - `_delegate_py_bin` should accept an optional starting dir, and `pick_python` should call it then add the `paho.mqtt` check. One walk strategy, one cache.
---
## Focus Area 3 - Portability & POSIX Compliance
The brief calls out "bash-isms when running under raw sh" and "BASH_SOURCE under non-bash shells like zsh". Every audited script has `#!/usr/bin/env bash`, so **direct execution is safe**. The risks are (a) a user sourcing a script from zsh/fish interactively, and (b) future shebang changes. Findings ordered by likelihood.
### 3.1 `BASH_SOURCE` unbound under zsh/dash if sourced (P1, latent)
`BASH_SOURCE` is used in 12 places (lib.sh:17, 950, 966; create:22; delegate-job:17; reconcile:17,48; resolve:12; update_yaml:10; status:10,12,29; stop:32). Under zsh, `${BASH_SOURCE[0]}` is unset -> `dirname ""` -> the `cd ""` either fails or lands in `$PWD`, silently sourcing the wrong `lib.sh` or none.
**Diagnosis**: The shebang protects `bash script.sh` execution. The real exposure is a user typing `source .agents/skills/lib.sh` from an interactive zsh (common on macOS where the default shell is zsh). This is the exact scenario the brief names.
**Proposed fix**: Add a portable fallback at the top of `lib.sh`:
```bash
# Portable script-dir resolution: BASH_SOURCE under bash, $0 under POSIX sh,
# ${(%):-%x} under zsh. Falls back to $0.
if [ -n "${BASH_SOURCE[0]:-}" ]; then
_src="${BASH_SOURCE[0]}"
elif [ -n "${ZSH_VERSION:-}" ]; then
eval '_src="${(%):-%x}"'
else
_src="$0"
fi
SKILL_DIR="$(cd "$(dirname "$_src")" && pwd)"
```
Alternatively, guard the whole library with `if [ -z "${BASH_VERSION:-}" ]; then echo "lib.sh requires bash" >&2; return 1 2>/dev/null || exit 1; fi` and document that scripts must be executed, not sourced, from foreign shells. The marker-file root lookup (FUTURE_WORKS FW-P6) would also remove the fragile `../..` depth assumptions.
### 3.2 `status.sh:29` - 4-level relative climb, depth-assumption fragility (P1)
```bash
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../" && pwd)"
```
This climbs exactly four levels (`scripts/ -> multi-agent-mux-status/ -> skills/ -> .agents/ -> <root>`). Every other script climbs two levels to reach `skills/` and then appends a known subpath. The four-level climb hard-codes the tree depth.
**Diagnosis**: If `multi-agent-mux-status` is ever nested one level deeper (or the `.agents` dir is relocated/symlinked), this resolves to the wrong directory and `get_job_status` silently falls back to `('jid','unknown')` because none of the candidate job paths exist.
**Proposed fix**: Resolve `PROJECT_ROOT` the same way `WORKSPACE_ROOT` is derived in `lib.sh:18` (`SKILL_DIR/../..`), i.e. `PROJECT_ROOT="$WORKSPACE_ROOT"` (which `lib.sh` already computes). Then `status.sh` doesn't need its own climb at all - `lib.sh` is the single source of truth for root resolution. This also subsumes FUTURE_WORKS FW-P6 for this file.
### 3.3 Bash-only syntax (P2, protected by shebang)
| Construct | Locations | POSIX `sh` equivalent |
|---|---|---|
| `[[ ... ]]` double brackets | lib.sh:36,41,58,76; delegate-job:20,22,29,31,33,74,85,... | `[ ... ]` (with quoting) |
| `for i in {1..15}` brace expansion | lib.sh:1048 | `seq 1 15` or a `while` loop |
| `for ((i=0; i<tries; i++))` C-style for | lib.sh:1124 | `i=0; while [ $i -lt $tries ]; do ...; i=$((i+1)); done` |
| `local` keyword | everywhere | not in POSIX (but in dash; most shells support it) |
| `read -r -d '' RECON_SRC` | reconcile.sh:284 | `-d` is bash/ksh; POSIX has no NUL-delim read |
| `+=` array/string append | delegate-job (via `+=`) | `x="$x$y"` |
| `mapfile`/`readarray` | not used OK | - |
**Diagnosis**: All protected by `#!/usr/bin/env bash`. Not bugs today. They become bugs only if (a) a shebang is changed to `#!/bin/sh`, or (b) a script is `source`d into a POSIX shell. The brief asks for these to be found, not necessarily fixed - flagging for awareness.
**Proposed fix (if POSIX portability is ever a goal)**: The two genuinely portable-blocking items are `read -r -d ''` (reconcile.sh:284) and brace expansion (lib.sh:1048). Both have trivial POSIX equivalents shown above. `[[ ]]` and `local` are widely supported (dash, ash, zsh) even though non-POSIX, so they're low priority. No change recommended unless a non-bash target is committed.
### 3.4 `set -a; source .env; set +a` (delegate-job:20-24) - fine, but unguarded
```bash
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
```
**Diagnosis**: Sourcing an arbitrary `.env` with `set -a` (export-all) executes any shell syntax in the file. If `.env` ever contains shell injection (e.g. a value with `$(...)`), it runs with the script's privileges. Standard for `.env` loaders, but worth noting given the brief's robustness focus. The `[[ -f .env ]]` also prefers cwd over the project root, which can load the wrong `.env` if run from a subdir.
**Proposed fix (optional)**: Prefer the project-root `.env` first, and consider a guarded parse (`while IFS== read key val; do ...`) instead of `source` if untrusted `.env` files are a concern. Low priority.
---
## Focus Area 4 - Error Handling & Robustness
### 4.1 Top-level `except Exception: pass` swallows data corruption (highest impact)
Seven top-level `try/except Exception: pass` blocks turn corrupt state into silent empty data:
| File | Lines | Consequence of swallowing |
|---|---|---|
| `lib.sh` (resolve_tmux_server) | 148-149 | Corrupt YAML -> silently returns `default` server -> stop/resume hit wrong tmux server |
| `lib.sh` (find_workspace_uuid) | 610-611 | Corrupt DB -> empty session list -> UUID resolution falls through to global tiers (the P0-C bug the library exists to prevent) |
| `lib.sh` (agent_identities) | 760-761 | Corrupt DB -> empty identities -> cache tier skipped silently |
| `stop_session.sh` | 112-113 | Corrupt state -> `MAPPED_DATA` empty -> stop proceeds with no target, wrong behavior |
| `status.sh` | 61-62 | Corrupt state -> prints "(no sessions registered)" instead of an error |
| `update_yaml_resumed.sh` | 84-85 | Corrupt state -> `DELEGATE_JOB_ID` empty silently |
| `reconcile.sh` | 320-321 | Corrupt state -> empty `d` -> drift report shows no drift on a corrupted file |
**Diagnosis**: These are the same pattern the brief calls "silent failures ... stderr swallowed without proper diagnostics." A corrupt `agent-sessions.yaml` or `.db` is a serious operational condition, but the code treats it identically to "file doesn't exist yet" (the legitimate empty case). There's no way for an operator to distinguish "fresh install" from "broken state."
**Proposed fix**: Distinguish "absent" from "broken":
```python
try:
if os.path.exists(db_path):
conn = sqlite3.connect(db_path, timeout=60.0)
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
if row: d = json.loads(row[0])
conn.close()
elif os.path.exists(yaml_path):
with open(yaml_path) as f: d = yaml.safe_load(f) or {}
except (sqlite3.DatabaseError, yaml.YAMLError, json.JSONDecodeError) as e:
# State exists but is unreadable - this is an error, not "fresh."
print(f"ERROR: state at {yaml_path} is corrupt: {e}", file=sys.stderr, flush=True)
raise SystemExit(1)
```
The `elif` (file absent) path stays silent (legitimate first-run). Only the "exists but unreadable" path becomes loud. Apply uniformly via the `load_state_json` helper from 2.2 so the policy lives in one place.
### 4.2 `export TMUX_SERVER_NAME="$(resolve_tmux_server ...)"` masks failure (SC2155)
`stop_session.sh:71` and `update_yaml_resumed.sh:36`:
```bash
export TMUX_SERVER_NAME="$(resolve_tmux_server "$SESSION_NAME")"
```
ShellCheck SC2155: the command substitution's exit code is masked by `export`. If `resolve_tmux_server` ever exits non-zero (today it always exits 0 via the fallback at lib.sh:151, but a future change could break that), the error is lost.
**Proposed fix**:
```bash
TMUX_SERVER_NAME="$(resolve_tmux_server "$SESSION_NAME")" || exit $?
export TMUX_SERVER_NAME
```
### 4.3 `lib.sh:1030-1033` `start_watchdog` - `cd` without `|| return` (SC2164)
```bash
local orig_pwd="$PWD"
cd "$workdir"
nohup bash "$monitor_script" --subscribe --idle-timeout 0 >> "$log_file" 2>&1 &
pid=$!
cd "$orig_pwd"
```
If `cd "$workdir"` fails (dir removed between the `[ -f "$monitor_script" ]` check and here), `nohup` runs in `$PWD` with a relative `monitor_script`/`log_file`, and `cd "$orig_pwd"` also runs unconditionally.
**Proposed fix**:
```bash
cd "$workdir" || { echo "ERROR: workdir vanished: $workdir" >&2; return 1; }
nohup bash "$monitor_script" --subscribe --idle-timeout 0 >> "$log_file" 2>&1 &
pid=$!
cd "$orig_pwd" || true
```
### 4.4 `reconcile.sh:56 / 262` - broad `set +e` window
```bash
set +e
YAML_PATH=... "$PYBIN" - <<'PYEOF'
... ~180 lines of Python ...
PYEOF
sub_rc=$?
set -e
```
The `set +e` covers the entire Python heredoc plus the polling fallback (lines 263-277). This is necessary because the Python intentionally `sys.exit(3)` to signal "broker unavailable," but the window is large: any shell error in the surrounding scaffolding (env var expansion, the fallback `_self` loop) is non-fatal during that window.
**Diagnosis**: Today the only command between `set +e` and `set -e` that can fail non-Python is the fallback polling loop, which already has `|| true`. The risk is low but the pattern is fragile - a future edit that adds a shell command inside the `set +e` span will silently ignore failures.
**Proposed fix**: Narrow the `set +e` to just the Python invocation (it already is - line 262 restores before the fallback). Action item: add a comment making the invariant explicit, and don't let future edits extend the `set +e` span.
### 4.5 `delegate_publish_event` - fully silent on broker outage (by design, but unlogged)
```bash
delegate_publish_event() {
...
"$py_bin" "$pub" --job "$job_id" --event "$event" --detail "$detail" || true
}
```
The `|| true` makes every publish failure non-fatal (correct per the contract: a delegate event must never abort create/stop/resume). But there's no diagnostic path: a broker outage during a stop means `stopped` is never published, the job's lifecycle event is missing, and nothing logs this.
**Proposed fix**: Log to stderr without affecting exit code:
```bash
"$py_bin" "$pub" --job "$job_id" --event "$event" --detail "$detail" \\
|| echo "WARN: delegate event '$event' for job $job_id failed (broker down?)" >&2
```
Keeps the non-fatal contract, gives operators a signal.
### 4.6 `reconcile.sh` `handle_terminal` - unvalidated MQTT job_id passed via env
```python
jid = payload.get("job_id") # from untrusted MQTT payload
event = payload.get("event")
...
env['MQTT_JID'] = jid
env['MQTT_EVENT'] = event
cmd = ['bash', '-c', 'source "$LIB_SH"; atomic_dump_yaml "$YAML_PATH" MQTT_JID=...']
```
HMAC is verified (line 173-176, good - addresses FUTURE_WORKS FW-P7). The values reach `atomic_dump_yaml`'s Python via environment variables, not string interpolation (correct - lib.sh comment "P1-B" calls this out). But `jid`/`event` are not length- or charset-validated before being set as env vars. A malicious-but-HMAC-valid payload (requires the auth token) with a multi-MB `job_id` could exhaust environment space or cause odd `os.environ['MQTT_JID']` behavior.
**Diagnosis**: Low risk (requires the HMAC token to pass), but the brief's robustness focus applies. Defense-in-depth only.
**Proposed fix**: After HMAC verification, validate format:
```python
if not (isinstance(jid, str) and len(jid) <= 128 and jid.isascii()):
print(f"MQTT Monitor: drop event: invalid job_id format", flush=True); return
if event not in ("completed", "error"):
print(f"MQTT Monitor: drop event: invalid event {event!r}", flush=True); return
```
### 4.7 `except Exception` counts by file (for prioritization)
| File | `except Exception: pass` (fully silent) | `except ... as e: print(...)` (logged) |
|---|---|---|
| `reconcile.sh` | 5 (lines 257, 314, 320, 525, 550, 585, 613) | 3 (205, 229, 256) |
| `lib.sh` | 5 (128, 148, 332, 448, 463) | 0 |
| `stop_session.sh` | 2 (103, 112) | 1 (324) |
| `status.sh` | 3 (55, 61, 108) | 0 |
| `update_yaml_resumed.sh` | 1 (84) | 0 |
The `sqlite3.OperationalError: pass` cases (missing `sessions` table on older DBs) are legitimate - the table was added in a migration. Those should stay silent. The top-level `except Exception: pass` cases (4.1 above) are the ones to fix.
---
## Prioritized Action List
| ID | Finding | Area | Severity | Effort |
|---|---|---|---|---|
| **O-1** | `reconcile.sh:243-256` MQTT loop spins `time.sleep(0.5)` instead of `loop_forever`/Event | 1 | High | Medium |
| **O-2** | Top-level `except Exception: pass` swallows corrupt state (7 sites) | 4 | High | Medium (fix once in `load_state_json`) |
| **O-3** | `delegate-job:119,205` `sleep 1` subscriber-readiness race | 1 | Medium | Medium (needs subscriber readiness token) |
| **O-4** | Three `tmux -L` reimplementations; `stop_session.sh` uses bare `tmux` w/o shim | 2 | Medium | Low (collapse to `_tmux_with_server`) |
| **O-5** | DB+YAML load boilerplate duplicated 7x | 2 | Medium | Medium (extract `load_state_json`) |
| **O-6** | `status.sh:29` 4-level `../` climb -> use `WORKSPACE_ROOT` from lib.sh | 3 | Medium | Low |
| **O-7** | `stop_session.sh:193,200` fixed `sleep 3`/`sleep 5` -> bounded poll | 1 | Low | Low |
| **O-8** | `*_exists` probes trapped in heredoc -> shared `lib.py` | 2 | Low | Medium |
| **O-9** | `infer_agent_from_session` duplicated verbatim | 2 | Low | Trivial |
| **O-10** | `_delegate_py_bin` vs `pick_python` divergent venv walks | 2 | Low | Low |
| **O-11** | `export VAR="$(cmd)"` masks rc (SC2155) in stop/resume | 4 | Low | Trivial |
| **O-12** | `start_watchdog` `cd` without `|| return` (SC2164) | 4 | Low | Trivial |
| **O-13** | `delegate_publish_event` fully silent on broker outage | 4 | Low | Trivial |
| **O-14** | `lib.sh:1174` magic `sleep 0.5` -> `_pane_quiescent` | 1 | Low | Low |
| **O-15** | `BASH_SOURCE` unbound under zsh if sourced | 3 | Low (latent) | Low |
| **O-16** | `wait_for_tui_ready` coarse 1 s poll + brace expansion | 1 | Low | Trivial |
| **O-17** | `handle_terminal` unvalidated MQTT jid/event format | 4 | Low (defense-in-depth) | Trivial |
### Recommended sequencing
1. **O-2 + O-5 together** - extracting `load_state_json` is the vehicle for fixing the silent-corruption swallows. One helper, one error policy, seven call sites cleaned up. Highest ROI.
2. **O-1** - standalone, removes the busiest poller in the codebase.
3. **O-4 + O-9 + O-6** - small, mechanical de-duplication in `lib.sh`; fixes the latent `stop_session.sh` wrong-server bug as a side effect.
4. **O-3** - requires a small subscriber-side change (readiness token), then an orchestrator-side wait. Test with a flaky-broker harness.
5. The rest (O-7, O-8, O-10-O-17) are independent low-risk cleanups; bundle into one follow-up PR.
### Out of scope (noted, not actioned)
- FUTURE_WORKS FW-P6 (marker-file root lookup) would supersede O-6 and O-15; tracked separately.
- FUTURE_WORKS FW-P7 (HMAC on termination) is already implemented (reconcile.sh:173); O-17 is the remaining defense-in-depth on top.
- The `[[ ]]`, `local`, brace-expansion, and `read -d ''` bash-isms (3.3) are protected by the `#!/usr/bin/env bash` shebang and are not bugs under the current execution model. No change recommended unless a POSIX target is committed.
---
## Methodology / Verification
- All line numbers verified against `25de01e` via `grep -rn` and `sed -n` on the working tree.
- ShellCheck run at `-S warning` over all 8 scripts; reported warnings: SC2155 (x2), SC2164 (x2), SC2034 (x2, `ONCE`/`EMIT_DIFF` - these are used by the `--once`/`--emit-diff` flags via the arg parser, so ShellCheck's "unused" is a false positive for the documented CLI surface).
- Sleeps enumerated via `grep -rn '\bsleep\b'` across `--include='*.sh' --include='multi-agent-mux-delegate-job'` (18 shell sleeps + 3 Python `time.sleep`).
- `except` handlers enumerated via `grep -rn -A1 'except.*:'`.
- No code was modified; this is an analysis report only.
@@ -1,216 +0,0 @@
# Report: Skill Optimization Implementation Review (Final)
- **Author**: Reviewer Cline (`canary-projects-multi-agent-mux-reviewer-cline`)
- **Brief**: `.mam/reports/brief-rereview-skill-optimization.md`
- **Plan reviewed against**: `.agents/reports/canary-projects-multi-agent-mux-planner-claude/report-skill-optimization-plan.md`
- **Scope**: Phase 1 (OP-1, OP-2, OP-3) + Phase 2 (OP-4, OP-5, OP-6) working-tree changes
- **Output path**: `.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-skill-optimization-review-final.md`
## Verdict
# ✅ PASS
All six focus-area items named in the brief (OP-1, OP-2, OP-3, OP-4, OP-6, OP-7) are correctly implemented in the working tree. `bash -n` passes on all four changed shell scripts; `python3 -m py_compile` passes on `job_subscriber.py`; no new `shellcheck` warnings were introduced (all reported SC2155/SC2164/SC2034 are pre-existing). Two non-blocking observations are noted below; one out-of-scope item (OP-5) is flagged for awareness.
---
## Validation Commands (run inside this session)
| Check | Command | Result |
|---|---|---|
| Syntax (shell) | `bash -n lib.sh stop_session.sh multi-agent-mux-delegate-job reconcile.sh` | All 4 OK |
| Syntax (python) | `python3 -m py_compile job_subscriber.py` | OK |
| Lint | `shellcheck -S warning` on the 4 changed scripts | No new warnings (all pre-existing) |
Reported shellcheck warnings (all pre-existing, unchanged by this diff): `lib.sh:1043,1046` SC2164 (`cd` in `start_watchdog`), `stop_session.sh:71` SC2155 (`export TMUX_SERVER_NAME=$(...)`), `reconcile.sh:33,34` SC2034 (`ONCE`/`EMIT_DIFF` — false positives, used by the `--once`/`--emit-diff` arg parser).
---
## Per-Item Review
### OP-1 - Reactive Tmux Graceful Stopping (`stop_session.sh`) — PASS
**Plan**: Implement `_wait_session_gone` in `lib.sh` polling `tmux has-session` at ~250 ms up to a deadline; call it from `stop_session.sh` replacing the fixed `sleep 3` / `sleep 5`.
**Implementation** (`lib.sh:1128-1136`):
```bash
_wait_session_gone() {
local sess="$1" max="${2:-5}" i
for ((i = 0; i < max * 4; i++)); do
_sks_tmux has-session -t "$sess" 2>/dev/null || return 0
sleep 0.25
done
return 1
}
```
Call sites (`stop_session.sh:193,200`):
```bash
_wait_session_gone "$SESSION_NAME" 5 || true
...
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
_wait_session_gone "$SESSION_NAME" 8 || true
```
**Findings**:
- Poll interval 0.25 s × `max*4` iterations = exactly `max` seconds deadline. Math is correct.
- Uses `_sks_tmux` (server-aware), so it respects `TMUX_SERVER_NAME`. Correct.
- `|| true` appended at both call sites is **essential**: `stop_session.sh` runs under `set -euo pipefail` (line 30), and `_wait_session_gone` returns 1 on timeout. Without `|| true`, a timeout would abort the whole graceful chain. The guard is present. ✅
- Happy path returns early on first `has-session` failure (~0.25 s), matching the plan's "<0.3 s" outcome.
**Verdict**: Correctly implements the plan.
### OP-2 - MQTT Subscriber Event-Driven Handshake (`delegate-job`) — PASS (with minor observation)
**Plan**: `job_subscriber.py` writes `SUBSCRIBED <topic>` on SUBACK; the wrapper replaces `sleep 1` with a fast-poll loop matching the sentinel, with a liveness check.
**Implementation**`job_subscriber.py:188-194`:
```python
def on_subscribe(_c, _u, mid, granted_qos, _props=None):
for topic in subscribed_topics:
print(f"SUBSCRIBED {topic}", flush=True)
client.on_subscribe = on_subscribe
```
Wrapper (`multi-agent-mux-delegate-job:119-135`, mirrored at :218-234):
```bash
local sub_ready=0
for ((i = 0; i < 25; i++)); do
if kill -0 "$sub_pid" 2>/dev/null; then
if grep -q '^SUBSCRIBED ' "$logf" 2>/dev/null; then
sub_ready=1; break
fi
else
echo "ERROR: subscriber died early (pid=$sub_pid, check $logf)" >&2; exit 1
fi
sleep 0.2
done
if [ "$sub_ready" -ne 1 ]; then
echo "WARNING: subscriber subscribe handshake timed out — falling back to proceed" >&2
fi
```
**Findings**:
- Sentinel `^SUBSCRIBED ` is written with `flush=True` so it is visible to the wrapper's `grep` immediately. ✅
- `kill -0 "$sub_pid"` liveness check runs **before** the grep, so a dead subscriber is caught and exits 1 rather than waiting the full 5 s. ✅ Correct ordering.
- 25 × 0.2 s = 5 s deadline; falls back gracefully with a WARNING (non-fatal) on timeout. ✅ Matches plan.
- `on_subscribe` signature `(_c, _u, mid, granted_qos, _props=None)` covers both paho-mqtt v2 (MQTTv3, 5-arg) and v3 (MQTTv5, 6-arg with props) — the `_props=None` default absorbs the extra arg. ✅ Forward-compatible.
- **Minor observation (non-blocking)**: `on_subscribe` iterates `subscribed_topics` and prints all topics on **every** suback. In the multi-job case, the sentinel fires after the **first** suback, not after all topics are subscribed. For the common single-job delegation this is exactly correct; for multi-job the wrapper proceeds slightly early. Since the orchestrator's own subscriber is the one that must be ready to receive the agent's `started` event — and that event is published to one job's topic — proceeding after the first suback is safe as long as the first-subscribed topic is the active job's. The registration order (`for job in jobs`) makes this hold. No fix required; noted for completeness.
- Logic is correctly duplicated for both the `direct` path (:119) and the `loop/discuss` orchestrator path (:218). ✅
**Verdict**: Correctly implements the plan.
### OP-3 - Main Event Loop Pacing (`reconcile.sh`) — PASS (with minor observation)
**Plan**: Replace `while True: time.sleep(0.5)` with `threading.Event().wait(timeout=next_deadline - now)`.
**Implementation** (`reconcile.sh:242-266`):
```python
import threading
stop_event = threading.Event()
start = time.time()
try:
while True:
now = time.time()
wall_left = (timeout - (now - start)) if timeout else None
idle_left = (idle_timeout - (now - state['last_msg'])) if idle_timeout else None
next_timeout = 5.0
if wall_left is not None:
next_timeout = min(next_timeout, wall_left)
if idle_left is not None:
next_timeout = min(next_timeout, idle_left)
if next_timeout <= 0:
...print which deadline hit...
break
stop_event.wait(timeout=next_timeout)
finally:
client.loop_stop()
```
**Findings**:
- Computes `next_timeout = min(5.0, wall_left, idle_left)` so the main thread sleeps only as long as the nearest deadline, capped at 5 s — a strict improvement over the old fixed 0.5 s wake-up. ✅
- Deadline-expired branch (`next_timeout <= 0`) correctly distinguishes wall vs idle timeout in its log message. ✅
- **Minor observation (non-blocking)**: `stop_event` is never `set()` by any callback (e.g. `on_message`), so `stop_event.wait(timeout=...)` behaves identically to `time.sleep(timeout)` here. It is still an improvement because `Event.wait` is interruptible by signals (e.g. SIGINT) and is the idiomatic primitive for a condition-style sleep; but the full "event-driven wakeup on terminal event" benefit described in the plan would require `on_message` to call `stop_event.set()` when `event in ("completed","error")`. The current implementation is correct and an improvement; the optional enhancement (waking immediately on terminal event rather than on the next deadline tick) is left for a follow-up. Non-blocking.
**Verdict**: Correctly implements the plan (the reactive-sleep core).
### OP-4 - Unify Divergent Tmux Server Resolvers (`lib.sh`) — PASS
**Plan**: Extract a single canonical `mam_tmux()` dispatcher; have `_tmux`/`_sks_tmux` delegate to it.
**Implementation** (`lib.sh:97-113`):
```bash
mam_tmux() {
_resolve_real_tmux_path
if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then
"$_REAL_TMUX_PATH" -L "$TMUX_SERVER_NAME" "$@"
else
"$_REAL_TMUX_PATH" "$@"
fi
}
_tmux() { _init_tmux_isolation; mam_tmux "$@"; }
tmux() { _tmux "$@"; }
```
`_sks_tmux` (`lib.sh:1118`) now reads: `_sks_tmux() { mam_tmux "$@"; }`.
**Findings**:
- `mam_tmux` calls `"$_REAL_TMUX_PATH"` (the resolved real binary), **not** the shell function `tmux()` — so there is no recursion. ✅ (This was verified carefully; an earlier transient revision of the diff showed a literal `"tmux"` call which would have recursed, but the final working tree uses `$_REAL_TMUX_PATH`.)
- `mam_tmux` calls `_resolve_real_tmux_path` directly (not the full `_init_tmux_isolation` PATH-shim setup), so it is safe to use from hot paths that don't want the shim side effect. `_tmux` still runs the full `_init_tmux_isolation` for callers that rely on the PATH shim. Correct separation of concerns. ✅
- `_sks_tmux` previously had its own 4-line inline resolver with the SC2086-prone `tmux -L $TMUX_SERVER_NAME "$@"`; it now delegates to `mam_tmux`, removing the duplication. ✅
- All four duplication sites named in the plan are consolidated: `_tmux`, `_sks_tmux`, and the inline `local_tmux` in `wait_for_tui_ready` (which already used `_sks_tmux`/`_tmux`). The `create_session.sh:212` `local_tmux` string is for the human-readable `START_CMD` YAML field, not a live call — left as-is, correctly.
**Verdict**: Correctly implements the plan; no recursion hazard.
### OP-6 - Consolidate TUI Ready / Dialog Tokens (`lib.sh`) — PASS
**Plan**: Declare `_MAM_DIALOG_TOKENS` and `_MAM_READY_TOKENS_CLAUDE` at top of `lib.sh`; refer to them everywhere.
**Implementation** (`lib.sh:25-27`):
```bash
_MAM_DIALOG_TOKENS='Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel'
_MAM_READY_TOKENS_CLAUDE='Anthropic|Assistant|Chat|Welcome|projects'
```
References (`grep` confirms 3 use sites):
- `lib.sh:1071` `wait_for_tui_ready` claude branch → `grep -E -q "$_MAM_READY_TOKENS_CLAUDE"`
- `lib.sh:1158` `_pane_dialog_open``grep -Eq "$_MAM_DIALOG_TOKENS"`
- `lib.sh:1224` `handle_startup_dialogs``grep -Eq "$_MAM_READY_TOKENS_CLAUDE"`
**Findings**:
- Both constants are byte-identical to the previously-inlined literals. ✅ No behavioral drift.
- All three former inline sites now reference the constants; no stray literal copies remain (`grep` for the raw token strings outside the constant declarations returns nothing). ✅
- Quoting is consistent (`"$_MAM_DIALOG_TOKENS"` preserves the `|` alternation for `grep -E`). ✅
**Verdict**: Correctly implements the plan.
### OP-7 - Guard against Non-Bash Sourced Environments (`lib.sh`) — PASS
**Plan**: Add a zsh-aware fallback or an explicit exit message warning users not to source from a foreign shell.
**Implementation** (`lib.sh:17-20`):
```bash
if [ -z "${BASH_VERSION:-}" ]; then
echo "ERROR: lib.sh must be executed/sourced from bash (foreign shell detected)" >&2
return 1 2>/dev/null || exit 1
fi
```
**Findings**:
- The guard runs **before** any `BASH_SOURCE` use (line 21), so a zsh `source` exits cleanly with a diagnostic instead of silently resolving `BASH_SOURCE[0]` to empty and sourcing the wrong `lib.sh`. ✅
- `return 1 2>/dev/null || exit 1` covers both cases: `return` works when sourced (function context), `exit` works when executed directly. ✅
- This implements the "explicit exit message" option from the plan. The fuller zsh `${(%):-%x}` fallback was the alternative; the chosen approach is simpler and aligns with AGENTS.md "Simplicity First". ✅
**Verdict**: Correctly implements the plan.
---
## Out-of-Scope / Awareness
- **OP-5 (Single-Source YAML/SQLite Load Boilerplate)** — listed under Phase 2 in the plan, but **not in the brief's focus-area list** and **not implemented** in this working-tree diff. The 7 duplicated load blocks remain. This is consistent with the brief's scoped focus (OP-1,2,3,4,6,7) but is called out so Phase 2 completion is not mis-reported. Recommend a follow-up PR for OP-5.
- **OP-8 (Reconcile Observability)** — Phase 3 item, not in the brief's scope, not implemented. The fallback polling loop at `reconcile.sh:269` still uses `bash "$_self" --once --emit-diff >/dev/null 2>&1 || true`. Recommend a follow-up.
---
## Non-Blocking Observations (no action required for PASS)
1. **OP-2 multi-job sentinel**: `on_subscribe` prints all topics on each suback; the wrapper proceeds after the first suback. Safe for single-job (the common path) and for multi-job given registration order, but a future multi-job refactor should gate on "all topics subacked" (e.g. count subacks vs `len(subscribed_topics)`).
2. **OP-3 unused `stop_event.set()`**: `stop_event` is never set, so it functions as an interruptible `time.sleep`. Adding `stop_event.set()` in `on_message` on terminal events would let the loop wake immediately on job completion instead of on the next deadline tick — an optional latency improvement, not a correctness issue.
Both observations are enhancements, not defects; neither blocks the PASS verdict.
+47 -63
View File
@@ -58,6 +58,12 @@ _resolve_real_herdr_path() {
_init_herdr_isolation() {
local wrapper_dir="$WORKSPACE_ROOT/.mam/shim"
mkdir -p "$wrapper_dir"
if [ -x "$wrapper_dir/herdr" ]; then
if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then
export PATH="$wrapper_dir:$PATH"
fi
return 0
fi
local tmp_file
tmp_file=$(mktemp "$wrapper_dir/herdr.XXXXXX")
@@ -117,7 +123,7 @@ except Exception:
# Headless bootstrap avoids herdr's "nested herdr is disabled" guard that
# blocks a normal interactive `herdr --session <name>` launch from inside
# an existing herdr pane (which is how MAM's own agents usually run).
setsid "$REAL_HERDR" --session "$_MAM_SESSION" server </dev/null >/dev/null 2>&1 &
nohup "$REAL_HERDR" --session "$_MAM_SESSION" server >/dev/null 2>&1 &
disown 2>/dev/null || true
for _mam_wait_i in $(seq 1 40); do
[ -S "${HOME:-$HOME_DIR}/.config/herdr/sessions/$_MAM_SESSION/herdr.sock" ] && break
@@ -127,8 +133,12 @@ except Exception:
fi
_real_herdr() {
if [ -n "$_MAM_SESSION" ]; then
"$REAL_HERDR" --session "$_MAM_SESSION" "$@"
local session="${HERDR_SERVER_NAME:-}"
if [ "$session" = "default" ]; then
session=""
fi
if [ -n "$session" ]; then
"$REAL_HERDR" --session "$session" "$@"
else
"$REAL_HERDR" "$@"
fi
@@ -210,52 +220,35 @@ print('\t'.join(env_flags) + '\n' + ' '.join(binary_tokens))
final_cmd=$(echo "$parsed" | tail -n +2)
fi
# Isolation now comes from `_MAM_SESSION` (a real, separate herdr
# session/server) rather than a workspace label match — just create a
# fresh workspace inside whatever session is active (default or
# isolated) and place the agent there. No cross-session label lookup
# needed since `--session` already scopes everything.
# Resolve ws_id and split direction:
# - If we have up to 3 agents, split 'right' (columns)
# - If we have 4 or more agents, split 'down' (rows)
res=$(_real_herdr workspace list 2>/dev/null | WS_CWD="${ws:-.}" python3 -c "
import sys, json, os
# Check if there is an existing workspace inside this session (to reuse and split view)
existing_ws=$(_real_herdr workspace list 2>/dev/null | python3 -c "
import sys, json
try:
target_label = os.path.basename(os.path.abspath(os.environ.get('WS_CWD', '.')))
wlist = json.loads(sys.stdin.read()).get('result', {}).get('workspaces', [])
match = None
for w in wlist:
if w.get('label') == target_label:
match = w
break
if not match and wlist:
match = wlist[0]
if match:
ws_id = match.get('workspace_id', 'w1')
pane_count = match.get('pane_count', 0)
split_dir = 'right' if pane_count < 3 else 'down'
print(f'{ws_id}\t{split_dir}')
d = json.loads(sys.stdin.read())
wss = d.get('result', {}).get('workspaces', [])
if wss:
print(wss[0].get('workspace_id', ''))
except Exception:
pass
")
ws_id=$(echo "$res" | cut -f1)
split_dir=$(echo "$res" | cut -f2)
if [ -z "$ws_id" ]; then
split_flag=""
if [ -n "$existing_ws" ]; then
ws_id="$existing_ws"
split_flag="--split right"
else
ws_id=$(_real_herdr workspace create --cwd "${ws:-.}" --no-focus 2>/dev/null | python3 -c "
import sys, json
try:
d = json.loads(sys.stdin.read()).get('result', {}).get('workspace', {}).get('workspace_id', '')
print(d)
d = json.loads(sys.stdin.read())
print(d.get('result', {}).get('workspace', {}).get('workspace_id', ''))
except Exception:
pass
")
split_dir="right"
ws_id="${ws_id:-w1}"
fi
ws_id="${ws_id:-w1}"
split_dir="${split_dir:-right}"
eval "_real_herdr agent start \"$name\" --workspace \"$ws_id\" --split \"$split_dir\" --cwd \"${ws:-.}\" $env_flags -- $final_cmd"
eval "_real_herdr agent start \"$name\" --workspace \"$ws_id\" --cwd \"${ws:-.}\" $split_flag $env_flags -- $final_cmd"
;;
kill-session)
sess=""
@@ -363,7 +356,7 @@ except Exception:
esac
;;
capture-pane)
sess="" src="visible" lines="100"
sess=""
while [ $# -gt 0 ]; do
case "$1" in
-t)
@@ -374,22 +367,10 @@ except Exception:
sess="$2"
shift 2
;;
-S)
# tmux `-S -N` (start N lines above the visible viewport) maps to
# herdr `--source recent --lines N`. Non-negative or non-numeric
# values keep the default visible viewport read.
if [ $# -ge 2 ] && [[ "$2" =~ ^-[0-9]+$ ]]; then
src="recent"
lines="${2#-}"
shift 2
else
shift
fi
;;
*) shift ;;
esac
done
_real_herdr agent read "$sess" --source "$src" --lines "$lines" 2>/dev/null || true
_real_herdr agent read "$sess" --source visible --lines 100 2>/dev/null || true
;;
send-keys)
sess="" key=""
@@ -483,11 +464,7 @@ except Exception:
esac
EOF
chmod +x "$tmp_file"
if [ -f "$wrapper_dir/herdr" ] && cmp -s "$tmp_file" "$wrapper_dir/herdr"; then
rm -f "$tmp_file"
else
mv -f "$tmp_file" "$wrapper_dir/herdr"
fi
mv -f "$tmp_file" "$wrapper_dir/herdr"
if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then
export PATH="$wrapper_dir:$PATH"
fi
@@ -507,9 +484,9 @@ herdr() {
}
# ---------------------------------------------------------------------------
# resolve_herdr_session <session_name>
# resolve_herdr_server <session_name>
#
# Query agent-sessions.yaml to find the herdr_session associated with a session.
# Query agent-sessions.yaml to find the herdr_server associated with a session.
# Fallback to HERDR_SERVER_NAME or 'default' if not registered or field is missing.
# Prints the resolved server name on stdout.
# ---------------------------------------------------------------------------
@@ -553,7 +530,13 @@ print(json.dumps(d, ensure_ascii=False))
PYEOF
}
resolve_herdr_session() {
# Despite the name (kept for caller compatibility — resume/stop/update_yaml_resumed
# all do `HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"`), this
# returns the isolated herdr *session* name to use for this MAM session row, not
# a workspace id. Real isolation is `--session <name>` (see `_MAM_SESSION` in the
# generated wrapper) — a workspace label match provides no actual isolation
# since agent/pane commands are server-global regardless of workspace.
resolve_herdr_workspace() {
local session_name="$1"
MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$session_name" python3 -c "
import sys, os, json
@@ -561,7 +544,7 @@ name = os.environ['SESSION_NAME']
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
for s in d.get('herdr_sessions', []):
if s.get('name') == name:
print(s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default')
print(s.get('herdr_workspace') or s.get('herdr_server') or 'default')
sys.exit(0)
print(os.environ.get('HERDR_SERVER_NAME', 'default'))
"
@@ -1200,7 +1183,8 @@ provision_isolation() {
mkdir -p "$root"
case "$agent" in
claude)
ln -sfn "$HOME/.claude/.credentials.json" "$root/.credentials.json"; seeded=".credentials.json"
if [ -e "$HOME/.claude.json" ]; then ln -sfn "$HOME/.claude.json" "$root/.claude.json"; seeded=".claude.json"; fi
ln -sfn "$HOME/.claude/.credentials.json" "$root/.credentials.json"; seeded="${seeded:+$seeded,}.credentials.json"
if [ -e "$HOME/.claude/settings.json" ]; then ln -sfn "$HOME/.claude/settings.json" "$root/settings.json"; seeded="$seeded,settings.json"; fi
if [ -d "$HOME/.claude/plugins" ]; then ln -sfn "$HOME/.claude/plugins" "$root/plugins"; seeded="$seeded,plugins"; fi
;;
@@ -1588,7 +1572,7 @@ send_keys_safe() {
_sks_herdr send-keys -t "$sess" C-m
return 0
fi
sleep "${SKS_PASTE_SLEEP:-1.5}"
sleep 0.5
local pane_content was_popup=0
pane_content=$(_pane_capture "$sess")
if printf '%s\n' "$pane_content" | grep -Eq "Pasted text|paste again to expand"; then
@@ -1597,7 +1581,7 @@ send_keys_safe() {
# lines at the terminal width (and some TUIs add indentation on the
# continuation line too), which can split/reformat the marker across two
# visual lines and make a literal grep miss it even though the paste landed.
elif [ -n "$marker_norm" ] && ! printf '%s' "$pane_content" | tr -d '[:space:]' | grep -Fq "$marker_norm"; then
elif ! printf '%s' "$pane_content" | tr -d '[:space:]' | grep -Fq "$marker_norm"; then
echo "send_keys_safe: paste not visible ($sess)" >&2
return 3
fi
@@ -1612,7 +1596,7 @@ send_keys_safe() {
if printf '%s\n' "$cur_content" | grep -Eq "● |✽ |[A-Za-z]+ing…|[A-Za-z]+ing\.\.\.|esc to interrupt"; then
return 0
fi
if [ "$was_popup" = "0" ] && { [ -z "$marker_norm" ] || ! _pane_tail "$sess" 3 | tr -d '[:space:]' | grep -Fq "$marker_norm"; } && [ "$cur_content" != "$pre_submit" ]; then
if [ "$was_popup" = "0" ] && ! _pane_tail "$sess" 3 | tr -d '[:space:]' | grep -Fq "$marker_norm" && [ "$cur_content" != "$pre_submit" ]; then
return 0
elif [ "$was_popup" = "1" ] && \
! printf '%s\n' "$cur_content" | grep -Eq "Pasted text|paste again to expand" && \
@@ -310,7 +310,7 @@ entry = {
'role': role,
'herdr_session_created_at': os.environ['NOW_ISO'],
'herdr_session_epoch': int(epoch) if epoch.isdigit() else 0,
'herdr_session': server_name,
'herdr_server': server_name,
'delegate_job_id': os.environ.get('DELEGATE_JOB_ID', '') or None,
'pane': {
'index': 0,
@@ -138,19 +138,6 @@ cmd_submit() {
- **Timeout**: $TIMEOUT s (Idle: $IDLE_TIMEOUT s)
- **Output Report Path**: .mam/jobs/$JOB_ID/$AGENT-reports/report-final.md
## 🔔 Execution Commands
On start run:
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event started
On progress (optional):
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event progress --detail '<short status>'
On success run:
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event completed --detail '<one-line summary>'
On failure run:
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event error --detail '<one-line reason>'
## 🔎 Task Description
$PROMPT
EOF
@@ -197,8 +184,7 @@ EOF
# an id from an earlier session is the #1 reason a delegated job sits idle and
# times out (see SKILL.md "Wrong job_id propagated to the agent"). We make the
# freshness explicit in the instruction header.
# Keep this short and ASCII-only to prevent paste/wrap rendering issues in CLI REPLs.
local instructions="Job $JOB_ID: Read .mam/jobs/$JOB_ID/brief.md and complete the task."
local instructions="Your job_id is \"$JOB_ID\". Detailed task requirements, instructions, and target output paths are documented in the task brief file at: .mam/jobs/$JOB_ID/brief.md. Please READ and follow .mam/jobs/$JOB_ID/brief.md to complete your work. Commands: start='$pub --event started', success='$pub --event completed --detail <summary>', error='$pub --event error --detail <reason>'."
run_agent "$JOB_ID" "$instructions"
@@ -454,7 +440,7 @@ run_agent() {
# the caller having exported HERDR_SERVER_NAME by hand. This is what lets
# delegation reach an agent living in an isolated herdr session (e.g. one
# created with --herdr-server) instead of silently looking in "default".
export HERDR_SERVER_NAME="$(resolve_herdr_session "$sess")"
export HERDR_SERVER_NAME="$(resolve_herdr_workspace "$sess")"
if ! herdr has-session -t "$sess" 2>/dev/null; then
echo "ERROR: 에이전트 세션 '$sess'이 존재하지 않습니다. 작업을 위임하기 전에 먼저 에이전트 세션을 기동해 주세요." >&2
@@ -1,5 +1,21 @@
---
name: multi-agent-mux-loop
description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached."
version: 1.0.0
author: godopu
license: MIT
platforms: [linux, macos]
environments: [terminal, herdr]
metadata:
hermes:
tags: [agent, herdr, multi-agent, loop, planning, review, orchestrator]
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-delegate-job]
prereq_skills: [multi-agent-mux-create]
---
# Multi-Agent Mux Loop — Autonomous Orchestration Loop
> **Companion skills**: `multi-agent-mux-create` (start), `multi-agent-mux-resume` (re-attach), `multi-agent-mux-delegate-job` (delegate).
> **Safety Guard**: `--max-loop` and `--plan-talk` restrict API cost runaways.
> **Single source of truth**: `./.mam/agent-sessions.yaml`.
@@ -102,6 +102,10 @@ if [ "$ALL_REVIEWERS" = true ] && [ -n "$REVIEWER_LIST" ]; then
log_warn "--all-reviewer takes precedence; ignoring --reviewer list ('$REVIEWER_LIST')."
fi
if [ "$PLAN_TALK_TURNS" -gt 0 ] && [ "$PLAN_MODE" = false ]; then
log_warn "--plan-talk was specified but --plan mode is not enabled. Discussion turns will be ignored."
fi
# Verdict must occupy the report's last non-blank line — a standalone token
# quoted mid-report (e.g. as a formatting example) never matches (P0-2).
has_verdict() {
@@ -159,7 +163,7 @@ import os, json
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
target_agent = os.environ.get('TARGET_AGENT')
reviewers = [s.get('name') for s in d.get('herdr_sessions', [])
if 'reviewer' in s.get('role', '').lower() and s.get('name') != target_agent]
if 'reviewer' in (s.get('role') or '').lower() and s.get('status') == 'running' and s.get('name') != target_agent]
print(','.join(reviewers))
"
}
@@ -199,7 +203,7 @@ import os, json
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
planner = ''
for s in d.get('herdr_sessions', []):
if 'planner' in s.get('role', '').lower():
if 'planner' in (s.get('role') or '').lower() and s.get('status') == 'running':
planner = s.get('name')
break
print(planner)
@@ -220,8 +224,37 @@ log_info "Initializing multi-agent-mux-loop controller..."
log_info "Target Agent: $TARGET_AGENT"
log_info "Task Goal: $TASK"
# Validate that Target Agent is registered and running
TARGET_STATUS=$(MAM_STATE_JSON="$(load_state_json)" TARGET="$TARGET_AGENT" python3 -c "
import os, json
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
target = os.environ.get('TARGET')
status = ''
for s in d.get('herdr_sessions', []):
if s.get('name') == target:
status = s.get('status')
break
print(status)
")
if [ -z "$TARGET_STATUS" ]; then
log_error "Target agent session '$TARGET_AGENT' is not registered in the session registry."
exit 1
elif [ "$TARGET_STATUS" != "running" ]; then
log_error "Target agent session '$TARGET_AGENT' is not running (current status: '$TARGET_STATUS'). Please start it first."
exit 1
fi
PLANNER_SESSION=$(resolve_planner_session)
log_info "Resolved Planner session: $PLANNER_SESSION"
if [ "$PLAN_MODE" = true ]; then
if [ -z "$PLANNER_SESSION" ]; then
log_error "Planner mode enabled (--plan) but no running session with a 'planner' role was found."
exit 1
fi
fi
CURRENT_PLAN=""
CREATED_JOBS=()
@@ -329,7 +362,9 @@ else
log_info "=== Phase 1: Self-Planning Mode (Direct Execution) ==="
EXISTING_PLAN_FILE=""
if [ -n "$PLANNER_SESSION" ]; then
EXISTING_PLAN_FILE=".agents/reports/$PLANNER_SESSION/report-final.md"
# Promoted plans are saved as plan-<job-id>.md by the Phase 3 promotion
# step below, not report-final.md; pick the most recently promoted one.
EXISTING_PLAN_FILE=$(ls -t ".agents/reports/$PLANNER_SESSION"/plan-*.md 2>/dev/null | head -n 1 || true)
fi
if [ -n "$EXISTING_PLAN_FILE" ] && [ -f "$EXISTING_PLAN_FILE" ]; then
log_info "Found existing promoted plan at '$EXISTING_PLAN_FILE'. Loading plan..."
@@ -385,8 +420,14 @@ if [ "$ALL_REVIEWERS" = true ]; then
if [ -n "$RESOLVED_REVS" ]; then
IFS=' ,' read -r -a REVIEWERS <<< "$RESOLVED_REVS"
fi
if [ "${#REVIEWERS[@]}" -eq 0 ]; then
log_error "--all-reviewer was specified, but no running reviewer sessions were found."
exit 1
fi
elif [ -n "$REVIEWER_LIST" ]; then
IFS=' ,' read -r -a REVIEWERS <<< "$REVIEWER_LIST"
# Strip whitespaces first to prevent empty array elements from trailing spaces
CLEAN_REVS=$(echo "$REVIEWER_LIST" | tr -d '[:space:]')
IFS=',' read -r -a REVIEWERS <<< "$CLEAN_REVS"
fi
loop_count=1
@@ -121,7 +121,7 @@ _changed = False
for s in d.get('herdr_sessions', []):
if s.get('delegate_job_id') == _jid and s.get('status') == 'running':
_name = s.get('name')
_srv = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
_srv = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
if _event == 'completed':
s['delegate_job_id'] = None
print('MQTT Monitor: job completed on ' + str(_name) + ' — session kept alive', flush=True)
@@ -131,8 +131,7 @@ for s in d.get('herdr_sessions', []):
s['terminated_at'] = _now.strftime('%Y-%m-%dT%H:%M:%SZ')
s['terminated_at_epoch'] = int(_now.timestamp())
s['termination_mode'] = 'auto-detected (MQTT ' + _event + ')'
_shim = os.path.join(os.environ.get('WORKSPACE_ROOT', os.getcwd()), '.mam/shim/herdr')
_cmd = [_shim] + (['-L', _srv] if _srv != 'default' else []) + ['kill-session', '-t', _name]
_cmd = ['herdr'] + (['-L', _srv] if _srv != 'default' else []) + ['kill-session', '-t', _name]
subprocess.run(_cmd, capture_output=True)
print('MQTT Monitor: terminated + killed ' + str(_name) + ' on ' + str(_srv) + ' due to MQTT ' + _event, flush=True)
_changed = True
@@ -310,8 +309,6 @@ import yaml
yaml_path = os.environ['YAML_PATH']
home = os.environ['HOME_DIR']
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
workspace_root = os.environ.get('WORKSPACE_ROOT', os.getcwd())
shim_herdr = os.path.join(workspace_root, '.mam/shim/herdr')
now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
@@ -345,12 +342,12 @@ unique_servers = {'default'}
if 'HERDR_SERVER_NAME' in os.environ:
unique_servers.add(os.environ['HERDR_SERVER_NAME'])
for s in d.get('herdr_sessions', []):
srv = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
srv = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
unique_servers.add(srv)
try:
for srv in sorted(unique_servers):
cmd = [shim_herdr]
cmd = ['herdr']
if srv != 'default':
cmd += ['-L', srv]
cmd += ['ls', '-F', '#{session_name}|#{session_created}']
@@ -372,7 +369,7 @@ except Exception:
def pane_meta(session, srv):
try:
cmd = [shim_herdr]
cmd = ['herdr']
if srv != 'default':
cmd += ['-L', srv]
cmd += ['list-panes', '-t', session, '-F',
@@ -399,7 +396,7 @@ if herdr_confirmed:
# (없으면 herdr-dead stopped 세션을 'terminated' 로 덮어써 resumable 플래그가 소실됨)
if s.get('status') in ('terminated', 'archived', 'stopped'):
continue
srv = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
srv = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
if (name, srv) not in alive_set:
s['status'] = 'terminated'
s['terminated_at'] = now_iso
@@ -37,7 +37,7 @@ if [ -z "$UUID" ]; then
exit 1
fi
HERDR_SERVER_NAME="$(resolve_herdr_session "$SESSION_NAME")"
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
export HERDR_SERVER_NAME
# 2. If herdr is alive, print warning or attach.
@@ -33,7 +33,7 @@ done
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
HERDR_SERVER_NAME="$(resolve_herdr_session "$SESSION_NAME")"
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
export HERDR_SERVER_NAME
# --agent 미지정 시 이름 suffix 로 fallback (P1-F: 가능하면 --agent 명시)
@@ -93,7 +93,7 @@ def get_job_status(s):
sessions_detail = []
for s in d.get('herdr_sessions', []):
name = s.get('name', '?')
server = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
server = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
jid, jstatus = get_job_status(s)
pane = s.get('pane') or {}
sessions_detail.append({
@@ -197,7 +197,7 @@ if not sessions:
print("(no sessions registered)")
for s in sessions:
name = s.get('name', '?')
server = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
server = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
status = s.get('status', '?')
herdr = 'alive' if f"{name}|{server}" in alive else 'dead'
cmd = (s.get('pane') or {}).get('cmd', '?')
@@ -82,7 +82,7 @@ if [ "$PURGE" = "1" ]; then
trap 'rm -f "$WORKSPACE_ROOT/.mam/purging-$SESSION_NAME"' EXIT
fi
HERDR_SERVER_NAME="$(resolve_herdr_session "$SESSION_NAME")"
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
export HERDR_SERVER_NAME
# --agent 미지정 시 이름 suffix 로 fallback (P1-F)