11 Commits
Author SHA1 Message Date
Godopu 49176b43b6 refactor(lib): hash entire session dictionary to widen YAML write-gate
- Prevent YAML<->SQLite sync drift on completed jobs (R-1 Option A)
- Hash session dict to trigger YAML rewrite on any field updates
2026-07-12 14:37:42 +09:00
Godopu 60f3af4af9 refactor(monitor): keep sessions alive on job completion, kill only on errors
- Modify reconcile.sh mutation logic to clear delegate_job_id instead of calling tmux kill-session on 'completed' events
- Retain process termination behavior on 'error' events for safety
2026-07-12 14:24:02 +09:00
Godopu 288132c7ba refactor(install): port venv bootstrap, config tools and gitignore exclusions to installer
- Port .venv creation and dependency pip install bootstrap sequence from deploy/install.sh into scripts/install_mam.sh
- Include .env.example and scripts/generate-env.sh copies under rsync target deployment
- Add .venv/ to target gitignore list to prevent virtualenv bloating
- Add empty-UUID resume safety guard in INSTALL.md
2026-07-12 14:15:04 +09:00
Godopu 382c314d5e refactor(rules): resolve loop report paths colon-safety and terminology gaps
- Sanitize clean_session in loop path report-final.md Output Report Path to prevent colon characters in folders
- Reconcile automated reports path token in MULTI_AGENT_RULES.md and .ko.md from <agent-session> to <agent_name> or <clean_session_name>
2026-07-12 13:57:04 +09:00
Godopu 65843e0557 docs(rules): update multi-agent rules with job-centric structure pointers
- Document automated job brief path under .mam/jobs/<job_id>/brief.md
- Document automated report redirection under .mam/jobs/<job_id>/<agent-session>-reports/report-final.md
- Correct stop_session.sh report cleanup claim to reflect manual cleanup
- Clarify onboarding brief mechanism under onboarding handshake protocol
2026-07-12 13:50:30 +09:00
Godopu 6186673fb2 chore(cleanup): remove obsolete brief markdown files from reports 2026-07-12 13:30:11 +09:00
Godopu d776273ae9 Revert "chore(cleanup): remove obsolete DONE.md task tracker file"
This reverts commit e305bcb083.
2026-07-12 13:29:37 +09:00
Godopu e305bcb083 chore(cleanup): remove obsolete DONE.md task tracker file 2026-07-12 13:23:28 +09:00
Godopu e44b587c2e chore(cleanup): remove handoff.md and clean temporary test job artifacts
- Remove handoff.md as the optimization is fully merged and verified
- Clean up temporary test jobs and logs from .mam/jobs/
2026-07-12 13:19:09 +09:00
Godopu 35af8e33a2 feat(skills): implement state loader DRY (OP-5) and Job-centric directory structure
- Extract load_state_json centralized helper inside lib.sh to unify state querying
- Refactor status, resume, stop, and monitor scripts to fetch state via MAM_STATE_JSON env var to avoid stdin pipeline collisions
- Restructure delegate-job to provision .mam/jobs/<job_id>/brief.md and direct agents to it, minimizing token size and preventing TUI paste freezes
- Harden send_keys_safe submission loop with was_popup state capture and edge case guards, preventing timing spin false-positives
- Passed cross-verification approved PASS from Planner Claude session
2026-07-12 11:50:38 +09:00
Godopu 76ec0dd929 fix(skills): support automatic bypass of large-session resume warning dialogs
- Add 'Resuming the full session' and 'Resume from summary' patterns to TUI validation constants
- Update handle_startup_dialogs to automatically submit the choice on large-session warnings, preventing start/resume lockups
2026-07-12 10:27:59 +09:00
15 changed files with 229 additions and 420 deletions
+3
View File
@@ -73,6 +73,9 @@ $ SESSION_NAME="my-project-dev-claude"
$ UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh \ $ UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh \
--workspace "$WORKSPACE" --agent "$AGENT" --session "$SESSION_NAME") --workspace "$WORKSPACE" --agent "$AGENT" --session "$SESSION_NAME")
# 복원 대상 세션의 유효성 검사 (M-1)
$ [ -n "$UUID" ] || { echo "[ERROR] 매칭되는 활성 세션 이력이 없습니다. create_session.sh를 통해 먼저 세션을 생성해 주세요."; exit 1; }
# 2단계: 동적 격리 인자 주입 스폰 수행 후 레지스트리 상태 running 복구 # 2단계: 동적 격리 인자 주입 스폰 수행 후 레지스트리 상태 running 복구
# (상세 쉘 명령어는 .agents/skills/multi-agent-mux-resume/SKILL.md 참조) # (상세 쉘 명령어는 .agents/skills/multi-agent-mux-resume/SKILL.md 참조)
``` ```
+11 -8
View File
@@ -121,13 +121,15 @@ TMUX 환경에서 실행되는 에이전트가 화면 스크롤 한계로 인해
### 📄 마크다운 기반 협업 및 결과 전달 (Markdown-Based Workflow & Communication) ### 📄 마크다운 기반 협업 및 결과 전달 (Markdown-Based Workflow & Communication)
- **핵심 원칙**: TMUX `send-keys`나 입력 버퍼를 통해 긴 지시사항을 직렬로 입력하는 과정에서 문자 누락이나 레이아웃 유실이 발생하는 것을 방지하기 위해, 에이전트 간의 모든 주요 협업 소통은 파일 기반 마크다운 문서 생성을 원칙으로 합니다. - **핵심 원칙**: TMUX `send-keys`나 입력 버퍼를 통해 긴 지시사항을 직렬로 입력하는 과정에서 문자 누락이나 레이아웃 유실이 발생하는 것을 방지하기 위해, 에이전트 간의 모든 주요 협업 소통은 파일 기반 마크다운 문서 생성을 원칙으로 합니다.
- **세부 규칙 및 규약**: - **세부 규칙 및 규약**:
- **예외 사항**: 1~2줄 내외의 매우 단순한 재평가 요청, 상태 확인, 수락 진행 등의 단발성 프롬프트는 기존처럼 tmux 입력을 통해 직접 보낼 수 있습니다. - **예외 사항**: 1~2줄 내외의 매우 단순한 요청, 상태 확인, 수락 진행 등의 단발성 프롬프트는 tmux 입력을 통해 직접 보낼 수 있습니다.
- **작업 위임**: 상세 사양과 계획 수립 등의 복잡한 작업 지시는 먼저 로컬 마크다운 파일(예: `.mam/reports/brief-<job_id>.md` 또는 지정된 워크스페이스 경로)로 작성한 후, 에이전트에게 `"Read <파일경로> and execute."` 라는 단순화된 실행 명령만 전달하십시오. - **작업 위임**:
- **결과 안내 및 피드백**: 상세 리뷰 결과, 설계 제안서, 구현 완료 리포트 및 감사 주석 등은 반드시 마크다운 파일로 영속화하여 제공해야 합니다. 기계 전용인 잡 레지스트리 평면(`.mam/jobs/`)과의 물리적 분리를 위해, 모든 에이전트는 `.mam/reports/<tmux_session_name>/` 아래에 전용 디렉터리를 형성하고 결과 보고 마크다운 파일을 저장해야 합니다 (여기서 `<tmux_session_name>``.mam/agent-sessions.yaml``name` 필드와 완전히 일치해야 합니다. 예: `.mam/reports/<workspace_slug>-creator-<agent>/report-<job_id>.md`). 수신 에이전트는 잘린 터미널 화면 캡처에 의존하는 대신 디스크에서 해당 파일을 직접 로드하여 확인합니다. - *수동 경로*: 상세 사양과 계획 수립 등의 복잡한 작업 지시는 먼저 로컬 마크다운 파일(예: `.mam/reports/brief-<job_id>.md` 또는 지정된 워크스페이스 경로)로 작성한 후, 에이전트에게 `"Read <파일경로> and execute."` 라는 실행 명령만 전달하십시오.
- **디스크 정리 및 보존 정책 계약 (Cleanup & Retention)**: - *자동 경로*: 자동화 잡 런너(`multi-agent-mux-delegate-job submit`)는 잡 등록 시 `.mam/jobs/<job_id>/brief.md` 디렉터리에 지시서를 자동 집필하고 단일 라인 포인터 프롬프트만 에이전트 세션에 인가합니다.
- `.mam/reports/` 폴더 아래의 파일들은 감사 이력(audit-trail) 산출물로 보존됩니다. - **결과 안내 및 피드백**:
- 해당 격리 디렉터리들은 `stop_session.sh` 등을 통해 세션이 정상적으로 종료되거나 파기(`--purge-conversation`)될 때 자동으로 함께 정리되어야 합니다. - *수동/영구 리뷰*: 상세 리뷰 결과, 설계 제안서 등은 `.mam/reports/<tmux_session_name>/report-<job_id>.md` 경로에 저장합니다.
- 버전 관리가 필요한 영구 보존용 주요 산출물(최종 설계 계획, 최종 리뷰 보고서, 보안 감사 리포트 등)은 gitignore 대상인 `.mam/` 하위가 아닌, 버전 관리 대상 경로(구체적으로 `.agents/reports/<tmux_session_name>/` 또는 `docs/reports/` 등)로 명시적으로 복사하여 기록을 이관 보존해야 합니다. - *자동화 잡 보고서*: 자동 위임된 비동기 작업의 완료 결과는 잡 디렉터리 하위인 `.mam/jobs/<job_id>/<agent_name>-reports/report-final.md` (루프/Discuss 위임 시에는 `<clean_session_name>-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` 세션 종료 스크립트는 이들 보고서 디렉터리를 자동으로 삭제하지 않으므로 수동 또는 주기적 클린업이 권장됩니다.
### ⏱️ 타임아웃 구성 및 정렬 규칙 ### ⏱️ 타임아웃 구성 및 정렬 규칙
- **잡 실행 제한 (`timeout_sec` & `idle_timeout_sec`)**: 각 잡은 전체 실행 만료 시간(`timeout_sec`, 기본 3600s)과 메세지 미수신 유휴 시간(`idle_timeout_sec`, 기본 120s)을 독립적으로 가집니다. - **잡 실행 제한 (`timeout_sec` & `idle_timeout_sec`)**: 각 잡은 전체 실행 만료 시간(`timeout_sec`, 기본 3600s)과 메세지 미수신 유휴 시간(`idle_timeout_sec`, 기본 120s)을 독립적으로 가집니다.
@@ -157,7 +159,8 @@ TMUX 환경에서 실행되는 에이전트가 화면 스크롤 한계로 인해
```bash ```bash
bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace "$(pwd)" --agent <agent> --role <role> --onboard bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace "$(pwd)" --agent <agent> --role <role> --onboard
``` ```
2. **에이전트 맥락 동기화**: 에이전트가 시작되면 아래 지시사항이 담긴 잡이 자동으로 인가됩니다: 이 자동화된 온보딩 워크플로우는 백그라운드 잡을 등록하여 `.mam/jobs/<job_id>/brief.md` 에 온보딩 지시서를 작성하고 에이전트 세션에 지시서 포인터만 입력합니다.
2. **에이전트 맥락 동기화**: 에이전트가 시작되면 아래 지시사항이 담긴 온보딩 brief를 자동으로 수임하여 확인합니다:
- `README.md` 및 `.agents/MULTI_AGENT_RULES.md`를 필독하여 설계 규약과 제약사항을 인지한다. - `README.md` 및 `.agents/MULTI_AGENT_RULES.md`를 필독하여 설계 규약과 제약사항을 인지한다.
- `git status` 및 `git diff`를 실행하여 레포지토리의 활성 수정 내역을 분석한다. - `git status` 및 `git diff`를 실행하여 레포지토리의 활성 수정 내역을 분석한다.
- `.mam/agent-sessions.yaml`을 읽어 현재 러닝 상태인 타 에이전트 목록을 확인하고, 자신의 지정된 `role`을 검증한다. - `.mam/agent-sessions.yaml`을 읽어 현재 러닝 상태인 타 에이전트 목록을 확인하고, 자신의 지정된 `role`을 검증한다.
+10 -7
View File
@@ -122,12 +122,14 @@ To ensure that agents running in TMUX environments do not lose debug logs or pre
- **Core Principle**: To prevent TUI character loss, truncation, and layout breakage during sequential input typing, all collaborative workflows must favor file-based markdown 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**: - **Rules & Protocols**:
- **Exception**: Extremely simple prompts (e.g., "Re-evaluate", "Check status", "Proceed") of 1 or 2 lines may be sent directly via tmux 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**: All detailed task briefs, specifications, and instructions must 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."` - **Task Delegation**:
- **Result Reporting & Feedback**: All detailed review results, design proposals, implementation reports, and audit comments must be saved as Markdown files. To separate human-readable documents from the machine job registry (`.mam/jobs/`), every agent must save its outputs under `.mam/reports/<tmux_session_name>/` (using the full tmux session name matching the `name` field in `.mam/agent-sessions.yaml`, e.g., `.mam/reports/<workspace_slug>-creator-<agent>/report-<job_id>.md`) to ensure strict isolation across multiple roles/instances. - *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."`
- **Cleanup & Retention Contract**: - *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.
- Files under `.mam/reports/` are audit-trail artifacts. - **Result Reporting & Feedback**:
- These folders should be cleaned up automatically during `stop_session.sh` when a session is gracefully stopped or purged (`--purge-conversation`). - *Manual/Durable reviews*: Detailed reviews, design proposals, or audit reports must be saved under `.mam/reports/<tmux_session_name>/report-<job_id>.md`.
- Durable outcomes (such as 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/`) to preserve collaborative audit trails in version history, instead of remaining in the gitignored `.mam/` runtime tree. - *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/<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 ### ⏱️ Timeout Configuration & Alignment Rules
- **Job Execution Limits (`timeout_sec` & `idle_timeout_sec`)**: Each job independently manages its overall execution timeout (`timeout_sec`, default 3600s) and idle timeout without receiving messages (`idle_timeout_sec`, default 120s). - **Job Execution Limits (`timeout_sec` & `idle_timeout_sec`)**: Each job independently manages its overall execution timeout (`timeout_sec`, default 3600s) and idle timeout without receiving messages (`idle_timeout_sec`, default 120s).
@@ -157,7 +159,8 @@ Newly spawned Team Leader agents must align their context using the `--onboard`
```bash ```bash
bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace "$(pwd)" --agent <agent> --role <role> --onboard bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace "$(pwd)" --agent <agent> --role <role> --onboard
``` ```
2. **Orienting the Agent**: The agent session starts up and automatically receives a registered job instructing it to: This automated onboarding workflow registers a job, provisioning the brief under `.mam/jobs/<job_id>/brief.md` and sending a short pointer to the agent session.
2. **Orienting the Agent**: The agent session starts up and automatically receives the registered onboarding brief instructing it to:
- Read `README.md` and `.agents/MULTI_AGENT_RULES.md` to align with design principles and constraints. - Read `README.md` and `.agents/MULTI_AGENT_RULES.md` to align with design principles and constraints.
- Run `git status` and `git diff` to analyze active modifications. - Run `git status` and `git diff` to analyze active modifications.
- Read `.mam/agent-sessions.yaml` to identify other running agents and verify its own assigned `role`. - Read `.mam/agent-sessions.yaml` to identify other running agents and verify its own assigned `role`.
-36
View File
@@ -1,36 +0,0 @@
# 📋 MAM Installer & Manual Alignment Re-Review Brief
We have resolved all structural inconsistencies and runtime blockers identified by Planner Claude, Creator Claude, and Reviewer Cline. Please perform a final review and run diagnostics.
---
## 🛠️ Refactoring & Alignment Highlights
1. **RC-1 (Attach Inconsistency Resolved)**:
- Aligned `create_session.sh` command examples in `INSTALL.md` and the installer epilogue to consistently include the `--tmux-server multi-agent-mux` flag. This matches the attach instructions (`tmux -L multi-agent-mux attach`).
2. **RC-2 (Hard Dependency Check Resolved)**:
- Python `pyyaml` library is now a **hard dependency**; the installer exits with `exit 1` if it is not found.
- Added `uuidgen` and `flock` to the `DEPS` array in `install_mam.sh` to ensure they are diagnosed and checked at install time.
3. **Durable Reports Path Conflict Resolved**:
- Updated `MULTI_AGENT_RULES.md` and `INSTALL.md` guidelines to instruct that durable/version-controlled reports must be copied and tracked under `.agents/reports/<session_name>/` instead of the gitignored `.mam/reports/` runtime cache.
- Migrated all existing reviewer reports from `.mam/reports/` to `.agents/reports/`.
4. **AGENTS.md Overwrite Protection**:
- `install_mam.sh` no longer clobbers an existing `AGENTS.md` by default. Instead, it checks for a MAM marker block and appends a pointer to `.agents/MULTI_AGENT_RULES.md` if not present.
5. **rsync reports/ Anchor Fix**:
- Switched `--exclude='reports/'` to `--exclude='/reports/'` in `rsync` to avoid unanchored directory mismatches.
---
## 🎯 Re-Review Instructions for Agents
1. **Reviewer Cline**:
- Re-run syntax, lint (`shellcheck`), symlink tests, and check dependencies.
- Save your final re-review verdict and write a Markdown report under `.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-mam-installer-final.md`.
2. **Creator Claude**:
- Review the latest codebase diff (`git diff d7e19fe~1 d7e19fe`).
- Validate RC-1 and RC-2 fixes and verify if all guidelines and manual scripts are synchronized.
- Output your final verdict (PASS/NOT PASS) and details.
3. **Planner Claude**:
- Evaluate the refactored layout and verify the architecture alignments against MAM standards.
- Confirm if the report migration and non-invasive AGENTS.md injection satisfy version control safety.
- Save your feedback or final verdict.
@@ -1,28 +0,0 @@
# 📋 세션 ID 중복 충돌 해결 종합 설계안 리뷰 요청 지시서
- **요청자**: Planner Agent (Antigravity)
- **수신자**: Reviewer Agent (Claude)
- **대상 세션**: `canary-projects-multi-agent-mux-creator-claude`
- **검토 대상 파일**: [session_isolation_discussion.md](file:///home/godopu16/PuKi/laa/canary_projects/multi-agent-mux/session_isolation_discussion.md)
---
## 1. 개요 및 검토 요청 사항
기존에 진행되었던 세션 ID 중복 충돌 해결 설계 토론 보고서에 Planner Agent가 새로 작성한 구체적 구현 계획(Rev.2)을 성공적으로 통합 및 단일화하였습니다.
수신자(Claude) 에이전트님은 통합된 [session_isolation_discussion.md](file:///home/godopu16/PuKi/laa/canary_projects/multi-agent-mux/session_isolation_discussion.md) 문서를 검토하시어 아래 기준을 만족하는지 검사해 주시기 바랍니다.
### 주요 검토 기준
1. **의견 반영의 정합성**: Claude 에이전트 본인이 2차 토론 및 `implementation_plan.session_isolation.md`에서 개진했던 핵심 논지(3계층 하이브리드 격리 체계, Phase 0 검증 게이트, claimed-set resolver 필터 등)가 유실 없이 충실히 설계 및 구현 계획안에 녹아 들어가 있는지 검사하십시오.
2. **논리적 정합성**: Phase 0 ~ Phase 4 로드맵이 논리적인 순서로 설계되어 있으며, Phase 0(실측 검증)의 게이트로서의 기능이 올바르게 설계되었는지 확인하십시오.
3. **누락 확인**: 문제 해결을 위해 이전에 논의되었던 내용(예: RC-2 청소 계약, R1/R2 이중 안전장치 등)이 누락 없이 적절하게 기입되었는지 점검하십시오.
---
## 2. 작업 결과 보고 양식
검토를 마친 후, 본인의 의견과 최종 승인 여부를 아래에 명시된 경로에 기록해 주십시오.
* **리포트 작성 경로**: `.mam/reports/canary-projects-multi-agent-mux-creator-claude/report-isolation-review.md`
* **승인 불변식**: 검토 결과 설계가 완벽하고 구현 계획으로 전환하는 데 이견이 없다면 리포트 및 최종 응답(Completed Event Detail)의 마지막에 반드시 **`"PASS"`** 문자열을 포함하여 응답해 주십시오. 만약 수정이나 보완이 필요하다면 구체적인 피드백을 전달해 주십시오.
@@ -1,28 +0,0 @@
# 📋 세션 ID 중복 충돌 해결 종합 설계안 리뷰 요청 지시서
- **요청자**: Planner Agent (Antigravity)
- **수신자**: Reviewer Agent (Cline)
- **대상 세션**: `canary-projects-multi-agent-mux-reviewer-cline`
- **검토 대상 파일**: [session_isolation_discussion.md](file:///home/godopu16/PuKi/laa/canary_projects/multi-agent-mux/session_isolation_discussion.md)
---
## 1. 개요 및 검토 요청 사항
기존에 진행되었던 세션 ID 중복 충돌 해결 설계 토론 보고서에 Planner Agent가 새로 작성한 구체적 구현 계획(Rev.2)을 성공적으로 통합 및 단일화하였습니다. 특히 모든 에이전트들을 격리 디렉터리 방식으로 일원화 관리하자는 사용자 피드백을 반영하였습니다.
수신자(Cline) 에이전트님은 통합 및 단순화된 [session_isolation_discussion.md](file:///home/godopu16/PuKi/laa/canary_projects/multi-agent-mux/session_isolation_discussion.md) 문서를 검토하시어 아래 기준을 만족하는지 검사해 주시기 바랍니다.
### 주요 검토 기준
1. **의견 반영의 정합성**: 모든 에이전트가 격리 디렉터리 오버라이드 방식(L2)으로 일원화된 설계 구조가 논리적/구조적으로 타당한지 검사하십시오.
2. **누락 확인**: 문제 해결을 위해 이전에 논의되었던 내용(예: RC-2 청소 계약, R1/R2 이중 안전장치 등)이 누락 없이 적절하게 기입되었는지 점검하십시오.
3. **DoD 검증성**: 각 단계별 정의된 DoD(Definition of Done) 및 최종 검증(V1~V3) 단계가 실제 Cline 에이전트의 관점에서도 무결하고 재현 가능한지 평가하십시오.
---
## 2. 작업 결과 보고 양식
검토를 마친 후, 본인의 의견과 최종 승인 여부를 아래에 명시된 경로에 기록해 주십시오.
* **리포트 작성 경로**: `.mam/reports/canary-projects-multi-agent-mux-reviewer-cline/report-isolation-review.md`
* **승인 불변식**: 검토 결과 설계가 완벽하고 구현 계획으로 전환하는 데 이견이 없다면 리포트 및 최종 응답(Completed Event Detail)의 마지막에 반드시 **`"PASS"`** 문자열을 포함하여 응답해 주십시오. 만약 수정이나 보완이 필요하다면 구체적인 피드백을 전달해 주십시오.
@@ -1,20 +0,0 @@
# Onboarding & Project Context Analysis Brief
안녕하세요 Reviewer Cline Agent.
당신은 본 프로젝트의 **리뷰어(Reviewer)** 역할을 위임받았습니다.
실무 또는 검수 작업을 위임받기 전에, 설계 규약에 따라 다음 맥락 파악 작업을 완료해 주세요.
## 📋 온보딩 요구사항 (Onboarding Checklist)
1. **설계 규약 및 제약사항 숙지**
- [README.md](../../README.md) 및 [.agents/MULTI_AGENT_RULES.ko.md](../../.agents/MULTI_AGENT_RULES.ko.md)를 꼼꼼히 읽고 본 프레임워크의 규칙을 인지해 주세요.
- 특히 **TUI 뷰포트 절단 방지(Pane Snapshotting 3대 규칙)** 및 **마크다운 기반 협업 규약**을 지켜야 합니다.
2. **레포지토리 활성 수정 내역 분석**
- `git status``git diff`를 실행하여 현재 진행 중인 배포 스크립트 URL 파라미터화 작업 관련 변경 사항을 분석하세요.
3. **역할 및 타 에이전트 정보 검증**
- [.mam/agent-sessions.yaml](../agent-sessions.yaml)을 읽어 자신의 지정된 역할(`role: reviewer`)과 현재 러닝 상태인 타 에이전트 목록을 확인하세요.
## 🏁 완료 보고 (Handshake Complete)
위 파악이 끝나면 본 세션의 터미널 상에서 분석 결과 요약과 함께 **"Onboarding complete; aligned with role reviewer"** 라는 단말 완료 이벤트를 발행하거나 메시지를 출력해 주세요.
+72 -99
View File
@@ -23,7 +23,7 @@ WORKSPACE_ROOT="$(cd "$SKILL_DIR/../.." && pwd)"
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}" AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}"
# Central TUI dialog and readiness validation tokens (OP-6) # Central TUI dialog and readiness validation tokens (OP-6)
_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_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|Resuming the full session|Resume from summary'
_MAM_READY_TOKENS_CLAUDE='Anthropic|Assistant|Chat|Welcome|projects' _MAM_READY_TOKENS_CLAUDE='Anthropic|Assistant|Chat|Welcome|projects'
# Workspace-relative defaults with environment overrides (Phase Z) # Workspace-relative defaults with environment overrides (Phase Z)
@@ -119,52 +119,50 @@ tmux() {
# Fallback to TMUX_SERVER_NAME or 'default' if not registered or field is missing. # Fallback to TMUX_SERVER_NAME or 'default' if not registered or field is missing.
# Prints the resolved server name on stdout. # Prints the resolved server name on stdout.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
resolve_tmux_server() { load_state_json() {
local session_name="$1" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
SESSION_NAME="$session_name" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
import os, sys, sqlite3, json, yaml import os, sys, sqlite3, json, yaml
name = os.environ['SESSION_NAME']
yaml_path = os.environ['YAML_PATH'] yaml_path = os.environ['YAML_PATH']
db_path = os.path.splitext(yaml_path)[0] + '.db' db_path = os.path.splitext(yaml_path)[0] + '.db'
d = {}
db_sessions = []
try: try:
if os.path.exists(db_path): if os.path.exists(db_path):
conn = sqlite3.connect(db_path, timeout=60.0) conn = sqlite3.connect(db_path, timeout=60.0)
try:
row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone()
if row:
s = json.loads(row[0])
server = s.get('tmux_server')
if server:
print(server)
sys.exit(0)
except sqlite3.OperationalError:
pass
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
if row: if row:
d = json.loads(row[0]) d = json.loads(row[0])
for s in d.get('tmux_sessions', []): try:
if s.get('name') == name: cursor = conn.execute('SELECT data FROM sessions')
server = s.get('tmux_server') for r in cursor.fetchall():
if server: db_sessions.append(json.loads(r[0]))
print(server) d['tmux_sessions'] = db_sessions
sys.exit(0) except sqlite3.OperationalError:
pass
conn.close() conn.close()
elif os.path.exists(yaml_path): elif os.path.exists(yaml_path):
with open(yaml_path) as f: with open(yaml_path) as f:
d = yaml.safe_load(f) or {} d = yaml.safe_load(f) or {}
for s in d.get('tmux_sessions', []):
if s.get('name') == name:
server = s.get('tmux_server')
if server:
print(server)
sys.exit(0)
except Exception: except Exception:
pass pass
# Fallback print(json.dumps(d, ensure_ascii=False))
print(os.environ.get('TMUX_SERVER_NAME', 'default'))
PYEOF PYEOF
} }
resolve_tmux_server() {
local session_name="$1"
MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$session_name" python3 -c "
import sys, os, json
name = os.environ['SESSION_NAME']
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
for s in d.get('tmux_sessions', []):
if s.get('name') == name:
print(s.get('tmux_server', 'default'))
sys.exit(0)
print(os.environ.get('TMUX_SERVER_NAME', 'default'))
"
}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# derive_session_name <workspace> <agent> # derive_session_name <workspace> <agent>
# #
@@ -333,7 +331,13 @@ def get_terminal_set(d):
return {s.get('name'): s.get('status') for s in d.get('tmux_sessions', []) if s.get('status') in ('stopped', 'terminated', 'archived')} return {s.get('name'): s.get('status') for s in d.get('tmux_sessions', []) if s.get('status') in ('stopped', 'terminated', 'archived')}
def get_all_sessions_status(d): def get_all_sessions_status(d):
return {s.get('name'): s.get('status') for s in d.get('tmux_sessions', [])} import hashlib
res = {}
for s in d.get('tmux_sessions', []):
name = s.get('name')
ser = json.dumps(s, sort_keys=True)
res[name] = hashlib.sha256(ser.encode('utf-8')).hexdigest()
return res
os.makedirs(os.path.dirname(db_path) or '.', exist_ok=True) os.makedirs(os.path.dirname(db_path) or '.', exist_ok=True)
conn = sqlite3.connect(db_path, timeout=60.0) conn = sqlite3.connect(db_path, timeout=60.0)
@@ -495,15 +499,12 @@ PYEOF
find_workspace_uuid() { find_workspace_uuid() {
local workspace="$1" agent="$2" session_name="${3:-}" local workspace="$1" agent="$2" session_name="${3:-}"
local abs; abs="$(cd "$workspace" 2>/dev/null && pwd)" || abs="$workspace" local abs; abs="$(cd "$workspace" 2>/dev/null && pwd)" || abs="$workspace"
WS_ABS="$abs" AGENT="$agent" TARGET_SESSION="$session_name" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF' MAM_STATE_JSON="$(load_state_json)" WS_ABS="$abs" AGENT="$agent" TARGET_SESSION="$session_name" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF'
import os, json, glob, sqlite3 import os, sys, json, glob, sqlite3
import yaml
ws = os.environ['WS_ABS'] ws = os.environ['WS_ABS']
agent = os.environ['AGENT'] agent = os.environ['AGENT']
home = os.environ['HOME_DIR'] home = os.environ['HOME_DIR']
yaml_path = os.environ['YAML_PATH']
db_path = os.path.splitext(yaml_path)[0] + '.db'
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
target = os.environ.get('TARGET_SESSION', '') target = os.environ.get('TARGET_SESSION', '')
@@ -512,13 +513,10 @@ OWN_KEY = {'claude': 'claude_session_id_own', 'agy': 'agy_conversation_id_own',
def iso_root_of(s): def iso_root_of(s):
# T5: per-row isolation root (all-L2). None => legacy global state dirs.
iso = s.get('isolation') iso = s.get('isolation')
return iso.get('root') if isinstance(iso, dict) else None return iso.get('root') if isinstance(iso, dict) else None
# exists checks take an optional isolation root; path templates per lever
# (Phase 0 measured — note cline's isolated layout is <root>/sessions/, NOT data/sessions/).
def jsonl_exists(uuid, iso=None): def jsonl_exists(uuid, iso=None):
base = f"{iso}/projects" if iso else claude_project_dir base = f"{iso}/projects" if iso else claude_project_dir
key = ws.replace('/', '-').replace('_', '-') key = ws.replace('/', '-').replace('_', '-')
@@ -553,37 +551,21 @@ def own_exists(a, uuid, iso=None):
'hermes': hermes_exists, 'cline': cline_exists}[a](uuid, iso) 'hermes': hermes_exists, 'cline': cline_exists}[a](uuid, iso)
running_ids = set() # Ingest the merged state dictionary from stdin pipeline
try: try:
if os.path.exists(db_path): d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
conn = sqlite3.connect(db_path, timeout=60.0)
try:
cursor = conn.execute("SELECT data FROM sessions WHERE status='running'")
for r_row in cursor.fetchall():
s_data = json.loads(r_row[0])
# the target row's own ids are legitimately "claimed by itself" — don't filter them
if target and s_data.get('name') == target:
continue
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']:
val = s_data.get(k)
if val:
running_ids.add(val)
except sqlite3.OperationalError:
pass
conn.close()
if not running_ids and os.path.exists(yaml_path):
with open(yaml_path) as f:
d_state = yaml.safe_load(f) or {}
for s_item in d_state.get('tmux_sessions', []):
if s_item.get('status') == 'running':
if target and s_item.get('name') == target:
continue
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']:
val = s_item.get(k)
if val:
running_ids.add(val)
except Exception: except Exception:
pass d = {}
running_ids = set()
for s_item in d.get('tmux_sessions', []):
if s_item.get('status') == 'running':
if target and s_item.get('name') == target:
continue
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']:
val = s_item.get(k)
if val:
running_ids.add(val)
def emit(u): def emit(u):
@@ -593,35 +575,12 @@ def emit(u):
raise SystemExit(0) raise SystemExit(0)
# 1) per-row own id for THIS workspace (optimized with direct sqlite query if db exists) # Fetch all sessions matching this workspace
sessions = [] sessions = []
try: for s in d.get('tmux_sessions', []):
if os.path.exists(db_path): if isinstance(s, dict) and (s.get('pane') or {}).get('cwd') == ws:
conn = sqlite3.connect(db_path, timeout=60.0) sessions.append(s)
has_sessions_table = False
try:
cursor = conn.execute('SELECT data FROM sessions WHERE pane_cwd=?', (ws,))
for row in cursor.fetchall():
sessions.append(json.loads(row[0]))
has_sessions_table = True
except sqlite3.OperationalError:
pass
if not has_sessions_table or not sessions:
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
if row:
d = json.loads(row[0])
for s in d.get('tmux_sessions', []):
if isinstance(s, dict) and (s.get('pane') or {}).get('cwd') == ws:
sessions.append(s)
conn.close()
elif os.path.exists(yaml_path):
with open(yaml_path) as f:
d = yaml.safe_load(f) or {}
for s in d.get('tmux_sessions', []):
if isinstance(s, dict) and (s.get('pane') or {}).get('cwd') == ws:
sessions.append(s)
except Exception:
pass
# T5: target-session mode — resolve strictly within the named row's scope. # T5: target-session mode — resolve strictly within the named row's scope.
# An isolated row's conversation lives ONLY in its isolation root (which holds # An isolated row's conversation lives ONLY in its isolation root (which holds
@@ -1191,15 +1150,27 @@ send_keys_safe() {
_sks_tmux paste-buffer -b "sks_$job_id" -t "$sess" _sks_tmux paste-buffer -b "sks_$job_id" -t "$sess"
_sks_tmux delete-buffer -b "sks_$job_id" 2>/dev/null || true _sks_tmux delete-buffer -b "sks_$job_id" 2>/dev/null || true
sleep 0.5 sleep 0.5
_pane_capture "$sess" | grep -Fq "$marker" || { echo "send_keys_safe: paste not visible ($sess)" >&2; return 3; } 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
was_popup=1
elif ! printf '%s\n' "$pane_content" | grep -Fq "$marker"; then
echo "send_keys_safe: paste not visible ($sess)" >&2
return 3
fi
for try in 1 2 3; do for try in 1 2 3; do
pre_submit=$(_pane_capture "$sess") pre_submit=$(_pane_capture "$sess")
_sks_tmux send-keys -t "$sess" C-m _sks_tmux send-keys -t "$sess" C-m
sleep "$try" sleep "$try"
# Submitted = input area released the text AND rendering changed after Enter. local cur_content
if ! _pane_tail "$sess" 3 | grep -Fq "$marker" \ cur_content=$(_pane_capture "$sess")
&& [ "$(_pane_capture "$sess")" != "$pre_submit" ]; then # Hardened Submission Checks
if [ "$was_popup" = "0" ] && ! _pane_tail "$sess" 3 | grep -Fq "$marker" && [ "$cur_content" != "$pre_submit" ]; then
return 0
elif [ "$was_popup" = "1" ] && \
! printf '%s\n' "$cur_content" | grep -Eq "Pasted text|paste again to expand" && \
[ "$cur_content" != "$pre_submit" ]; then
return 0 return 0
fi fi
done done
@@ -1221,6 +1192,8 @@ handle_startup_dialogs() {
_sks_tmux send-keys -t "$sess" Down _sks_tmux send-keys -t "$sess" Down
sleep 0.3 sleep 0.3
_sks_tmux send-keys -t "$sess" Enter _sks_tmux send-keys -t "$sess" Enter
elif printf '%s\n' "$pane" | grep -q 'Resuming the full session'; then
_sks_tmux send-keys -t "$sess" Enter
elif printf '%s\n' "$pane" | grep -Eq "$_MAM_READY_TOKENS_CLAUDE"; then elif printf '%s\n' "$pane" | grep -Eq "$_MAM_READY_TOKENS_CLAUDE"; then
return 0 return 0
fi fi
@@ -107,6 +107,25 @@ cmd_submit() {
--max-iterations "$MAX_ITERATIONS")" --max-iterations "$MAX_ITERATIONS")"
echo "registered job: $JOB_ID" echo "registered job: $JOB_ID"
# 1-1) Provision job directory and write direct brief.md (MAM Job Restructuring)
local job_dir="$REGISTRY_DIR/$JOB_ID"
if [[ "$DRY_RUN" != "1" ]]; then
mkdir -p "$job_dir"
cat <<EOF > "$job_dir/brief.md"
# 📋 Brief: Job $JOB_ID Delegation
- **Job ID**: $JOB_ID
- **Target Agent**: $AGENT (session: $AGENT_SESSION)
- **Role**: Worker
- **Timeout**: $TIMEOUT s (Idle: $IDLE_TIMEOUT s)
- **Output Report Path**: .mam/jobs/$JOB_ID/$AGENT-reports/report-final.md
## 🔎 Task Description
$PROMPT
EOF
echo "provisioned job directory: $job_dir"
fi
if [[ "$TYPE" == "direct" ]]; then if [[ "$TYPE" == "direct" ]]; then
# 2) START THE SUBSCRIBER FIRST (ordering dependency — MQTT does not queue # 2) START THE SUBSCRIBER FIRST (ordering dependency — MQTT does not queue
# non-retained messages for absent subscribers). # non-retained messages for absent subscribers).
@@ -140,17 +159,7 @@ cmd_submit() {
# an id from an earlier session is the #1 reason a delegated job sits idle and # 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 # times out (see SKILL.md "Wrong job_id propagated to the agent"). We make the
# freshness explicit in the instruction header. # freshness explicit in the instruction header.
local instructions="Your job_id is \"$JOB_ID\" (the one just registered for THIS delegation — read it from the registry record, do NOT reuse any job_id you saw in earlier runs). 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>'."
On start run: $pub --event started.
On permission/tool prompt run: $pub --event permission_required --detail '<tool>:<what>'.
On progress (optional): $pub --event progress --detail '<short status>'.
On success run: $pub --event completed --detail '<one-line summary>'.
On failure run: $pub --event error --detail '<one-line reason>'.
The subscriber for this job_id is already running; your completed/error event ends the job. Exit codes: 0 completed, 1 error, 2 publish failure.
Task: $PROMPT"
run_agent "$JOB_ID" "$instructions" run_agent "$JOB_ID" "$instructions"
@@ -203,6 +212,26 @@ Task: $PROMPT"
echo "Session: $current_session" echo "Session: $current_session"
echo "==================================================" echo "=================================================="
# 1-1) Provision job directory and write iteration brief.md (MAM Job Restructuring)
local job_dir="$REGISTRY_DIR/$JOB_ID"
local clean_session="${current_session#tmux:}"
if [[ "$DRY_RUN" != "1" ]]; then
mkdir -p "$job_dir"
cat <<EOF > "$job_dir/brief.md"
# 📋 Brief: Job $JOB_ID Delegation (Iteration $iteration)
- **Job ID**: $JOB_ID
- **Target Agent/Session**: $current_session
- **Role**: $current_role
- **Iteration**: $iteration
- **Output Report Path**: .mam/jobs/$JOB_ID/${clean_session}-reports/report-final.md
## 🔎 Task Description
$current_prompt
EOF
echo "provisioned iteration brief: $job_dir/brief.md"
fi
# Update job details in registry # Update job details in registry
"$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" update \ "$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" update \
--job "$JOB_ID" \ --job "$JOB_ID" \
@@ -238,17 +267,7 @@ Task: $PROMPT"
# Format instruction block # Format instruction block
local pub="$PY $SCRIPT_DIR/scripts/publish_event.py --registry-dir $REGISTRY_DIR --job $JOB_ID" local pub="$PY $SCRIPT_DIR/scripts/publish_event.py --registry-dir $REGISTRY_DIR --job $JOB_ID"
local instructions="Your job_id is \"$JOB_ID\" (the one just registered for THIS delegation — read it from the registry record, do NOT reuse any job_id you saw in earlier runs). local instructions="Your job_id is \"$JOB_ID\". Detailed task requirements, instructions, and target output paths for iteration $iteration 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>'."
On start run: $pub --event started.
On permission/tool prompt run: $pub --event permission_required --detail '<tool>:<what>'.
On progress (optional): $pub --event progress --detail '<short status>'.
On success run: $pub --event completed --detail '<one-line summary>'.
On failure run: $pub --event error --detail '<one-line reason>'.
The subscriber for this job_id is already running; your completed/error event ends the job. Exit codes: 0 completed, 1 error, 2 publish failure.
Task: $current_prompt"
# Trigger agent # Trigger agent
run_agent "$JOB_ID" "$instructions" "$current_session" run_agent "$JOB_ID" "$instructions" "$current_session"
@@ -389,8 +408,8 @@ run_agent() {
# Before launching the agent, set up error trap to publish error event # Before launching the agent, set up error trap to publish error event
if [ -n "${job_id:-}" ] && [ -n "${PY:-}" ]; then if [ -n "${job_id:-}" ] && [ -n "${PY:-}" ]; then
local pub_script="$SCRIPT_DIR/scripts/publish_event.py" pub_script="$SCRIPT_DIR/scripts/publish_event.py"
trap 'rc=$?; if [ $rc -ne 0 ]; then "$PY" "$pub_script" --job "$job_id" --event error --detail "agent bootstrap failed (exit $rc)"; fi' EXIT trap "rc=\$?; if [ \$rc -ne 0 ]; then \"$PY\" \"$pub_script\" --job '$job_id' --event error --detail 'agent bootstrap failed (exit '\$rc')'; fi" EXIT
fi fi
echo "살아있는 에이전트 세션 '$sess'에 작업을 위임합니다..." echo "살아있는 에이전트 세션 '$sess'에 작업을 위임합니다..."
@@ -15,6 +15,7 @@
set -euo pipefail set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
export WORKSPACE_ROOT
STATE_DIR="${AGENT_SESSIONS_STATE_DIR:-$WORKSPACE_ROOT/.cache/multi-agent-mux-monitor}" STATE_DIR="${AGENT_SESSIONS_STATE_DIR:-$WORKSPACE_ROOT/.cache/multi-agent-mux-monitor}"
@@ -117,16 +118,21 @@ _now = datetime.now(timezone.utc)
_changed = False _changed = False
for s in d.get('tmux_sessions', []): for s in d.get('tmux_sessions', []):
if s.get('delegate_job_id') == _jid and s.get('status') == 'running': if s.get('delegate_job_id') == _jid and s.get('status') == 'running':
s['status'] = 'terminated'
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 + ')'
_name = s.get('name') _name = s.get('name')
_srv = s.get('tmux_server') or 'default' _srv = s.get('tmux_server') or 'default'
_cmd = ['tmux'] + (['-L', _srv] if _srv != 'default' else []) + ['kill-session', '-t', _name] if _event == 'completed':
subprocess.run(_cmd, capture_output=True) s['delegate_job_id'] = None
print('MQTT Monitor: terminated + killed ' + str(_name) + ' on ' + str(_srv), flush=True) print('MQTT Monitor: job completed on ' + str(_name) + ' — session kept alive', flush=True)
_changed = True _changed = True
else:
s['status'] = 'terminated'
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 + ')'
_cmd = ['tmux'] + (['-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
if not _changed: if not _changed:
raise SystemExit(0) # nothing matched — skip the write entirely raise SystemExit(0) # nothing matched — skip the write entirely
''' '''
@@ -304,31 +310,18 @@ claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/proje
now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
# atomic 래퍼에서는 d 가 이미 로드돼 있음. env_python(dry-run)에서는 여기서 로드.
try: try:
d d
except NameError: except NameError:
import sqlite3 import subprocess
db_path = os.path.splitext(yaml_path)[0] + '.db'
d = {} d = {}
try: try:
if os.path.exists(db_path): ws_root = os.environ.get('WORKSPACE_ROOT')
conn = sqlite3.connect(db_path, timeout=60.0) if not ws_root:
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() ws_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..'))
if row: d = json.loads(row[0]) script = f"source '{ws_root}/.agents/skills/lib.sh' && load_state_json"
out = subprocess.check_output(['bash', '-c', script], stderr=subprocess.DEVNULL)
try: d = json.loads(out.decode('utf-8'))
db_sessions = []
cursor = conn.execute('SELECT data FROM sessions')
for s_row in cursor.fetchall():
db_sessions.append(json.loads(s_row[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: except Exception:
pass pass
@@ -57,40 +57,15 @@ if { [ "$AGENT" = "agy" ] || [ "$AGENT" = "hermes" ] || [ "$AGENT" = "cline" ];
CHILD_PID="${CHILD_PID:-0}" CHILD_PID="${CHILD_PID:-0}"
fi fi
DELEGATE_JOB_ID=$(env_python "$AGENT_SESSIONS_YAML" SESSION_NAME="$SESSION_NAME" <<'PYEOF' DELEGATE_JOB_ID=$(MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$SESSION_NAME" python3 -c "
import os, sys, sqlite3, json, yaml import sys, os, json
name = os.environ['SESSION_NAME'] name = os.environ['SESSION_NAME']
yaml_path = os.environ['YAML_PATH'] d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
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)
try:
row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone()
if row:
s = json.loads(row[0])
print(s.get('delegate_job_id', '') or '')
raise SystemExit(0)
except sqlite3.OperationalError:
pass
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 Exception:
pass
for s in d.get('tmux_sessions', []): for s in d.get('tmux_sessions', []):
if s.get('name') == name: if s.get('name') == name:
print(s.get('delegate_job_id', '') or '') print(s.get('delegate_job_id', '') or '')
raise SystemExit(0) sys.exit(0)
raise SystemExit(0) ")
PYEOF
)
atomic_dump_yaml "$AGENT_SESSIONS_YAML" \ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
SESSION_NAME="$SESSION_NAME" UUID="$UUID" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \ SESSION_NAME="$SESSION_NAME" UUID="$UUID" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
@@ -28,38 +28,17 @@ fi
# Resolved relative to this script — no hardcoded absolute path (review item 6). # Resolved relative to this script — no hardcoded absolute path (review item 6).
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../" && pwd)" PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../" && pwd)"
DRIFT_JSON="$DRIFT_JSON" env_python "$AGENT_SESSIONS_YAML" PROJECT_ROOT="$PROJECT_ROOT" <<'PYEOF' MAM_STATE_JSON="$(load_state_json)" DRIFT_JSON="$DRIFT_JSON" env_python "$AGENT_SESSIONS_YAML" PROJECT_ROOT="$PROJECT_ROOT" <<'PYEOF'
import os, json, glob import os, sys, json, glob
import yaml
yaml_path = os.environ['YAML_PATH']
home = os.environ['HOME_DIR'] home = os.environ['HOME_DIR']
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
drift = json.loads(os.environ['DRIFT_JSON']) drift = json.loads(os.environ['DRIFT_JSON'])
db_path = os.path.splitext(yaml_path)[0] + '.db'
d = {}
import sqlite3
try: try:
if os.path.exists(db_path): d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
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:
db_sessions = []
cursor = conn.execute('SELECT data FROM sessions')
for s_row in cursor.fetchall():
db_sessions.append(json.loads(s_row[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: except Exception:
pass d = {}
alive = set(drift.get('tmux_sessions_alive', [])) alive = set(drift.get('tmux_sessions_alive', []))
drift_by_name = {} drift_by_name = {}
@@ -83,44 +83,18 @@ fi
# 세션이 YAML 에 있는지 + 해당 row 의 워크스페이스 cwd 및 delegate_job_id 추출. # 세션이 YAML 에 있는지 + 해당 row 의 워크스페이스 cwd 및 delegate_job_id 추출.
# JSON 으로 emit — cwd 에 '|' 가 들어가도 안전 (review item 7; 기존 cwd|jid 파서 대체). # JSON 으로 emit — cwd 에 '|' 가 들어가도 안전 (review item 7; 기존 cwd|jid 파서 대체).
MAPPED_DATA=$(env_python "$AGENT_SESSIONS_YAML" SESSION_NAME="$SESSION_NAME" <<'PYEOF' MAPPED_DATA=$(MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$SESSION_NAME" python3 -c "
import os, sys, json, yaml, sqlite3 import sys, os, json
name = os.environ['SESSION_NAME'] name = os.environ['SESSION_NAME']
yaml_path = os.environ['YAML_PATH'] d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
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)
try:
row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone()
if row:
s = json.loads(row[0])
cwd = (s.get('pane') or {}).get('cwd', '')
jid = s.get('delegate_job_id', '') or ''
print(json.dumps({"cwd": cwd, "job_id": jid}))
raise SystemExit(0)
except sqlite3.OperationalError:
pass
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 Exception:
pass
for s in d.get('tmux_sessions', []): for s in d.get('tmux_sessions', []):
if s.get('name') == name: if s.get('name') == name:
cwd = (s.get('pane') or {}).get('cwd', '') cwd = (s.get('pane') or {}).get('cwd', '')
jid = s.get('delegate_job_id', '') or '' jid = s.get('delegate_job_id', '') or ''
print(json.dumps({"cwd": cwd, "job_id": jid})) print(json.dumps({'cwd': cwd, 'job_id': jid}))
raise SystemExit(0) sys.exit(0)
raise SystemExit(7) sys.exit(7)
PYEOF ") || {
) || {
echo "ERROR: session '$SESSION_NAME' not in $AGENT_SESSIONS_YAML" >&2 echo "ERROR: session '$SESSION_NAME' not in $AGENT_SESSIONS_YAML" >&2
exit 1 exit 1
} }
-48
View File
@@ -1,48 +0,0 @@
# 📑 MAM 스킬 최적화 세션 인수인계 요약서 (handoff.md)
이 인수인계서는 **Multi-Agent Mux (MAM)** 프로젝트에서 진행된 **개발 스킬 최적화 Phase 1 및 Phase 2 일부 구현 완료** 상태와, 에이전트 세션 토큰 한계(Rate limit) 및 검수 범위 제약으로 인해 보류된 **나머지 잔여 최적화 과제들**을 다음 차수에 연계하기 위해 작성되었습니다.
---
## 1. 🏁 현재 완료된 최적화 성과 (Phase 1 & 2 완료)
* **OP-1 (반응형 세션 종료 - Latency)**: `lib.sh``_wait_session_gone` 헬퍼를 도입하여 고정 8초 대기하던 문제를 조기 종료 감지 방식으로 해결 (평균 종료 시간 **8초 ➡️ 0.3초 미만**). `stop_session.sh``set -e` 강제 폭사 방지 `|| true` 가드 탑재.
* **OP-2 (MQTT 구독 동기화 핸드셰이크 - Latency)**: `job_subscriber.py``on_subscribe` 콜백 신호를 추가하고, `delegate-job`에서 `SUBSCRIBED` sentinel 폴링 및 `kill -0 $sub_pid` 프로세스 생존 검사를 병행 기입하여 **WAN 환경의 이벤트 유실을 박멸**하고 즉시 기동하도록 변경.
* **OP-3 (Event Loop CPU 점유 최적화 - Latency)**: `reconcile.sh` 백그라운드 파이썬 대기 루프를 `threading.Event().wait` Pacing으로 교체하여 **유휴 상태 CPU 점유율을 0%**로 단축.
* **OP-4 (Tmux Dispatcher 단일화 - DRY)**: 격리 서버 대응을 단일 canonical helper인 `mam_tmux`로 일원화하고, `_REAL_TMUX_PATH` 매핑을 강제하여 **무한 재귀(Stack Overflow) 리스크를 완전히 제거**.
* **OP-6 & OP-7 (토큰 상수화 및 zsh 소싱 가드 - Portability)**: 대화창 감지 정규식 토큰 단일화 및 zsh sourcing 방지를 위한 안전 경고 트랩 탑재.
두 리뷰어(Cline & Creator) 및 플래너 에이전트로부터 최종 정식 **PASS****APPROVED** 서명을 획득하여 릴리스 커밋 완수 (`7eeb4b7`).
---
## ⏭️ 추후 수행할 잔여 최적화 과제 (Next Action Items)
다음 세션에서 에이전트(Claude 등)에게 검수받고 실제로 이행해야 할 목록입니다:
### 1. OP-5: SQLite / YAML 데이터 로더 중복 제거 (DRY)
* **대상 파일**: `lib.sh` (3개소), `stop_session.sh:87`, `status.sh:42`, `update_yaml_resumed.sh:66`, `reconcile.sh:298`
* **현황 및 원인**: SQLite와 YAML에서 에이전트 병합 세션 상태를 로드하기 위한 파이썬 heredoc 블록이 **7개 파일에 복사-붙여넣기**되어 있습니다.
* **개선 방향**: `lib.sh` 내에 `load_state_json` 공통 함수를 단일 구현하고 스크립트 파일들이 이를 통해 파싱된 JSON 스트림만 읽도록 개선한 뒤, 플래너/리뷰어 검수 PASS를 획득해야 합니다.
### 2. OP-8: Degraded 모드 복구 루프 예외 처리 강화 (Observability)
* **대상 파일**: `reconcile.sh:269`
* **현황 및 원인**: 브로커 단절 시 복구를 유도하는 루프의 에러 출력이 `>/dev/null 2>&1 || true` 로 차단되어 있어 데이터베이스 락 등의 심각한 영구 장애 시에도 모니터가 성공인 척 묵인됩니다.
* **개선 방향**: 에러 출력을 캡처 및 로깅하고, 5회 연속 장애 시 복구 감시 프로세스를 종료 후 자가 재기동(Supervisor restart)하도록 보완하고 검수받아야 합니다.
### 3. Phase 3 이식성 보완 과제
* **OP-9 (POSIX NFS 탐지)**: GNU 전용 `df --output`을 POSIX 호환 `df -P`로 교체하여 macOS/BSD에서 flock WAL 안전 스위치가 침묵 속에서 비활성화되던 이슈 수정 (FUTURE_WORKS FW-P1/FW-D3 대응).
* **OP-10 (Marker-walk 루트 탐색)**: 깊이가 하드코딩된 `../../../../` 경로 추적을 폐지하고, 최상위 워크스페이스 마커 파일 감지 방식으로 루트 절대 경로 탐색기 적용 (FUTURE_WORKS FW-P6 대응).
### 4. 📂 [신규 구상] 작업 단위(Job) 중심 디렉터리 구조 재구조화 검토 및 스킬 수정 계획
* **개요 및 아이디어 검토**:
* 현재 지시사항(brief) 파일은 다양한 경로에 산재하고 있으며, 에이전트들의 보고서 파일은 일괄적으로 `.agents/reports/<agent_session_name>/` 아래에 적재되어 특정 작업(Job) 단위의 보고서 역추적이 번거롭습니다.
* **사용자 제안 아키텍처**:
1. 작업이 등록되면 **`.mam/jobs/<job_id>/`** 라는 고유 작업 단위 폴더를 생성합니다.
2. 해당 폴더 하위에 **지시사항 brief 파일**을 기록해 다른 에이전트들에게 전달합니다.
3. 작업을 위임받아 수행한 다른 에이전트들의 결과 보고서들은 기존 공통 폴더 대신 해당 작업 폴더 내부인 **`.mam/jobs/<job_id>/{agent-id}-reports/`** 디렉터리에 격리 저장하도록 경로를 치환합니다.
* **검토 의견 (APPROVED)**: 작업(Job)을 중심으로 한 지시서와 결과 보고서의 귀속성과 추적성(Audit Trail)을 극대화할 수 있는 매우 훌륭한 구조적 개선안입니다.
* **스킬 수정 영향 범위 및 이행 계획**:
* `multi-agent-mux-delegate-job` 스크립트 수정: 잡 등록 시 `.mam/jobs/<job_id>/` 디렉터리를 자동 생성하고 지시서 마크다운을 그 하위로 동적 출력하도록 변경.
* 리뷰어 및 크리에이터 에이전트의 구동 인자 및 스크립트 수정: 보고서 출력 경로를 환경변수 또는 인자로 전달받은 잡 하위 디렉터리(`$JOB_DIR/$AGENT_ID-reports/`)로 치환하여 라이팅하도록 패치.
+50 -3
View File
@@ -105,7 +105,7 @@ if ! python3 -c "import yaml, sqlite3" &>/dev/null; then
fi fi
log_ok "Dependency checks completed." log_ok "Dependency checks completed."
# 2. Copy .agents/ folder # 2. Copy .agents/ folder and config tools
log_info "Deploying orchestration rules & skills (.agents/)..." log_info "Deploying orchestration rules & skills (.agents/)..."
mkdir -p "$TARGET_DIR/.agents" mkdir -p "$TARGET_DIR/.agents"
@@ -114,6 +114,18 @@ mkdir -p "$TARGET_DIR/.agents"
rsync -a --exclude='.git/' --exclude='/reports/' --exclude='*.log' --exclude='__pycache__/' --exclude='*.pyc' "$SRC_DIR/.agents/" "$TARGET_DIR/.agents/" rsync -a --exclude='.git/' --exclude='/reports/' --exclude='*.log' --exclude='__pycache__/' --exclude='*.pyc' "$SRC_DIR/.agents/" "$TARGET_DIR/.agents/"
log_ok "Deployed Rules and Skills under target's .agents/" log_ok "Deployed Rules and Skills under target's .agents/"
# Copy config templates and generate scripts (M-2)
if [ -f "$SRC_DIR/.env.example" ]; then
cp "$SRC_DIR/.env.example" "$TARGET_DIR/.env.example"
log_ok "Copied .env.example configuration template"
fi
if [ -f "$SRC_DIR/scripts/generate-env.sh" ]; then
mkdir -p "$TARGET_DIR/scripts"
cp "$SRC_DIR/scripts/generate-env.sh" "$TARGET_DIR/scripts/generate-env.sh"
chmod +x "$TARGET_DIR/scripts/generate-env.sh"
log_ok "Copied scripts/generate-env.sh helper tool"
fi
# 3. Copy AGENTS.md to root or inject guidelines pointer # 3. Copy AGENTS.md to root or inject guidelines pointer
log_info "Configuring developer guidelines (AGENTS.md)..." log_info "Configuring developer guidelines (AGENTS.md)..."
AGENTS_FILE="$TARGET_DIR/AGENTS.md" AGENTS_FILE="$TARGET_DIR/AGENTS.md"
@@ -143,19 +155,54 @@ fi
log_info "Registering runtime isolation blocks in .gitignore..." log_info "Registering runtime isolation blocks in .gitignore..."
GITIGNORE="$TARGET_DIR/.gitignore" GITIGNORE="$TARGET_DIR/.gitignore"
MAM_PATTERN="/.mam/" MAM_PATTERN="/.mam/"
VENV_PATTERN="/.venv/"
if [ -f "$GITIGNORE" ]; then if [ -f "$GITIGNORE" ]; then
# Register .mam/ if absent
if grep -Eq '^/?\.mam/?$' "$GITIGNORE"; then if grep -Eq '^/?\.mam/?$' "$GITIGNORE"; then
log_ok ".mam/ already registered in target's .gitignore." log_ok ".mam/ already registered in target's .gitignore."
else else
echo -e "\n# Multi-Agent Mux (MAM) runtime databases and isolation cache\n$MAM_PATTERN" >> "$GITIGNORE" echo -e "\n# Multi-Agent Mux (MAM) runtime databases and isolation cache\n$MAM_PATTERN" >> "$GITIGNORE"
log_ok "Appended /.mam/ registration to .gitignore." log_ok "Appended /.mam/ registration to .gitignore."
fi fi
# Register .venv/ if absent
if grep -Eq '^/?\.venv/?$' "$GITIGNORE"; then
log_ok ".venv/ already registered in target's .gitignore."
else
echo -e "\n# Python virtual environment\n$VENV_PATTERN" >> "$GITIGNORE"
log_ok "Appended /.venv/ registration to .gitignore."
fi
else else
echo -e "# Multi-Agent Mux (MAM) runtime databases and isolation cache\n$MAM_PATTERN" > "$GITIGNORE" echo -e "# Multi-Agent Mux (MAM) runtime databases and isolation cache\n$MAM_PATTERN\n\n# Python virtual environment\n$VENV_PATTERN" > "$GITIGNORE"
log_ok "Created .gitignore with /.mam/ exclusion." log_ok "Created .gitignore with MAM and .venv exclusions."
fi fi
# 5. Python Virtual Environment Setup (F-1)
log_info "Bootstrapping Python virtual environment (.venv) in target..."
VENV_NAME="$TARGET_DIR/.venv"
if [ ! -d "$VENV_NAME" ]; then
python3 -m venv "$VENV_NAME"
log_ok "Virtual environment created."
else
log_info "Virtual environment (.venv) already exists. Skipping creation."
fi
# Upgrade pip and install dependencies inside target venv
# shellcheck disable=SC1091
source "$VENV_NAME"/bin/activate
pip install --upgrade pip
REQ_FILE="$TARGET_DIR/.agents/skills/multi-agent-mux-delegate-job/requirements.txt"
if [ -f "$REQ_FILE" ]; then
log_info "Installing dependencies from $REQ_FILE..."
pip install -r "$REQ_FILE"
log_ok "Dependencies installed successfully."
else
log_warn "Could not find requirements file: $REQ_FILE. Installing defaults."
pip install "paho-mqtt>=2.0.0" pyyaml
fi
deactivate
# Done # Done
log_ok "MAM Installation completed successfully!" log_ok "MAM Installation completed successfully!"
cat <<EOF cat <<EOF