feat(o1): implement reviewer feedback rebuttal and re-adjudication protocol (100% PASS)

This commit is contained in:
2026-08-06 09:21:28 +09:00
parent 2d5e20a6aa
commit cdeb9e2c8f
7 changed files with 550 additions and 22 deletions
@@ -16,6 +16,7 @@ PLAN_MODE=false
PLAN_TALK_TURNS=1
ALL_REVIEWERS=false
MAX_LOOP=3
MAX_REBUT=1
VERBOSE=false
CLEANUP=false
TARGET_AGENT=""
@@ -31,6 +32,7 @@ usage() {
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
@@ -54,6 +56,12 @@ while [[ "$#" -gt 0 ]]; do
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 ;;
@@ -63,6 +71,10 @@ while [[ "$#" -gt 0 ]]; do
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
@@ -116,6 +128,31 @@ has_verdict() {
[[ "$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"
@@ -437,6 +474,12 @@ 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 \
@@ -471,6 +514,10 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do
# 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'..."
@@ -528,6 +575,8 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do
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"
@@ -543,13 +592,18 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do
exit 1
fi
# 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).
# 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
if echo "$FEEDBACK_AGGREGATE" | grep -qE '^\[ESCALATE: PLANNER\][[:space:]]*\r?$'; then
COMPLEX_FIX=true
fi
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..."
@@ -582,6 +636,10 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do
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" \
@@ -597,6 +655,135 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do
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: <reviewer>]' 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