From 626f35adfc7d99f5550c0e3e6ade89a761825d35 Mon Sep 17 00:00:00 2001 From: Godopu Date: Thu, 16 Jul 2026 12:49:07 +0900 Subject: [PATCH] refactor: harden run_loop.sh verdict parser, add atomic promotion, and revise multi_agent_workflow.md guidelines --- .agents/multi_agent_workflow.md | 54 +++++++++----- .agents/skills/lib.sh | 4 + .../multi-agent-mux-loop/scripts/run_loop.sh | 73 +++++++++++++------ 3 files changed, 91 insertions(+), 40 deletions(-) diff --git a/.agents/multi_agent_workflow.md b/.agents/multi_agent_workflow.md index cd3660d..e6266f2 100644 --- a/.agents/multi_agent_workflow.md +++ b/.agents/multi_agent_workflow.md @@ -1,6 +1,6 @@ # Multi-Agent Collaboration Workflow Reference Guide -본 문서는 대규모 프로젝트나 정밀한 설계·구현 요구사항을 처리하기 위해 **Planner, Developer, Reviewer** 에이전트 간의 역할 분담 및 피드백 루프를 운영하는 **다중 에이전트 협업 워크플로우(Multi-Agent Collaboration Workflow)**를 정의합니다. +본 문서는 대규모 프로젝트나 정밀한 설계·구현 요구사항을 처리하기 위해 **Planner, Developer, Reviewer** 에이전트 간의 역할 분담 및 피드백 루프를 운영하는 **다중 에이전트 협업 워크플로우(Multi-Agent Collaboration Workflow)**를 정의합니다. 다수의 에이전트 간 협업을 위해서는 앞으로 수동 프롬프트 환류가 아닌, 자동화된 [multi-agent-mux-loop](skills/multi-agent-mux-loop/SKILL.md) 스킬만을 단독으로 사용하여 자율 루프를 기동합니다. --- @@ -76,30 +76,46 @@ ### 3단계: 피드백 루프 및 통과 (Review Phase) 1. Developer 에이전트가 리뷰어 세션에 작업 완료 사실과 변경 범위를 전달합니다. 2. 리뷰어들은 `git diff`를 바탕으로 개별 검증을 수행하고 보고서 형태의 리뷰 피드백을 출력합니다. - - **반려 (`NOT PASS`)** -> 피드백 요약본을 Planner 에이전트에게 전송하여 상위 레벨 계획(Rev.n) 개시. - - **통과 (`PASS`)** -> 모든 검토 사항이 해결되었음을 명시. + - **반려 (`[VERDICT: NOT PASS]`, 리포트 마지막에 단독 행으로)** -> 피드백 요약본을 Planner 에이전트에게 전송하여 상위 레벨 계획(Rev.n) 개시. + - **통과 (`[VERDICT: PASS]`, 리포트 마지막에 단독 행으로)** -> 모든 검토 사항이 해결되었음을 명시. 3. 모든 리뷰어가 PASS를 발행하면 작업을 완결하고, 다른 에이전트 세션들은 종료하지 않고 다음 태스크 지시가 있을 때까지 프롬프트 대기 상태(Standby)로 유지합니다. --- -## 3. Best Practices & 템플릿 +## 3. Best Practices & 자동화 지침 -### 3.1. 작업 시작 시 Planner 에이전트 프롬프트 템플릿 -``` -[역할 요구] -프로젝트의 구조 및 내용을 파악하고, 현재 작업 요건에 맞춰 구현 계획서(implementation_plan.md) 및 태스크 체크리스트(task.md)를 작성해 주세요. +수동 템플릿 작성 및 전달은 폐지되었습니다. 모든 에이전트 간 피드백 루프는 [multi-agent-mux-loop](skills/multi-agent-mux-loop/SKILL.md)를 활용해 자동으로 오케스트레이션합니다. -[검토 초점] -- 설계 수준에서 논리적 모순이 발생할 여지가 없는지 -- naive 구현과 대비되는 핵심 차별점이 코드 명세에 정확히 명시되었는지 +### 3.1. 개념-옵션 매핑 규격 + +워크플로우 단계별로 활용할 수 있는 `run_loop.sh` 옵션 규격은 다음과 같습니다: + +| 워크플로우 단계 | 해당 CLI 옵션 | 설명 | +| :--- | :--- | :--- | +| **Phase 1: Planning** | `--plan` | Planner 에이전트를 기동하여 최초 계획 작성을 강제합니다. | +| **Phase 1: Debate** | `--plan-talk N` | Planner와 Creator가 상호 대화식 챌린지 루프를 `N`회 돌며 계획을 교차 정제합니다. | +| **Phase 2: Execution** | (기본값) | `--target-agent`로 명시한 주 작업 세션에 코딩 태스크를 주입합니다. | +| **Phase 3: Review** | `--reviewer "A,B"` | 지정된 리뷰어 세션 리스트(`A`, `B` 등)에 교차 Peer Review를 위임합니다. | +| **Phase 3: Consensus** | `--all-reviewer` | 모든 리뷰어가 PASS를 냈을 때만 최종 통과를 허용합니다. (미지정 시 1명만 PASS여도 통과) | +| **Iterative Loop** | `--max-loop M` | NOT PASS 또는 린트 실패 시 최대 `M`회까지 Planner와 Creator 간 피드백 루프를 반복합니다. | + +### 3.2. 실전 자율 루프 기동 예시 + +```bash +bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ + --plan \ + --plan-talk 1 \ + --reviewer "canary-projects-multi-agent-mux-reviewer-cline,canary-projects-multi-agent-mux-reviewer-agy" \ + --all-reviewer \ + --max-loop 3 \ + --target-agent "canary-projects-multi-agent-mux-creator-agy" \ + --task "작업할 상세 요구사항을 여기에 입력..." ``` -### 3.2. 피드백 루프 환류 시 프롬프트 템플릿 -``` -Reviewer 검토 결과 구현 코드 차원에서 아래의 블로킹(NOT PASS) 피드백이 발생했습니다. -위 피드백을 수용하여 설계 문서를 수정하는 계획을 수립하고, implementation_plan.md (Rev.[N]) 및 task.md 내용을 업데이트해 주세요. +### 3.3. 동작성 제약 및 Pitfalls -[피드백 내용] -- [블로킹 항목 1] -- [블로킹 항목 2] -``` +1. **세션 가동 전제**: `run_loop.sh` 기동 전에 타겟 세션(Planner, Target Agent, Reviewer)들이 모두 tmux 세션으로 기동되어 (`status.sh` 기준 `alive` 및 `running`) 있어야 합니다. +2. **동시 루프 기동 금지**: 동일한 작업 트리 내에서 다수의 `run_loop.sh` 제어기를 동시에 기동하면 SQLite DB 갱신 경합 및 YAML 데이터 오염이 발생하므로 절대로 병렬 기동하지 마십시오. +3. **정형 토큰 단독 행 작성 필수**: 앵커링 파서 하드닝에 의해, 리뷰 리포트 파일 내에서 `[VERDICT: PASS]` 또는 `[VERDICT: NOT PASS]` 토큰은 반드시 리포트의 **마지막에 단독 행으로** 기재되어야 합니다. 코드 인용이나 변경 diff 내의 토큰은 매칭 대상에서 완전 배제됩니다. +4. **결함 시 fail-closed**: 최종 Verdict 토큰이 누락되거나 리포트 픽업 실패 시, 파서는 안전을 위해 `NOT PASS`로 오픽업(Fail-closed) 판정하여 교정 사이클을 수행하므로 반드시 리포트 끝에 단독 행으로 토큰을 찍도록 지시해야 합니다. +5. **원자적 아카이빙 (Promotion)**: 루프 성공 종료 시 최종 계획서와 검증 리포트들은 `.agents/reports//` 디렉토리로 원자적으로 덮어쓰기(`mv -f`)되어 보존되므로, 해당 경로의 리포트들로 VCS 추적성을 확보해야 합니다. diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 8bda604..eb75658 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -1168,6 +1168,10 @@ send_keys_safe() { _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 + if [[ "$sess" =~ "agy" ]]; then + _sks_tmux send-keys -t "$sess" C-m + return 0 + fi sleep 0.5 local pane_content was_popup=0 pane_content=$(_pane_capture "$sess") diff --git a/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh b/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh index 9c8cd6b..da15842 100644 --- a/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh +++ b/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh @@ -8,6 +8,7 @@ set -euo pipefail # 1. Load Common Framework Library SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +# shellcheck disable=SC1091 source "$REPO_ROOT/.agents/skills/lib.sh" # Default configuration parameters @@ -83,6 +84,12 @@ log_error() { echo -e "\033[1;31m[✗]\033[0m $1" } +# Verdict must occupy its own line — quoted/diff-embedded tokens ('+', '>' prefixed) never match. +has_verdict() { + local file="$1" verdict="$2" + grep -qE "^\[VERDICT: ${verdict}\][[:space:]]*\r?$" "$file" +} + # Helper: Blocking wait for a delegate job's completion or error state (with safety timeout) wait_for_job() { local job_id="$1" @@ -161,29 +168,29 @@ import sqlite3, os, yaml, json yaml_path = '.mam/agent-sessions.yaml' db_path = '.mam/agent-sessions.db' name = os.environ.get('NAME') -agent = 'claude' +agent = None if os.path.exists(db_path): try: conn = sqlite3.connect(db_path) row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone() if row: - agent = json.loads(row[0]).get('agent', 'claude') + agent = json.loads(row[0]).get('agent') conn.close() - print(agent) - raise SystemExit(0) except Exception: pass -if os.path.exists(yaml_path): +if not agent and os.path.exists(yaml_path): try: with open(yaml_path) as f: d = yaml.safe_load(f) or {} for s in d.get('tmux_sessions', []): if s.get('name') == name: - print(s.get('agent', 'claude')) - raise SystemExit(0) + agent = s.get('agent') or s.get('pane', {}).get('cmd') + break except Exception: pass -print('claude') +if not agent: + agent = 'agy' if 'agy' in name else 'cline' if 'cline' in name else 'hermes' if 'hermes' in name else 'claude' +print(agent) " } @@ -248,7 +255,7 @@ if [ "$PLAN_MODE" = true ]; then log_info "Requesting initial implementation plan from Planner..." PLAN_JOB_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \ --agent-session "tmux:$PLANNER_SESSION" \ - --agent "claude" \ + --agent "$(resolve_agent_type "$PLANNER_SESSION")" \ --type "direct" \ --prompt "태스크 목표를 바탕으로 구체적인 구현 계획서를 작성해주세요. 목표: $TASK") @@ -266,7 +273,7 @@ if [ "$PLAN_MODE" = true ]; then fi # Retrieve plan text safely - PLAN_FILE=$(find ".mam/jobs/$PLAN_JOB_ID" -name "*.md" 2>/dev/null | head -n 1 || true) + PLAN_FILE=$(find ".mam/jobs/$PLAN_JOB_ID" -maxdepth 2 -name "report-final.md" 2>/dev/null | head -n 1 || true) if [ -z "$PLAN_FILE" ] || [ ! -f "$PLAN_FILE" ]; then log_error "Final plan file not found." exit 1 @@ -298,7 +305,7 @@ if [ "$PLAN_MODE" = true ]; then exit 1 fi - CRITIQUE_FILE=$(find ".mam/jobs/$DEBATE_JOB_ID" -name "*.md" 2>/dev/null | head -n 1 || true) + CRITIQUE_FILE=$(find ".mam/jobs/$DEBATE_JOB_ID" -maxdepth 2 -name "report-final.md" 2>/dev/null | head -n 1 || true) if [ -z "$CRITIQUE_FILE" ] || [ ! -f "$CRITIQUE_FILE" ]; then log_error "Critique file not found." exit 1 @@ -308,7 +315,7 @@ if [ "$PLAN_MODE" = true ]; then log_info "Planner refining plan with Creator's feedback..." REFINE_JOB_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \ --agent-session "tmux:$PLANNER_SESSION" \ - --agent "claude" \ + --agent "$(resolve_agent_type "$PLANNER_SESSION")" \ --type "direct" \ --prompt "작업자(Creator)로부터 다음 이의제기 피드백을 받았습니다. 피드백을 반영하여 계획서를 정교하게 업데이트(Refine)하여 다시 출력해주세요. 피드백:\n$CRITIQUE_TEXT\n기존 계획서:\n$CURRENT_PLAN") @@ -325,7 +332,7 @@ if [ "$PLAN_MODE" = true ]; then exit 1 fi - REFINE_FILE=$(find ".mam/jobs/$REFINE_JOB_ID" -name "*.md" 2>/dev/null | head -n 1 || true) + REFINE_FILE=$(find ".mam/jobs/$REFINE_JOB_ID" -maxdepth 2 -name "report-final.md" 2>/dev/null | head -n 1 || true) if [ -z "$REFINE_FILE" ] || [ ! -f "$REFINE_FILE" ]; then log_error "Refinement plan file not found." exit 1 @@ -395,7 +402,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do --agent-session "tmux:$TARGET_AGENT" \ --agent "$(resolve_agent_type "$TARGET_AGENT")" \ --type "direct" \ - --prompt "작업 완료 상태에 대해 스스로 검증(Self-Review)하여 결함이 없음을 확인하고 종결해주세요. 리포트에 반드시 '[VERDICT: PASS]' 혹은 '[VERDICT: NOT PASS]' 태그를 명시해주세요.") + --prompt "작업 완료 상태에 대해 스스로 검증(Self-Review)하여 결함이 없음을 확인하고 종결해주세요. 리뷰 리포트 마지막에 단독 행으로 반드시 '[VERDICT: PASS]' 혹은 '[VERDICT: NOT PASS]' 태그를 명시해주세요.") SELF_REV_ID=$(extract_job_id "$SELF_REV_OUTPUT") if [ -z "$SELF_REV_ID" ]; then @@ -405,8 +412,8 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do CREATED_JOBS+=("$SELF_REV_ID") wait_for_job "$SELF_REV_ID" - REPORT_FILE=$(find ".mam/jobs/$SELF_REV_ID" -name "*.md" 2>/dev/null | head -n 1 || true) - if [ -n "$REPORT_FILE" ] && [ -f "$REPORT_FILE" ] && grep -q "\[VERDICT: PASS\]" "$REPORT_FILE" && ! grep -q "\[VERDICT: NOT PASS\]" "$REPORT_FILE"; then + REPORT_FILE=$(find ".mam/jobs/$SELF_REV_ID" -maxdepth 2 -name "report-final.md" 2>/dev/null | head -n 1 || true) + if [ -n "$REPORT_FILE" ] && [ -f "$REPORT_FILE" ] && has_verdict "$REPORT_FILE" "PASS" && ! has_verdict "$REPORT_FILE" "NOT PASS"; then log_success "Self-Review PASS." break else @@ -432,7 +439,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do --agent-session "tmux:$rev" \ --agent "$(resolve_agent_type "$rev")" \ --type "direct" \ - --prompt "다음 구현 사항(작업 목표: $TASK) 및 누적 변경분(git diff)에 대해 린트, 동작성, 유실 등의 관점에서 교차 코드 리뷰를 수행해주세요. 확인 후 최종 Verdict로 '[VERDICT: PASS]' 혹은 '[VERDICT: NOT PASS]' 태그를 리뷰 리포트에 명시적으로 작성해주세요. 변경분:\n$CHANGES_DIFF") + --prompt "다음 구현 사항(작업 목표: $TASK) 및 누적 변경분(git diff)에 대해 린트, 동작성, 유실 등의 관점에서 교차 코드 리뷰를 수행해주세요. 확인 후 최종 Verdict로 '[VERDICT: PASS]' 혹은 '[VERDICT: NOT PASS]' 태그를 리뷰 리포트 마지막에 단독 행으로 명시적으로 작성해주세요. 변경분:\n$CHANGES_DIFF") REV_JOB_ID=$(extract_job_id "$REV_OUTPUT") if [ -z "$REV_JOB_ID" ]; then @@ -459,7 +466,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do fi # Parse verdict from report file (fails-safe, M-2 anchored checks) - REPORT_FILE=$(find ".mam/jobs/$job_id" -name "*.md" 2>/dev/null | head -n 1 || true) + REPORT_FILE=$(find ".mam/jobs/$job_id" -maxdepth 2 -name "report-final.md" 2>/dev/null | head -n 1 || true) if [ -z "$REPORT_FILE" ] || [ ! -f "$REPORT_FILE" ]; then log_warn "Reviewer '$rev' report not found. Counting as NOT PASS." all_passed=false @@ -469,7 +476,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do REPORT_CONTENT=$(cat "$REPORT_FILE" 2>/dev/null || echo "") # Precedence rules: NOT PASS wins over PASS. Absence of verdict tags is treated as NOT PASS (fail-closed) - if grep -q "\[VERDICT: NOT PASS\]" "$REPORT_FILE" || ! grep -q "\[VERDICT: PASS\]" "$REPORT_FILE"; then + if has_verdict "$REPORT_FILE" "NOT PASS" || ! has_verdict "$REPORT_FILE" "PASS"; then log_warn "Reviewer '$rev': NOT PASS" all_passed=false FEEDBACK_AGGREGATE="$FEEDBACK_AGGREGATE\n--- Reviewer ($rev) Feedback ---\n$REPORT_CONTENT" @@ -498,7 +505,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do REFINE_PLAN_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \ --agent-session "tmux:$PLANNER_SESSION" \ - --agent "claude" \ + --agent "$(resolve_agent_type "$PLANNER_SESSION")" \ --type "direct" \ --prompt "리뷰어들로부터 다음과 같이 정교한 코드 수정 피드백이 도착했습니다. 해당 피드백을 수렴하여 구현 계획서(Plan)를 갱신(Refine)하여 다시 작성해주세요. 피드백:\n$FEEDBACK_AGGREGATE\n기존 계획서:\n$CURRENT_PLAN") @@ -510,7 +517,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do CREATED_JOBS+=("$REFINE_PLAN_ID") wait_for_job "$REFINE_PLAN_ID" - REFINE_PLAN_FILE=$(find ".mam/jobs/$REFINE_PLAN_ID" -name "*.md" 2>/dev/null | head -n 1 || true) + REFINE_PLAN_FILE=$(find ".mam/jobs/$REFINE_PLAN_ID" -maxdepth 2 -name "report-final.md" 2>/dev/null | head -n 1 || true) if [ -z "$REFINE_PLAN_FILE" ] || [ ! -f "$REFINE_PLAN_FILE" ]; then log_error "Refined plan file not found." exit 1 @@ -543,6 +550,30 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do loop_count=$((loop_count + 1)) done +# Promote finalized plan and passed reviewer reports to durable location +if [ "$PLAN_MODE" = true ] && [ -n "${CURRENT_PLAN:-}" ] && [ -n "${PLAN_JOB_ID:-}" ]; then + plan_dest_dir=".agents/reports/$PLANNER_SESSION" + log_info "Promoting final plan to durable location: $plan_dest_dir" + mkdir -p "$plan_dest_dir" + echo "$CURRENT_PLAN" > "$plan_dest_dir/plan-${PLAN_JOB_ID}.md.tmp" + mv -f "$plan_dest_dir/plan-${PLAN_JOB_ID}.md.tmp" "$plan_dest_dir/plan-${PLAN_JOB_ID}.md" +fi + +if [ "${#REVIEWERS[@]}" -gt 0 ]; then + log_info "Promoting final review reports to durable location..." + for idx in "${!JOB_IDS[@]}"; do + job_id="${JOB_IDS[$idx]}" + rev="${JOB_REVS[$idx]}" + report_file=$(find ".mam/jobs/$job_id" -maxdepth 2 -name "report-final.md" 2>/dev/null | head -n 1 || true) + if [ -n "$report_file" ] && [ -f "$report_file" ]; then + dest_dir=".agents/reports/$rev" + mkdir -p "$dest_dir" + cp "$report_file" "$dest_dir/report-${job_id}.md.tmp" + mv -f "$dest_dir/report-${job_id}.md.tmp" "$dest_dir/report-${job_id}.md" + fi + done +fi + # =========================================================================== # PHASE 4: CLEANUP # ===========================================================================