#!/usr/bin/env bash # =========================================================================== # run_loop.sh — Autonomous Planning, Execution, and Peer-Review Orchestrator # =========================================================================== 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" # shellcheck disable=SC1091 source "$REPO_ROOT/.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh" # shellcheck disable=SC1091 source "$REPO_ROOT/.agents/skills/multi-agent-mux-loop/scripts/loop_lock.sh" # Default configuration parameters PLAN_MODE=false PLAN_TALK_TURNS=1 ALL_REVIEWERS=false MAX_LOOP=3 MAX_REBUT=1 VERBOSE=false CLEANUP=false TARGET_AGENT="" TASK="" REVIEWER_LIST="" # Print usage instructions usage() { echo "Usage: $0 [options] --target-agent --task " echo "Options:" echo " --plan Enable Planner agent intervention & design phase" echo " --plan-talk N Planner-Creator discussion limit turns (default: 1)" echo " --reviewer \"A,B\" Targeted reviewer session name list (comma-separated)" echo " --all-reviewer Enforce PASS verdict from all active reviewer sessions" echo " --max-loop N Max execution-review corrective loop runs (default: 3)" echo " --max-rebut N Max rebuttal attempts per review iteration (default: 1, 0: disabled)" echo " --verbose Print detailed execution timeline traces" echo " --cleanup Purge temporary job directories upon success" exit 1 } # Parse options safely while [[ "$#" -gt 0 ]]; do case "$1" in --plan) PLAN_MODE=true; shift ;; --plan-talk) if [[ ! "$2" =~ ^[0-9]+$ ]]; then echo "ERROR: --plan-talk requires a positive integer." exit 1 fi PLAN_TALK_TURNS="$2"; shift 2 ;; --reviewer) REVIEWER_LIST="$2"; shift 2 ;; --all-reviewer) ALL_REVIEWERS=true; shift ;; --max-loop) if [[ ! "$2" =~ ^[0-9]+$ ]] || [ "$2" -le 0 ]; then echo "ERROR: --max-loop requires a positive non-zero integer." exit 1 fi MAX_LOOP="$2"; shift 2 ;; --max-rebut) if [[ ! "$2" =~ ^[0-9]+$ ]]; then echo "ERROR: --max-rebut requires a non-negative integer." exit 1 fi MAX_REBUT="$2"; shift 2 ;; --verbose) VERBOSE=true; shift ;; --cleanup) CLEANUP=true; shift ;; --target-agent) TARGET_AGENT="$2"; shift 2 ;; --task) TASK="$2"; shift 2 ;; -h|--help) usage ;; *) echo "Unknown option: $1"; usage ;; esac done # Run-wide cap on rebuttal rounds. Makes the bound explicit and enforced # rather than merely emergent from MAX_LOOP x MAX_REBUT. REBUT_TOTAL_BUDGET=$((MAX_REBUT * MAX_LOOP)) if [ -z "$TARGET_AGENT" ] || [ -z "$TASK" ]; then echo "ERROR: --target-agent and --task are mandatory fields." usage fi MAM_LOOP_MARKER="${MAM_LOOP_MARKER:-$REPO_ROOT/.mam/loop-guard-active}" _mam_release_guard() { mam_release_loop_lock "$MAM_LOOP_MARKER" || true; } delegate_job_safe() { local orig_script="$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job" local tmp_script tmp_script="${orig_script}.${RANDOM}_$$.tmp" cp "$orig_script" "$tmp_script" trap 'rm -f "$tmp_script"' EXIT INT TERM HUP local rc=0 bash "$tmp_script" "$@" || rc=$? rm -f "$tmp_script" trap _mam_release_guard EXIT INT TERM HUP return $rc } log_info() { echo -e "\033[1;34m[*]\033[0m $1" } log_success() { echo -e "\033[1;32m[✓]\033[0m $1" } log_warn() { echo -e "\033[1;33m[!]\033[0m $1" } log_error() { echo -e "\033[1;31m[✗]\033[0m $1" } _mam_acquire_rc=0 mam_acquire_loop_lock "$MAM_LOOP_MARKER" || _mam_acquire_rc=$? case "$_mam_acquire_rc" in 0) ;; 3) _holder_pid=$(_mam_lock_field "$MAM_LOOP_MARKER" "pid" 2>/dev/null || echo "unknown") log_error "Another multi-agent-mux-loop is already running in this workspace." log_error " holder PID: $_holder_pid" log_error " refusing to start a second loop (SQLite/YAML would race)." exit 1 ;; *) log_error "Cannot determine whether another loop is running (ps unavailable?)." log_error " refusing to start rather than risk two loops writing the registry." exit 1 ;; esac trap _mam_release_guard EXIT INT TERM HUP # --all-reviewer silently takes precedence over an explicit --reviewer list; # warn so the discarded list isn't mistaken for having been honored (P2-1). 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() { local file="$1" verdict="$2" local last_line pattern last_line=$(grep -v '^[[:space:]]*$' "$file" 2>/dev/null | tail -n 1) pattern="^\[VERDICT: ${verdict}\][[:space:]]*\r?\$" [[ "$last_line" =~ $pattern ]] } # Detect a standalone '[TAG]' line anywhere in a report FILE. # # Always read the file, never a concatenated aggregate: FEEDBACK_AGGREGATE # joins reports with a literal backslash-n (bash `echo` without -e leaves it # as text), so a tag on a report's FIRST line ends up sharing a physical line # with the '--- Reviewer (x) Feedback ---' banner and the '^' anchor misses it. has_tag_line() { local file="$1" tag="$2" [ -f "$file" ] || return 1 grep -qE "^\[${tag}\][[:space:]]*\r?\$" "$file" } # Emit the payload of every standalone '[KEY: value]' line in a report file. extract_tag_values() { local file="$1" key="$2" [ -f "$file" ] || return 0 sed -n -E "s/^\[${key}:[[:space:]]*([^]]*)\][[:space:]]*\r?$/\1/p" "$file" \ | sed -E 's/[[:space:]]+$//' | grep -v '^$' || true } # Locate a job's final report (transient .mam job tree, agent-name subdir). find_report() { find ".mam/jobs/$1" -maxdepth 2 -name "report-final.md" 2>/dev/null | head -n 1 || true } # Helper: Blocking wait for a delegate job's completion or error state (with safety timeout) wait_for_job() { local job_id="$1" local check_interval=3 local max_wait="${2:-3900}" local deadline deadline=$((SECONDS + max_wait)) if [ "$VERBOSE" = true ]; then log_info "Monitoring job '$job_id' for status changes (timeout: ${max_wait}s)..." fi while [ "$SECONDS" -lt "$deadline" ]; do local status status=$(python3 -c " import json, os try: with open('.mam/jobs/$job_id.json') as f: print(json.load(f).get('status', 'unknown')) except Exception: print('unknown') " 2>/dev/null || echo "unknown") if [ "$status" = "completed" ]; then if [ "$VERBOSE" = true ]; then log_success "Job '$job_id' completed successfully." fi return 0 elif [ "$status" = "error" ]; then log_error "Job '$job_id' finished with errors." return 1 elif [ "$status" = "cancelled" ]; then log_error "Job '$job_id' was cancelled." return 1 fi sleep "$check_interval" done log_error "Job '$job_id' timed out after ${max_wait}s." return 1 } # Resolve active reviewers (excluding $TARGET_AGENT) via lib.sh's # load_state_json — the single source of truth for the merged DB/YAML # session state, instead of hand-rolling a 4th copy of that lookup (P1-2). resolve_all_reviewers() { TARGET_AGENT="$TARGET_AGENT" MAM_STATE_JSON="$(load_state_json)" python3 -c " 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') or '').lower() and s.get('status') == 'running' and s.get('name') != target_agent] print(','.join(reviewers)) " } # Resolve target agent type (claude, cline, agy) via load_state_json. resolve_agent_type() { local name="$1" NAME="$name" MAM_STATE_JSON="$(load_state_json)" python3 -c " import os, json name = os.environ.get('NAME') d = json.loads(os.environ.get('MAM_STATE_JSON', '{}')) agent = None for s in d.get('herdr_sessions', []): if s.get('name') == name: agent = s.get('agent') or s.get('pane', {}).get('cmd') break if not agent: # Exact hyphen-segment match, not a naive substring 'in' check, so a # decoy substring inside an unrelated segment can't misclassify. segments = name.split('-') if 'agy' in segments: agent = 'agy' elif 'cline' in segments: agent = 'cline' elif 'hermes' in segments: agent = 'hermes' else: agent = 'claude' print(agent) " } # Resolve planner session dynamically via load_state_json. resolve_planner_session() { MAM_STATE_JSON="$(load_state_json)" python3 -c " 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') or '').lower() and s.get('status') == 'running': planner = s.get('name') break print(planner) " } # Portable Job ID extraction helper (fails-safe, avoids SC1091/grep GNU dependency) extract_job_id() { local output="$1" local job_id # Portable extraction equivalent to PCRE K job_id=$(echo "$output" | grep -o 'registered job: [A-Za-z0-9]*' | awk '{print $3}' || true) echo "$job_id" } # Main Execution Loop Flow 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=() # =========================================================================== # PHASE 1: PLANNING & DISCUSSIONS # =========================================================================== if [ "$PLAN_MODE" = true ]; then log_info "=== Phase 1: Interactive Planning Phase ===" # Step 1.1: Request initial plan from Planner log_info "Requesting initial implementation plan from Planner..." PLAN_JOB_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$PLANNER_SESSION" \ --agent "$(resolve_agent_type "$PLANNER_SESSION")" \ --type "direct" \ --role "Planner" \ --prompt "태스크 목표를 바탕으로 구체적인 구현 계획서를 작성해주세요. 목표: $TASK") PLAN_JOB_ID=$(extract_job_id "$PLAN_JOB_OUTPUT") if [ -z "$PLAN_JOB_ID" ]; then log_error "Failed to register planner job. Output:\n$PLAN_JOB_OUTPUT" exit 1 fi CREATED_JOBS+=("$PLAN_JOB_ID") log_info "Planner Job ID: $PLAN_JOB_ID" if ! wait_for_job "$PLAN_JOB_ID"; then log_error "Planning phase failed during initial plan design." exit 1 fi # Retrieve plan text safely 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 fi CURRENT_PLAN=$(cat "$PLAN_FILE") # Step 1.2: Interactive Creator-Planner Debate turn=1 while [ "$turn" -le "$PLAN_TALK_TURNS" ]; do log_info "Discussion Turn $turn/$PLAN_TALK_TURNS: Creator challenging the plan..." # Creator critique job DEBATE_JOB_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$TARGET_AGENT" \ --agent "$(resolve_agent_type "$TARGET_AGENT")" \ --type "direct" \ --role "Worker" \ --prompt "Planner가 제시한 다음 계획서를 꼼꼼히 검토하고, 실제 구현 시 마주할 수 있는 맹점이나 제약사항 1가지를 발굴하여 Planner에게 이의를 제기(Challenge)해주세요. 계획서:\n$CURRENT_PLAN") DEBATE_JOB_ID=$(extract_job_id "$DEBATE_JOB_OUTPUT") if [ -z "$DEBATE_JOB_ID" ]; then log_error "Failed to register Creator critique job. Output:\n$DEBATE_JOB_OUTPUT" exit 1 fi CREATED_JOBS+=("$DEBATE_JOB_ID") log_info "Critique Job ID: $DEBATE_JOB_ID" if ! wait_for_job "$DEBATE_JOB_ID"; then log_error "Creator critique step failed." exit 1 fi 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 fi CRITIQUE_TEXT=$(cat "$CRITIQUE_FILE") log_info "Planner refining plan with Creator's feedback..." REFINE_JOB_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$PLANNER_SESSION" \ --agent "$(resolve_agent_type "$PLANNER_SESSION")" \ --type "direct" \ --role "Planner" \ --prompt "작업자(Creator)로부터 다음 이의제기 피드백을 받았습니다. 피드백을 반영하여 계획서를 정교하게 업데이트(Refine)하여 다시 출력해주세요. 피드백:\n$CRITIQUE_TEXT\n기존 계획서:\n$CURRENT_PLAN") REFINE_JOB_ID=$(extract_job_id "$REFINE_JOB_OUTPUT") if [ -z "$REFINE_JOB_ID" ]; then log_error "Failed to register plan refinement job. Output:\n$REFINE_JOB_OUTPUT" exit 1 fi CREATED_JOBS+=("$REFINE_JOB_ID") log_info "Refinement Job ID: $REFINE_JOB_ID" if ! wait_for_job "$REFINE_JOB_ID"; then log_error "Planner refinement step failed." exit 1 fi 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 fi CURRENT_PLAN=$(cat "$REFINE_FILE") turn=$((turn + 1)) done log_success "Interactive planning completed. Plan finalized." else log_info "=== Phase 1: Self-Planning Mode (Direct Execution) ===" EXISTING_PLAN_FILE="" if [ -n "$PLANNER_SESSION" ]; then # Promoted plans are saved as plan-.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..." CURRENT_PLAN=$(cat "$EXISTING_PLAN_FILE") fi fi # =========================================================================== # PHASE 2: IMPLEMENTATION (CREATION) # =========================================================================== log_info "=== Phase 2: Code Implementation ===" # Fix the pre-implementation commit as the diff baseline so review diffs stay # cumulative and non-empty even after the Creator commits per DoD (P0-1). BASE_COMMIT=$(cd -P "$REPO_ROOT" 2>/dev/null && git rev-parse HEAD 2>/dev/null || echo "") EXECUTION_PROMPT="계획서가 존재하지 않으므로, 작업자(Creator)의 판단하에 스스로 구현 계획 및 설계를 수립한 뒤, 이를 바탕으로 코드를 구현하고 다음 작업 목표를 완성해주세요. 작업 목표: $TASK" if [ -n "$CURRENT_PLAN" ]; then EXECUTION_PROMPT="이미 수립된 다음 계획서에 입각하여 작업자의 판단하에 코드를 구현하고 작업 목표를 완성해주세요. 계획서:\n$CURRENT_PLAN\n\n작업 목표: $TASK" fi EXEC_JOB_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$TARGET_AGENT" \ --agent "$(resolve_agent_type "$TARGET_AGENT")" \ --type "direct" \ --role "Worker" \ --prompt "$EXECUTION_PROMPT") EXEC_JOB_ID=$(extract_job_id "$EXEC_JOB_OUTPUT") if [ -z "$EXEC_JOB_ID" ]; then log_error "Failed to register Creator execution job. Output:\n$EXEC_JOB_OUTPUT" exit 1 fi CREATED_JOBS+=("$EXEC_JOB_ID") log_info "Creator Job ID: $EXEC_JOB_ID" if ! wait_for_job "$EXEC_JOB_ID"; then log_error "Creator execution failed." exit 1 fi log_success "Initial implementation finished." # =========================================================================== # PHASE 3: VERIFICATION LOOP (PEER REVIEW) # =========================================================================== log_info "=== Phase 3: Verification & Corrective Review Loop ===" # Resolve reviewer array REVIEWERS=() if [ "$ALL_REVIEWERS" = true ]; then # Parse list safely using command substitution + fallback RESOLVED_REVS=$(resolve_all_reviewers) 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 # 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 while [ "$loop_count" -le "$MAX_LOOP" ]; do log_info "Review Loop Iteration $loop_count/$MAX_LOOP..." # O-1: the per-iteration budget resets each pass, because each pass produces # NEW findings that may be legitimately rebuttable. The run-wide total is # capped separately by REBUT_TOTAL_BUDGET so the reset cannot be mistaken for # an unbounded channel (see MULTI_AGENT_RULES.md §3.1 rule 3). REBUT_BUDGET="$MAX_REBUT" if [ "${#REVIEWERS[@]}" -eq 0 ]; then log_warn "No reviewers specified. Conducting Creator Self-Review..." SELF_REV_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$TARGET_AGENT" \ --agent "$(resolve_agent_type "$TARGET_AGENT")" \ --type "direct" \ --role "Reviewer" \ --prompt "작업 완료 상태에 대해 스스로 검증(Self-Review)하여 결함이 없음을 확인하고 종결해주세요. 리뷰 리포트 마지막에 단독 행으로 반드시 '[VERDICT: PASS]' 혹은 '[VERDICT: NOT PASS]' 태그를 명시해주세요.") SELF_REV_ID=$(extract_job_id "$SELF_REV_OUTPUT") if [ -z "$SELF_REV_ID" ]; then log_error "Failed to register Self-Review job." exit 1 fi CREATED_JOBS+=("$SELF_REV_ID") wait_for_job "$SELF_REV_ID" 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 log_warn "Self-Review NOT PASS." if [ "$loop_count" -eq "$MAX_LOOP" ]; then log_error "Reached max loop count. Self-review loop aborted with failures." exit 1 fi fi else log_info "Active reviewers: ${REVIEWERS[*]}" CHANGES_DIFF=$(mam_collect_changes_diff "$REPO_ROOT" "$BASE_COMMIT") || { log_error "Could not determine the change set; refusing to request a review on no evidence." log_error "$CHANGES_DIFF" exit 1 } # We use space-separated lists or simple loops to bypass bash-4 associative array requirement (M-7 macOS compatibility) declare -a JOB_IDS=() declare -a JOB_REVS=() # Objecting reviewers and their report paths, kept in parallel arrays so the # rebuttal round can address each objection individually (O-1). declare -a FAIL_REVS=() declare -a FAIL_REPORTS=() for rev in "${REVIEWERS[@]}"; do log_info "Requesting code review from Reviewer '$rev'..." REV_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$rev" \ --agent "$(resolve_agent_type "$rev")" \ --type "direct" \ --role "Reviewer" \ --prompt "다음 구현 사항(작업 목표: $TASK) 및 누적 변경분(git diff)에 대해 린트, 동작성, 유실 등의 관점에서 교차 코드 리뷰를 수행해주세요. 확인 후 최종 Verdict로 '[VERDICT: PASS]' 혹은 '[VERDICT: NOT PASS]' 태그를 리뷰 리포트 마지막에 단독 행으로 명시적으로 작성해주세요. 만약 단순 버그 수정으로는 부족하고 설계 변경/재작업 수준의 재계획이 필요하다고 판단되면, 리포트 아무 곳에나 단독 행으로 '[ESCALATE: PLANNER]' 태그도 함께 남겨주세요. 변경분:\n$CHANGES_DIFF") REV_JOB_ID=$(extract_job_id "$REV_OUTPUT") if [ -z "$REV_JOB_ID" ]; then log_error "Failed to register review job for '$rev'." exit 1 fi CREATED_JOBS+=("$REV_JOB_ID") JOB_IDS+=("$REV_JOB_ID") JOB_REVS+=("$rev") done # Wait for all reviews all_passed=true FEEDBACK_AGGREGATE="" for idx in "${!JOB_IDS[@]}"; do job_id="${JOB_IDS[$idx]}" rev="${JOB_REVS[$idx]}" if ! wait_for_job "$job_id"; then log_warn "Reviewer '$rev' job crashed." all_passed=false continue fi # Parse verdict from report file (fails-safe, M-2 anchored checks) 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 continue fi 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 has_verdict "$REPORT_FILE" "NOT PASS" || ! has_verdict "$REPORT_FILE" "PASS"; then log_warn "Reviewer '$rev': NOT PASS" all_passed=false FAIL_REVS+=("$rev") FAIL_REPORTS+=("$REPORT_FILE") FEEDBACK_AGGREGATE="$FEEDBACK_AGGREGATE\n--- Reviewer ($rev) Feedback ---\n$REPORT_CONTENT" else log_success "Reviewer '$rev': PASS" fi done if [ "$all_passed" = true ]; then log_success "All reviewers issued [VERDICT: PASS]. Loop completed successfully." break else if [ "$loop_count" -eq "$MAX_LOOP" ]; then log_error "Reached max loop count ($MAX_LOOP). Review loop aborted with failures." exit 1 fi # Scan the report FILES, not FEEDBACK_AGGREGATE: the aggregate's literal # '\n' separators break '^' anchoring for a tag on a report's first line. # bash 3.2 (macOS stock) aborts on "${arr[@]}" when arr is empty under # `set -u`. FAIL_REPORTS IS empty on the crashed-job / missing-report # paths above, which reach here via all_passed=false without appending. COMPLEX_FIX=false for _fr in ${FAIL_REPORTS[@]+"${FAIL_REPORTS[@]}"}; do if has_tag_line "$_fr" "ESCALATE: PLANNER"; then COMPLEX_FIX=true break fi done if [ "$PLAN_MODE" = true ] && [ "$COMPLEX_FIX" = true ]; then log_warn "Feedback involves complex code modifications. Diverting to Planner to revise plan..." REFINE_PLAN_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$PLANNER_SESSION" \ --agent "$(resolve_agent_type "$PLANNER_SESSION")" \ --type "direct" \ --role "Planner" \ --prompt "리뷰어들로부터 다음과 같이 정교한 코드 수정 피드백이 도착했습니다. 해당 피드백을 수렴하여 구현 계획서(Plan)를 갱신(Refine)하여 다시 작성해주세요. 피드백:\n$FEEDBACK_AGGREGATE\n기존 계획서:\n$CURRENT_PLAN") REFINE_PLAN_ID=$(extract_job_id "$REFINE_PLAN_OUTPUT") if [ -z "$REFINE_PLAN_ID" ]; then log_error "Failed to register plan refinement job." exit 1 fi CREATED_JOBS+=("$REFINE_PLAN_ID") wait_for_job "$REFINE_PLAN_ID" 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 fi CURRENT_PLAN=$(cat "$REFINE_PLAN_FILE") CORRECTION_PROMPT="갱신된 다음 계획서에 입각하여 지적된 오류들을 수정하고 코드를 다시 구현해주세요. 계획서:\n$CURRENT_PLAN\n피드백 상세:\n$FEEDBACK_AGGREGATE" else log_info "Applying straight bugfixes based on reviewer feedback..." CORRECTION_PROMPT="리뷰어들이 지적한 다음 피드백에 입각하여 코드를 수정해주세요. 피드백:\n$FEEDBACK_AGGREGATE" fi if [ "$MAX_REBUT" -gt 0 ]; then CORRECTION_PROMPT="$CORRECTION_PROMPT\n\n[이의제기 채널] 위 피드백 중 타당하지 않다고 판단되는 항목이 있다면, 그 항목은 구현하지 말고 반론을 제기하십시오. 반론 시 리포트에 단독 행으로 '[REBUT: <리뷰어_세션명>]' 태그를 남기고, 해당 리뷰어의 지적 중 어느 항목을 왜 거부하는지 근거를 함께 서술하십시오. 타당한 지적은 정상적으로 반영하십시오. 반론할 항목이 없으면 태그를 남기지 마십시오. 대상 리뷰어 세션명: ${FAIL_REVS[*]-(없음)}" fi # Creator execution corrective job CORRECT_JOB_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$TARGET_AGENT" \ --agent "$(resolve_agent_type "$TARGET_AGENT")" \ --type "direct" \ --role "Worker" \ --prompt "$CORRECTION_PROMPT") CORRECT_JOB_ID=$(extract_job_id "$CORRECT_JOB_OUTPUT") if [ -z "$CORRECT_JOB_ID" ]; then log_error "Failed to register Creator correction job." exit 1 fi CREATED_JOBS+=("$CORRECT_JOB_ID") wait_for_job "$CORRECT_JOB_ID" # --------------------------------------------------------------------- # O-1: REBUTTAL & ADJUDICATION ROUND # --------------------------------------------------------------------- # The Creator's correction report was previously produced and discarded. # Read it: standalone '[REBUT: ]' lines open an adjudication # round with exactly those reviewers. # # Invariant: a rebuttal NEVER substitutes for a PASS. A sustained # rebuttal only withdraws an objection; the next iteration still runs a # full review pass, so the loop can still only exit 0 on unanimous # [VERDICT: PASS]. if [ "$MAX_REBUT" -gt 0 ] && [ "$REBUT_BUDGET" -gt 0 ] && [ "$REBUT_TOTAL_BUDGET" -gt 0 ]; then CORRECT_REPORT=$(find_report "$CORRECT_JOB_ID") REBUT_TARGETS=() if [ -n "$CORRECT_REPORT" ]; then while IFS= read -r _t; do [ -n "$_t" ] || continue # Only honour rebuttals aimed at a reviewer that actually objected, # and de-duplicate: a Creator may file one [REBUT:] per rejected # finding, all naming the same reviewer — that is ONE adjudication. for _fv in ${FAIL_REVS[@]+"${FAIL_REVS[@]}"}; do [ "$_t" = "$_fv" ] || continue _dup=0 for _e in ${REBUT_TARGETS[@]+"${REBUT_TARGETS[@]}"}; do if [ "$_e" = "$_t" ]; then _dup=1; break; fi done [ "$_dup" -eq 0 ] && REBUT_TARGETS+=("$_t") break done done <<< "$(extract_tag_values "$CORRECT_REPORT" "REBUT")" fi if [ "${#REBUT_TARGETS[@]}" -gt 0 ]; then REBUT_BUDGET=$((REBUT_BUDGET - 1)) REBUT_TOTAL_BUDGET=$((REBUT_TOTAL_BUDGET - 1)) REBUTTAL_TEXT=$(cat "$CORRECT_REPORT" 2>/dev/null || echo "") log_warn "Creator filed a rebuttal against: ${REBUT_TARGETS[*]} (iteration budget left: $REBUT_BUDGET, run total left: $REBUT_TOTAL_BUDGET)" OVERRULED_REVS=() for rt in "${REBUT_TARGETS[@]}"; do log_info "Re-adjudication: returning rebuttal to Reviewer '$rt'..." ADJ_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$rt" \ --agent "$(resolve_agent_type "$rt")" \ --type "direct" \ --role "Reviewer" \ --prompt "작업자(Creator)가 귀하의 리뷰 지적 중 일부를 타당하지 않다고 판단하여 반론을 제기했습니다. 반론을 검토하고 재심(re-adjudication)하십시오. 반론이 타당하여 귀하의 지적을 철회한다면 리포트 마지막에 단독 행으로 '[ADJUDICATION: SUSTAINED]' 를, 반론이 타당하지 않아 지적을 유지한다면 '[ADJUDICATION: OVERRULED]' 를 명시하십시오. 반론 내용:\n$REBUTTAL_TEXT") ADJ_JOB_ID=$(extract_job_id "$ADJ_OUTPUT") if [ -z "$ADJ_JOB_ID" ]; then log_error "Failed to register re-adjudication job for '$rt'." exit 1 fi CREATED_JOBS+=("$ADJ_JOB_ID") if ! wait_for_job "$ADJ_JOB_ID"; then log_warn "Re-adjudication job for '$rt' failed; objection stands." OVERRULED_REVS+=("$rt") continue fi ADJ_REPORT=$(find_report "$ADJ_JOB_ID") # Fail-closed: anything that is not an explicit SUSTAINED keeps the # reviewer's objection alive. if [ -n "$ADJ_REPORT" ] && has_tag_line "$ADJ_REPORT" "ADJUDICATION: SUSTAINED"; then log_success "Reviewer '$rt' SUSTAINED the rebuttal; objection withdrawn." else log_warn "Reviewer '$rt' OVERRULED the rebuttal; objection stands." OVERRULED_REVS+=("$rt") fi done # Deadlock: the Creator rebutted and the reviewer held. In --plan mode # the Planner arbitrates; otherwise the reviewer prevails (fail-closed, # charter §1: a reviewer's PASS is the completion gate). if [ "${#OVERRULED_REVS[@]}" -gt 0 ]; then ARBITRATION="REVIEWER" if [ "$PLAN_MODE" = true ] && [ -n "$PLANNER_SESSION" ]; then log_info "Deadlock on ${OVERRULED_REVS[*]}; escalating to Planner for arbitration..." ARB_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$PLANNER_SESSION" \ --agent "$(resolve_agent_type "$PLANNER_SESSION")" \ --type "direct" \ --role "Planner" \ --prompt "작업자(Creator)와 리뷰어의 의견이 충돌하여 교착 상태입니다. 계획서 관점에서 어느 쪽이 타당한지 재정(arbitration)하십시오. 리포트 마지막에 단독 행으로 '[ARBITRATION: CREATOR]' 또는 '[ARBITRATION: REVIEWER]' 를 명시하십시오. 리뷰어 지적:\n$FEEDBACK_AGGREGATE\n작업자 반론:\n$REBUTTAL_TEXT\n기존 계획서:\n$CURRENT_PLAN") ARB_JOB_ID=$(extract_job_id "$ARB_OUTPUT") if [ -z "$ARB_JOB_ID" ]; then log_error "Failed to register Planner arbitration job." exit 1 fi CREATED_JOBS+=("$ARB_JOB_ID") if wait_for_job "$ARB_JOB_ID"; then ARB_REPORT=$(find_report "$ARB_JOB_ID") if [ -n "$ARB_REPORT" ] && has_tag_line "$ARB_REPORT" "ARBITRATION: CREATOR"; then ARBITRATION="CREATOR" fi else log_warn "Planner arbitration job failed; defaulting to REVIEWER." fi fi if [ "$ARBITRATION" = "CREATOR" ]; then log_success "Planner ruled for the Creator; objections withdrawn for this round." else log_warn "Ruling stands with the reviewer(s); Creator must comply." # Close the channel for the REST OF THIS ITERATION and require # compliance. Note this does not persist into the next iteration # (which re-reviews and may raise different findings); the # run-wide cap REBUT_TOTAL_BUDGET is what bounds the whole run. REBUT_BUDGET=0 COMPLY_OUTPUT=$(delegate_job_safe submit \ --agent-session "herdr:$TARGET_AGENT" \ --agent "$(resolve_agent_type "$TARGET_AGENT")" \ --type "direct" \ --role "Worker" \ --prompt "귀하의 반론은 재심 결과 기각되었습니다. 더 이상 반론하지 말고 다음 리뷰 피드백을 그대로 반영하여 코드를 수정하십시오. 피드백:\n$FEEDBACK_AGGREGATE") COMPLY_JOB_ID=$(extract_job_id "$COMPLY_OUTPUT") if [ -z "$COMPLY_JOB_ID" ]; then log_error "Failed to register Creator compliance job." exit 1 fi CREATED_JOBS+=("$COMPLY_JOB_ID") wait_for_job "$COMPLY_JOB_ID" fi fi fi fi fi fi 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 # =========================================================================== if [ "$CLEANUP" = true ]; then log_info "Cleaning up temporary job directories created during this loop run..." for job in "${CREATED_JOBS[@]}"; do if [ -d ".mam/jobs/$job" ]; then rm -rf ".mam/jobs/$job" rm -f ".mam/jobs/$job.subscriber.out" if [ "$VERBOSE" = true ]; then log_info "Purged temp assets for job: $job" fi fi done log_success "Cleanup complete." fi log_success "Mux loop finished with 100% PASS verdicts." exit 0