fix(loop): resolve B-13 by implementing runtime freeze snapshot and dual-root isolation
- Add Stage 2 runtime freeze snapshot at run_loop.sh bootstrap to prevent in-flight tooling mutations - Implement dual-root architecture separating code execution (frozen snapshot) and workspace state (real repo) - Ensure original argv preservation and safe cleanup of freeze directories in exit traps - Add 5 regression guards in tests/test_o3_scoped_guard.py (271/271 PASS) - Update IMPROVEMENTS.md, VERSIONS.md, and include peer review report
This commit is contained in:
@@ -5,9 +5,14 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# B-13: the arg parser below consumes "$@" (shift), so capture argv now —
|
||||
# the freeze re-exec needs the original arguments (measured: $#=0 after parse).
|
||||
MAM_LOOP_ARGV=("$@")
|
||||
|
||||
# 1. Load Common Framework Library
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
MAM_REAL_ROOT="${MAM_REAL_ROOT:-$REPO_ROOT}"
|
||||
# shellcheck disable=SC1091
|
||||
source "$REPO_ROOT/.agents/skills/lib.sh"
|
||||
# shellcheck disable=SC1091
|
||||
@@ -84,29 +89,51 @@ if [ -z "$TARGET_AGENT" ] || [ -z "$TASK" ]; then
|
||||
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; }
|
||||
# --- 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
|
||||
# replacement died with "unexpected EOF"). Re-exec once from a snapshot.
|
||||
# NOTE: log_* are not defined until :114 — use echo here, not log_warn (C2).
|
||||
if [ -z "${MAM_LOOP_FREEZE_DIR:-}" ] && [ "${MAM_LOOP_NO_FREEZE:-0}" != "1" ]; then
|
||||
_freeze=""
|
||||
_freeze="$(mktemp -d "${TMPDIR:-/tmp}/mam-loop-freeze.XXXXXX" 2>/dev/null)" || _freeze=""
|
||||
if [ -n "$_freeze" ] && mkdir -p "$_freeze/.agents" 2>/dev/null \
|
||||
&& cp -R "$REPO_ROOT/.agents/skills" "$_freeze/.agents/skills" 2>/dev/null; then
|
||||
export MAM_LOOP_FREEZE_DIR="$_freeze"
|
||||
export MAM_LOOP_FREEZE_OWNED="1" # ← C1: cleanup gate requires this
|
||||
export MAM_REAL_ROOT="$REPO_ROOT"
|
||||
export WORKSPACE_ROOT="$REPO_ROOT"
|
||||
[ -f "$REPO_ROOT/.mam.env" ] && export MAM_ENV_FILE="$REPO_ROOT/.mam.env"
|
||||
exec bash "$_freeze/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh" \
|
||||
${MAM_LOOP_ARGV[@]+"${MAM_LOOP_ARGV[@]}"} # ← P1: original argv (bash 3.2 guarded)
|
||||
fi
|
||||
[ -n "$_freeze" ] && rm -rf "$_freeze"
|
||||
echo -e "\033[1;33m[!]\033[0m freeze snapshot failed — continuing unfrozen (B-13 protection off)" >&2
|
||||
fi
|
||||
|
||||
# Runs the delegate-job wrapper in place. Deliberately creates no copy and
|
||||
# installs no trap:
|
||||
# * a copy inside .agents/skills/ pollutes the source tree and leaks on
|
||||
# SIGKILL (B-6). It never protected across turns anyway — the copy is made
|
||||
# per call, so a wrapper broken in turn N is copied broken in turn N+1;
|
||||
# * every call site is a command substitution, so a trap set here fires when
|
||||
# that subshell ends. `$$` is still the parent's pid there, so
|
||||
# _mam_release_guard passed its ownership check and dropped the loop lock
|
||||
# after the first delegated job (D1).
|
||||
# The callers' own "Failed to register ..." branches are unreachable when the
|
||||
# wrapper exits non-zero (set -e aborts the assignment first), so the diagnosis
|
||||
# has to be emitted here.
|
||||
MAM_LOOP_MARKER="${MAM_LOOP_MARKER:-$MAM_REAL_ROOT/.mam/loop-guard-active}"
|
||||
_mam_release_guard() {
|
||||
mam_release_loop_lock "$MAM_LOOP_MARKER" || true
|
||||
# B-13: 스냅샷은 우리가 만들었을 때만 지운다 (외부 주입 값은 건드리지 않음)
|
||||
if [ -n "${MAM_LOOP_FREEZE_DIR:-}" ] && [ "${MAM_LOOP_FREEZE_OWNED:-0}" = "1" ]; then
|
||||
case "$MAM_LOOP_FREEZE_DIR" in
|
||||
*/mam-loop-freeze.*) rm -rf "$MAM_LOOP_FREEZE_DIR" ;;
|
||||
*) : ;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
# Runs the delegate-job wrapper from the frozen skills tree (REPO_ROOT).
|
||||
# Stage 2 creates a single snapshot at loop initialization outside the skill tree (B-13),
|
||||
# keeping the running loop immune across turns without per-call copies or tree pollution (B-6).
|
||||
delegate_job_safe() {
|
||||
local orig_script="$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job"
|
||||
local wrapper_script="$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job"
|
||||
local rc=0
|
||||
bash "$orig_script" "$@" || rc=$?
|
||||
bash "$wrapper_script" "$@" || rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
log_error "delegate_job_safe failed (exit $rc): $orig_script"
|
||||
log_error "delegate_job_safe failed (exit $rc): $wrapper_script"
|
||||
log_error " if this loop edits framework skills in place, check that file's syntax:"
|
||||
log_error " bash -n \"$orig_script\""
|
||||
log_error " bash -n \"$wrapper_script\""
|
||||
fi
|
||||
return $rc
|
||||
}
|
||||
@@ -145,7 +172,7 @@ case "$_mam_acquire_rc" in
|
||||
;;
|
||||
esac
|
||||
trap _mam_release_guard EXIT INT TERM HUP
|
||||
rm -f "$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job".*.tmp 2>/dev/null || true
|
||||
rm -f "$MAM_REAL_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job".*.tmp 2>/dev/null || true
|
||||
|
||||
# --all-reviewer silently takes precedence over an explicit --reviewer list;
|
||||
# warn so the discarded list isn't mistaken for having been honored (P2-1).
|
||||
@@ -458,7 +485,7 @@ 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 "")
|
||||
BASE_COMMIT=$(cd -P "$MAM_REAL_ROOT" 2>/dev/null && git rev-parse HEAD 2>/dev/null || echo "")
|
||||
|
||||
EXECUTION_PROMPT="계획서가 존재하지 않으므로, 작업자(Creator)의 판단하에 스스로 구현 계획 및 설계를 수립한 뒤, 이를 바탕으로 코드를 구현하고 다음 작업 목표를 완성해주세요. 작업 목표: $TASK"
|
||||
if [ -n "$CURRENT_PLAN" ]; then
|
||||
@@ -550,7 +577,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do
|
||||
else
|
||||
log_info "Active reviewers: ${REVIEWERS[*]}"
|
||||
|
||||
CHANGES_DIFF=$(mam_collect_changes_diff "$REPO_ROOT" "$BASE_COMMIT") || {
|
||||
CHANGES_DIFF=$(mam_collect_changes_diff "$MAM_REAL_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
|
||||
|
||||
Reference in New Issue
Block a user