diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 75322f6..bb3c3cd 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -16,6 +16,9 @@ if [ -z "${BASH_VERSION:-}" ]; then echo "ERROR: lib.sh must be executed/sourced from bash (foreign shell detected)" >&2 + # Reachable when this file is executed directly rather than sourced; + # `return` only fails (falling through to `exit`) in that path. + # shellcheck disable=SC2317 return 1 2>/dev/null || exit 1 fi SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -1018,10 +1021,10 @@ start_watchdog() { # Start the wildcard monitor subscriber daemon with --idle-timeout 0 (never idle out) # and ensure it runs with $workdir as cwd to anchor relative log paths. local orig_pwd="$PWD" - cd "$workdir" + cd "$workdir" || return 1 nohup bash "$monitor_script" --subscribe --idle-timeout 0 >> "$log_file" 2>&1 & pid=$! - cd "$orig_pwd" + cd "$orig_pwd" || return 1 fi echo "$pid" diff --git a/.agents/skills/multi-agent-mux-loop/SKILL.md b/.agents/skills/multi-agent-mux-loop/SKILL.md index 59eec7b..3449328 100644 --- a/.agents/skills/multi-agent-mux-loop/SKILL.md +++ b/.agents/skills/multi-agent-mux-loop/SKILL.md @@ -143,7 +143,7 @@ sequenceDiagram | **Phase 2: Execution** | (기본값) | `--target-agent`로 명시한 주 작업 세션에 코딩 태스크를 주입합니다. | | **Phase 3: Review** | `--reviewer "A,B"` | 지정된 리뷰어 세션 리스트(`A`, `B` 등)에 교차 Peer Review를 위임합니다. | | **Phase 3: Consensus** | `--all-reviewer` | 레지스트리에 등록된 모든 active 리뷰어 세션을 자동으로 수집하여 리뷰를 돌립니다. (지정/수집된 모든 리뷰어의 PASS 만장일치가 항상 필요합니다.) | -| **Iterative Loop** | `--max-loop M` | NOT PASS 또는 린트 실패 시 최대 `M`회까지 Planner와 Creator 간 피드백 루프를 반복합니다. | +| **Iterative Loop** | `--max-loop M` | NOT PASS 판정 시 최대 `M`회까지 Creator가 자체 수정합니다. `--plan` 모드에서 리뷰어가 리포트에 `[ESCALATE: PLANNER]` 태그를 남기면 설계 변경 수준으로 판단하여 Planner에게 계획 갱신을 위임합니다 (린트는 리뷰어가 검토 관점 중 하나로 확인할 뿐, 별도의 자동 게이트는 아닙니다). | --- 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 da15842..1ce04a2 100644 --- a/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh +++ b/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh @@ -84,10 +84,20 @@ log_error() { echo -e "\033[1;31m[✗]\033[0m $1" } -# Verdict must occupy its own line — quoted/diff-embedded tokens ('+', '>' prefixed) never match. +# --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 + +# 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" - grep -qE "^\[VERDICT: ${verdict}\][[:space:]]*\r?$" "$file" + local last_line pattern + last_line=$(grep -v '^[[:space:]]*$' "$file" 2>/dev/null | tail -n 1) + pattern="^\[VERDICT: ${verdict}\][[:space:]]*\r?\$" + [[ "$last_line" =~ $pattern ]] } # Helper: Blocking wait for a delegate job's completion or error state (with safety timeout) @@ -128,100 +138,58 @@ except Exception: return 1 } -# Resolve active reviewers from SQL DB or YAML (excluding $TARGET_AGENT) +# 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" python3 -c " -import sqlite3, os, yaml, json -yaml_path = '.mam/agent-sessions.yaml' -db_path = '.mam/agent-sessions.db' + 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 = [] -if os.path.exists(db_path): - try: - conn = sqlite3.connect(db_path) - cursor = conn.execute('SELECT data FROM sessions') - for r in cursor.fetchall(): - s = json.loads(r[0]) - if s.get('role') == 'reviewer' and s.get('name') != target_agent: - reviewers.append(s.get('name')) - conn.close() - except Exception: - pass -if not reviewers 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('role') == 'reviewer' and s.get('name') != target_agent: - reviewers.append(s.get('name')) - except Exception: - pass +reviewers = [s.get('name') for s in d.get('tmux_sessions', []) + if s.get('role') == 'reviewer' and s.get('name') != target_agent] print(','.join(reviewers)) " } -# Resolve target agent type (claude, cline, agy) +# Resolve target agent type (claude, cline, agy) via load_state_json. resolve_agent_type() { local name="$1" - NAME="$name" python3 -c " -import sqlite3, os, yaml, json -yaml_path = '.mam/agent-sessions.yaml' -db_path = '.mam/agent-sessions.db' + 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 -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') - conn.close() - except Exception: - pass -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: - agent = s.get('agent') or s.get('pane', {}).get('cmd') - break - except Exception: - pass +for s in d.get('tmux_sessions', []): + if s.get('name') == name: + agent = s.get('agent') or s.get('pane', {}).get('cmd') + break if not agent: - agent = 'agy' if 'agy' in name else 'cline' if 'cline' in name else 'hermes' if 'hermes' in name else 'claude' + # 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 +# Resolve planner session dynamically via load_state_json. resolve_planner_session() { - python3 -c " -import sqlite3, os, yaml, json -yaml_path = '.mam/agent-sessions.yaml' -db_path = '.mam/agent-sessions.db' + MAM_STATE_JSON="$(load_state_json)" python3 -c " +import os, json +d = json.loads(os.environ.get('MAM_STATE_JSON', '{}')) planner = 'canary-projects-multi-agent-mux-planner-claude' -if os.path.exists(db_path): - try: - conn = sqlite3.connect(db_path) - row = conn.execute('SELECT name FROM sessions WHERE role=\"planner\" LIMIT 1').fetchone() - if row: - planner = row[0] - conn.close() - print(planner) - raise SystemExit(0) - except Exception: - pass -if 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('role') == 'planner': - print(s.get('name')) - raise SystemExit(0) - except Exception: - pass +for s in d.get('tmux_sessions', []): + if s.get('role') == 'planner': + planner = s.get('name') + break print(planner) " } @@ -350,6 +318,11 @@ 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=$(git rev-parse HEAD 2>/dev/null || echo "") + EXECUTION_PROMPT="다음 작업 목표를 완성해주세요: $TASK" if [ "$PLAN_MODE" = true ]; then EXECUTION_PROMPT="최종 합의된 다음 계획서에 입각하여 코드를 수정 및 구현해주세요. 계획서:\n$CURRENT_PLAN" @@ -432,14 +405,20 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do for rev in "${REVIEWERS[@]}"; do log_info "Requesting code review from Reviewer '$rev'..." - # Cumulative working tree diff (M-6) - CHANGES_DIFF=$(git diff 2>/dev/null || echo "No git diff available") + # Cumulative diff since BASE_COMMIT (M-6): includes committed AND + # uncommitted changes, so it stays non-empty even after the Creator + # commits per the documented DoD (bare `git diff` alone would not). + if [ -n "$BASE_COMMIT" ]; then + CHANGES_DIFF=$(git diff "$BASE_COMMIT" 2>/dev/null || echo "No git diff available") + else + CHANGES_DIFF=$(git diff 2>/dev/null || echo "No git diff available") + fi REV_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \ --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]' 태그를 리뷰 리포트 마지막에 단독 행으로 명시적으로 작성해주세요. 만약 단순 버그 수정으로는 부족하고 설계 변경/재작업 수준의 재계획이 필요하다고 판단되면, 리포트 아무 곳에나 단독 행으로 '[ESCALATE: PLANNER]' 태그도 함께 남겨주세요. 변경분:\n$CHANGES_DIFF") REV_JOB_ID=$(extract_job_id "$REV_OUTPUT") if [ -z "$REV_JOB_ID" ]; then @@ -494,9 +473,11 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do exit 1 fi - # Re-planning check for complex refactoring feedback + # Re-planning check: rely on the explicit '[ESCALATE: PLANNER]' tag a + # reviewer is instructed to emit, rather than sniffing English keywords + # (reviewers report in Korean, so keyword matching never fired) (P1-1). COMPLEX_FIX=false - if echo "$FEEDBACK_AGGREGATE" | grep -Eiq "refactor|complex|design|architect"; then + if echo "$FEEDBACK_AGGREGATE" | grep -qE '^\[ESCALATE: PLANNER\][[:space:]]*\r?$'; then COMPLEX_FIX=true fi