feat(o1): implement reviewer feedback rebuttal and re-adjudication protocol (100% PASS)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user