diff --git a/.agents/hooks/loop_delegation_guard.sh b/.agents/hooks/loop_delegation_guard.sh index 52d9d97..5543ae0 100755 --- a/.agents/hooks/loop_delegation_guard.sh +++ b/.agents/hooks/loop_delegation_guard.sh @@ -131,6 +131,6 @@ emit("deny", "The /multi-agent-mux-loop skill is active, so direct file edits are out " "of scope for the orchestrator. Stop editing and delegate instead: run " "bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh " - "--target-agent --task . " + "--creator --task . " "See .agents/MULTI_AGENT_RULES.md #3.2 (Invocation-Aware Scoped Guard).") PY diff --git a/.agents/skills/multi-agent-mux-loop/SKILL.md b/.agents/skills/multi-agent-mux-loop/SKILL.md index b2a51b7..46c238a 100644 --- a/.agents/skills/multi-agent-mux-loop/SKILL.md +++ b/.agents/skills/multi-agent-mux-loop/SKILL.md @@ -26,7 +26,7 @@ metadata: ## What this skill does Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. It supports: -- **Collaborative Planning** (`--plan` and `--plan-talk N`): Planner designs the solution, Creator challenges the plan for N turns to resolve edge cases, then implementation starts. +- **Collaborative Planning** (`--plan`, optional `--planner `, and `--plan-talk N`): Planner designs the solution, Creator challenges the plan for N turns to resolve edge cases, then implementation starts. `--planner` selects the planner session; omit it to auto-resolve the first running planner. - **Creator Self-Planning & Development** (default without `--plan`): Planner 에이전트에게 계획 작성을 위임하지 않고, 기존에 승격된 계획서가 있다면 이를 로드하여 코드를 구현하며, 계획서가 존재하지 않는 경우 작업자(Creator: developer/writer)가 스스로 구현 계획 및 설계 수립을 포함한 개발 전 과정을 직접 진행합니다. - **Targeted Peer-Review** (`--reviewer`): Runs custom-selected reviewer agents to verify code changes. - **Total Peer-Review** (`--all-reviewer`): Enforces a unanimous PASS verdict from all registered reviewer sessions. @@ -163,11 +163,14 @@ sequenceDiagram | 워크플로우 단계 | 해당 CLI 옵션 | 설명 | | :--- | :--- | :--- | | **Phase 1: Planning** | `--plan` | Planner 에이전트를 기동하여 최초 계획 작성을 강제합니다. (옵션을 지정하지 않을 경우 새 계획서 작성을 생략하며, 기존 계획서가 있는 경우 이를 로드하고, 없는 경우 Creator가 직접 계획 및 설계를 수립하여 즉시 구현에 착수합니다.) | +| **Phase 1: Planner session** | `--planner ` | 계획 단계를 수행할 세션을 명시합니다. **`--plan`과 함께만** 사용합니다. 생략 시 running planner를 자동 탐색합니다. | | **Phase 1: Debate** | `--plan-talk N` | Planner와 Creator가 상호 대화식 챌린지 루프를 `N`회 돌며 계획을 교차 정제합니다. | -| **Phase 2: Execution** | (기본값) | `--target-agent`로 명시한 주 작업 세션에 코딩 태스크를 주입합니다. | +| **Phase 2: Execution** | `--creator ` | 코드를 구현할 Creator 세션에 코딩 태스크를 주입합니다. (필수) | | **Phase 3: Review** | `--reviewer "A,B"` | 지정된 리뷰어 세션 리스트(`A`, `B` 등)에 교차 Peer Review를 위임합니다. | | **Phase 3: Consensus** | `--all-reviewer` | 레지스트리에 등록된 모든 active 리뷰어 세션을 자동으로 수집하여 리뷰를 돌립니다. (`--reviewer` 옵션과는 상호 배타적이며, 지정/수집된 모든 리뷰어의 PASS 만장일치가 항상 필요합니다.) | | **Iterative Loop** | `--max-loop M` | NOT PASS 판정 시 최대 `M`회까지 Creator가 자체 수정합니다. `--plan` 모드에서 리뷰어가 리포트에 `[ESCALATE: PLANNER]` 태그를 남기면 설계 변경 수준으로 판단하여 Planner에게 계획 갱신을 위임합니다 (린트는 리뷰어가 검토 관점 중 하나로 확인할 뿐, 별도의 자동 게이트는 아닙니다). | +| **Rebuttal** | `--max-rebut N` | 이터레이션당 Creator 반론 횟수 (기본 1, `0`이면 끔). | +| **Control** | `--verbose` / `--cleanup` | 상세 로그 / 성공 시 임시 job 디렉터리 삭제. | --- @@ -176,26 +179,27 @@ sequenceDiagram ```bash # 1. Creator Self-Planning & Development + Self-review (direct task execution using existing promoted plan or Creator's own self-plan) bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ - --target-agent "" \ + --creator "" \ --task "Fix typo in deploy/README.md" # 2. Collaborative planning + Targeted Reviewers + Safety limits # (실전 자율 루프 기동의 표준 패턴 — 리뷰어 2인 지정 + 최대 3회 반복) bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ + --creator "" \ --plan \ + --planner "" \ --plan-talk 1 \ --reviewer "," \ --max-loop 3 \ --verbose \ - --target-agent "" \ --task "Refactor the session backup mechanism to handle NFS flock" # 3. Total validation (all reviewers must PASS) bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ + --creator "" \ --all-reviewer \ --max-loop 5 \ --cleanup \ - --target-agent "" \ --task "Close CI shellcheck coverage gaps" ``` 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 519d00d..7979a10 100644 --- a/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh +++ b/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh @@ -31,12 +31,15 @@ CLEANUP=false TARGET_AGENT="" TASK="" REVIEWER_LIST="" +PLANNER_SESSION_OVERRIDE="" # Print usage instructions usage() { - echo "Usage: $0 [options] --target-agent --task " + echo "Usage: $0 [options] --creator --task " echo "Options:" + echo " --creator Creator session that implements the task (required)" echo " --plan Enable Planner agent intervention & design phase" + echo " --planner Planner session (requires --plan; default: auto-resolve)" 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" @@ -73,7 +76,12 @@ while [[ "$#" -gt 0 ]]; do MAX_REBUT="$2"; shift 2 ;; --verbose) VERBOSE=true; shift ;; --cleanup) CLEANUP=true; shift ;; - --target-agent) TARGET_AGENT="$2"; shift 2 ;; + --creator) TARGET_AGENT="$2"; shift 2 ;; + --target-agent) + echo "ERROR: --target-agent was removed. Use --creator instead." >&2 + exit 1 + ;; + --planner) PLANNER_SESSION_OVERRIDE="$2"; shift 2 ;; --task) TASK="$2"; shift 2 ;; -h|--help) usage ;; *) echo "Unknown option: $1"; usage ;; @@ -85,10 +93,17 @@ done REBUT_TOTAL_BUDGET=$((MAX_REBUT * MAX_LOOP)) if [ -z "$TARGET_AGENT" ] || [ -z "$TASK" ]; then - echo "ERROR: --target-agent and --task are mandatory fields." + echo "ERROR: --creator and --task are mandatory fields." >&2 usage fi +if [ -n "${PLANNER_SESSION_OVERRIDE:-}" ] && [ "$PLAN_MODE" = false ]; then + echo "ERROR: --planner was specified without --plan." >&2 + echo "To enable planning, include the --plan flag:" >&2 + echo " run_loop.sh --creator --plan --planner --task \"...\"" >&2 + exit 1 +fi + # --- B-13 Stage 2: freeze the runtime before the loop can be edited under us --- # bash keeps reading a running script from disk by byte offset, so a worker that # edits .agents/skills/ mid-loop can break this very file (measured: even a valid @@ -351,15 +366,35 @@ elif [ "$TARGET_STATUS" != "running" ]; then 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 +if [ -n "${PLANNER_SESSION_OVERRIDE:-}" ]; then + # Explicit --planner skips auto-resolve. Pre-freeze already required --plan. + PLANNER_SESSION="$PLANNER_SESSION_OVERRIDE" + PLANNER_STATUS=$(MAM_STATE_JSON="$(load_state_json)" TARGET="$PLANNER_SESSION" 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 "$PLANNER_STATUS" ]; then + log_error "Specified planner session '$PLANNER_SESSION' is not registered in the session registry." + exit 1 + elif [ "$PLANNER_STATUS" != "running" ]; then + log_error "Specified planner session '$PLANNER_SESSION' is not running (current status: '$PLANNER_STATUS')." + exit 1 + fi +else + PLANNER_SESSION=$(resolve_planner_session) + if [ "$PLAN_MODE" = true ] && [ -z "$PLANNER_SESSION" ]; then log_error "Planner mode enabled (--plan) but no running session with a 'planner' role was found." exit 1 fi fi +log_info "Resolved Planner session: $PLANNER_SESSION" CURRENT_PLAN="" CREATED_JOBS=() diff --git a/deploy/INSTALL.md b/deploy/INSTALL.md index 5c6a3d3..ef51638 100644 --- a/deploy/INSTALL.md +++ b/deploy/INSTALL.md @@ -108,7 +108,7 @@ $ bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ --plan-talk 1 \ --reviewer "reviewer-a,reviewer-b" \ --max-loop 3 \ - --target-agent my-project-dev-claude \ + --creator my-project-dev-claude \ --task "구현할 명확한 개발 작업 목표" ``` * `--max-loop`는 코드 오류 발견 시 최대 교정(반복 수정) 횟수 제한 가드레일 역할을 합니다. diff --git a/tests/test_loop_cli.py b/tests/test_loop_cli.py new file mode 100644 index 0000000..41225b6 --- /dev/null +++ b/tests/test_loop_cli.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""CLI parser and planner-resolution tests for multi-agent-mux-loop (Rev.4).""" + +import os +import subprocess +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +RUN_LOOP_SH = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "run_loop.sh" +SKILL_MD = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "SKILL.md" + + +def _run_loop(args, env_extra=None, cwd=None): + env = dict(os.environ) + if env_extra: + env.update(env_extra) + return subprocess.run( + ["bash", str(RUN_LOOP_SH), *args], + capture_output=True, + text=True, + env=env, + cwd=str(cwd or REPO_ROOT), + ) + + +def _combined(res): + return (res.stdout or "") + (res.stderr or "") + + +def test_help_lists_creator_not_target_agent(): + res = _run_loop(["--help"]) + assert res.returncode != 0 + text = _combined(res) + assert "--creator" in text + assert "--planner" in text + assert "--target-agent" not in text + + +def test_missing_creator_and_task_fail_fast(): + res = _run_loop([]) + assert res.returncode != 0 + assert "ERROR: --creator and --task are mandatory fields." in _combined(res) + + +def test_creator_parses_and_reaches_session_check(tmp_path): + res = _run_loop( + ["--creator", "nonexistent-agent", "--task", "t"], + env_extra={"MAM_LOOP_NO_FREEZE": "1", "MAM_LOOP_MARKER": str(tmp_path / "loop-guard")}, + ) + assert res.returncode != 0 + text = _combined(res) + assert "was removed" not in text + assert "mandatory fields" not in text + assert "not registered" in text or "already running" in text + + +def test_target_agent_is_rejected(): + res = _run_loop(["--target-agent", "any-session", "--task", "t"]) + assert res.returncode == 1 + text = _combined(res) + assert "ERROR: --target-agent was removed. Use --creator instead." in text + assert "mandatory fields" not in text + + +def test_planner_without_plan_fail_fast(): + res = _run_loop(["--creator", "c1", "--planner", "p1", "--task", "t"]) + assert res.returncode == 1 + text = _combined(res) + assert "ERROR: --planner was specified without --plan." in text + assert "--plan --planner" in text + + +def test_skill_md_uses_creator_flag(): + content = SKILL_MD.read_text(encoding="utf-8") + assert "--creator" in content + assert "--planner" in content + assert "--target-agent" not in content + + +def _seed_sessions(yaml_path, sessions): + lines = ["herdr_sessions:"] + for s in sessions: + lines.append(f"- name: {s['name']}") + lines.append(f" status: {s['status']}") + lines.append(f" role: {s['role']}") + yaml_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + db_path = yaml_path.with_suffix(".db") + if db_path.exists(): + db_path.unlink() + + +def _loop_env(mam_sandbox, marker_path): + return { + "MAM_LOOP_NO_FREEZE": "1", + "MAM_LOOP_MARKER": str(marker_path), + "AGENT_SESSIONS_YAML": str(mam_sandbox / ".mam" / "agent-sessions.yaml"), + "WORKSPACE_ROOT": str(mam_sandbox), + "MAM_REAL_ROOT": str(mam_sandbox), + } + + +def test_explicit_planner_unregistered(mam_sandbox, tmp_path): + yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml" + _seed_sessions(yaml_path, [{"name": "creator-1", "status": "running", "role": "creator"}]) + marker = tmp_path / "loop-guard-unreg" + res = _run_loop( + ["--creator", "creator-1", "--plan", "--planner", "missing-planner", "--task", "t"], + env_extra=_loop_env(mam_sandbox, marker), + cwd=mam_sandbox, + ) + assert res.returncode != 0 + assert "Specified planner session 'missing-planner' is not registered in the session registry." in _combined(res) + + +def test_explicit_planner_not_running(mam_sandbox, tmp_path): + yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml" + _seed_sessions( + yaml_path, + [ + {"name": "creator-1", "status": "running", "role": "creator"}, + {"name": "planner-stopped", "status": "stopped", "role": "planner"}, + ], + ) + marker = tmp_path / "loop-guard-stopped" + res = _run_loop( + ["--creator", "creator-1", "--plan", "--planner", "planner-stopped", "--task", "t"], + env_extra=_loop_env(mam_sandbox, marker), + cwd=mam_sandbox, + ) + assert res.returncode != 0 + assert "Specified planner session 'planner-stopped' is not running (current status: 'stopped')." in _combined(res) + + +def test_explicit_planner_skips_auto_resolve(mam_sandbox, tmp_path): + yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml" + _seed_sessions( + yaml_path, + [ + {"name": "creator-1", "status": "running", "role": "creator"}, + {"name": "planner-first", "status": "running", "role": "planner"}, + {"name": "planner-explicit", "status": "running", "role": "planner"}, + ], + ) + marker = tmp_path / "loop-guard-skip" + res = _run_loop( + ["--creator", "creator-1", "--plan", "--planner", "planner-explicit", "--task", "t"], + env_extra=_loop_env(mam_sandbox, marker), + cwd=mam_sandbox, + ) + text = _combined(res) + assert "Resolved Planner session: planner-explicit" in text + assert "Resolved Planner session: planner-first" not in text diff --git a/tests/test_o2_race_free_lock.py b/tests/test_o2_race_free_lock.py index 2527019..67bd3c9 100644 --- a/tests/test_o2_race_free_lock.py +++ b/tests/test_o2_race_free_lock.py @@ -187,7 +187,7 @@ def test_o2_12_run_loop_exits_on_lock_failure(tmp_path): marker = tmp_path / "loop-guard-active" proc = acquire_bg(marker) try: - cmd = ["bash", str(RUN_LOOP), "--target-agent", "dummy-agent", "--task", "test"] + cmd = ["bash", str(RUN_LOOP), "--creator", "dummy-agent", "--task", "test"] run_env = dict(os.environ) run_env["MAM_LOOP_MARKER"] = str(marker) res = subprocess.run(cmd, capture_output=True, text=True, cwd=str(tmp_path), env=run_env) diff --git a/tests/test_o3_scoped_guard.py b/tests/test_o3_scoped_guard.py index 64b32bb..1a0b4d0 100644 --- a/tests/test_o3_scoped_guard.py +++ b/tests/test_o3_scoped_guard.py @@ -379,7 +379,7 @@ def test_b13_reexec_preserves_original_argv(tmp_path): """ run_loop_sh = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "run_loop.sh" res = subprocess.run( - ["bash", str(run_loop_sh), "--target-agent", "nonexistent-agent", "--task", "test goal with spaces"], + ["bash", str(run_loop_sh), "--creator", "nonexistent-agent", "--task", "test goal with spaces"], capture_output=True, text=True, cwd=str(REPO_ROOT), @@ -461,7 +461,7 @@ def test_b13_no_freeze_switch_disables_reexec(tmp_path): env = os.environ.copy() env["MAM_LOOP_NO_FREEZE"] = "1" res = subprocess.run( - ["bash", str(run_loop_sh), "--target-agent", "nonexistent-agent", "--task", "test goal"], + ["bash", str(run_loop_sh), "--creator", "nonexistent-agent", "--task", "test goal"], capture_output=True, text=True, env=env, diff --git a/tests/test_tier4_e2e.py b/tests/test_tier4_e2e.py index 0789267..e2f88f2 100644 --- a/tests/test_tier4_e2e.py +++ b/tests/test_tier4_e2e.py @@ -432,7 +432,7 @@ d['herdr_sessions'] = [ # Run run_loop.sh cmd_loop = [ "bash", str(loop_script), - "--target-agent", worker_name, + "--creator", worker_name, "--reviewer", reviewer_name, "--plan", "--plan-talk", "1",