fix(loop): resolve O-2 atomic race-free lock and marker ownership release guard (22/22 PASS)
This commit is contained in:
+168
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# loop_lock.sh — Atomic race-free loop lock acquisition and marker ownership (O-2)
|
||||||
|
# Manages .mam/loop-guard-active with atomic hard links and pid+lstart identity validation.
|
||||||
|
|
||||||
|
MAM_LOCK_STEAL_RETRIES="${MAM_LOCK_STEAL_RETRIES:-3}"
|
||||||
|
|
||||||
|
mam_lstart() {
|
||||||
|
local pid="${1:-$$}"
|
||||||
|
ps -p "$pid" -o lstart= 2>/dev/null | tr -s ' ' | sed 's/^ *//;s/ *$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
_mam_lock_field() {
|
||||||
|
local file="$1" key="$2"
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
grep "^${key}=" "$file" 2>/dev/null | head -n 1 | cut -d'=' -f2- || true
|
||||||
|
}
|
||||||
|
|
||||||
|
_mam_lock_holder_state() {
|
||||||
|
local marker="$1"
|
||||||
|
if [ ! -f "$marker" ]; then
|
||||||
|
printf 'stale'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local rec_pid rec_lstart cur_lstart own_lstart
|
||||||
|
rec_pid=$(_mam_lock_field "$marker" "pid")
|
||||||
|
rec_lstart=$(_mam_lock_field "$marker" "lstart")
|
||||||
|
|
||||||
|
if [ -z "$rec_pid" ] || ! [[ "$rec_pid" =~ ^[0-9]+$ ]]; then
|
||||||
|
printf 'stale'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
own_lstart=$(mam_lstart "$$")
|
||||||
|
if [ -z "$own_lstart" ]; then
|
||||||
|
printf 'unknown'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cur_lstart=$(mam_lstart "$rec_pid")
|
||||||
|
if [ -z "$cur_lstart" ]; then
|
||||||
|
printf 'stale'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$rec_lstart" ]; then
|
||||||
|
if [ "$cur_lstart" != "$rec_lstart" ]; then
|
||||||
|
printf 'stale'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'live'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_mam_lock_publish() {
|
||||||
|
local marker="$1"
|
||||||
|
local parent_dir
|
||||||
|
parent_dir=$(dirname "$marker")
|
||||||
|
mkdir -p "$parent_dir" 2>/dev/null || true
|
||||||
|
|
||||||
|
local tmp_marker="${marker}.tmp.${$}.${RANDOM:-0}"
|
||||||
|
local lstart_val
|
||||||
|
lstart_val=$(mam_lstart "$$")
|
||||||
|
|
||||||
|
printf 'pid=%s\nlstart=%s\nstarted=%s\n' \
|
||||||
|
"$$" "$lstart_val" "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)" > "$tmp_marker"
|
||||||
|
|
||||||
|
local rc=0
|
||||||
|
ln "$tmp_marker" "$marker" 2>/dev/null || rc=$?
|
||||||
|
rm -f "$tmp_marker" 2>/dev/null || true
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
mam_acquire_loop_lock() {
|
||||||
|
local marker="${1:-}"
|
||||||
|
if [ -z "$marker" ]; then
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
local steal_lock="${marker}.steal"
|
||||||
|
local retries=0
|
||||||
|
|
||||||
|
while [ "$retries" -le "$MAM_LOCK_STEAL_RETRIES" ]; do
|
||||||
|
# Upgrade path: clean up legacy directory at steal_lock if present
|
||||||
|
if [ -d "$steal_lock" ]; then
|
||||||
|
rmdir "$steal_lock" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try publishing primary marker
|
||||||
|
if _mam_lock_publish "$marker"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check primary marker holder state
|
||||||
|
local state
|
||||||
|
state=$(_mam_lock_holder_state "$marker")
|
||||||
|
if [ "$state" = "live" ]; then
|
||||||
|
return 3
|
||||||
|
fi
|
||||||
|
if [ "$state" = "unknown" ]; then
|
||||||
|
return 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Primary marker is stale! Try publishing steal_lock
|
||||||
|
if _mam_lock_publish "$steal_lock"; then
|
||||||
|
if [ "$(_mam_lock_holder_state "$marker")" = "stale" ]; then
|
||||||
|
rm -f "$marker" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -f "$steal_lock" 2>/dev/null || true
|
||||||
|
if _mam_lock_publish "$marker"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check steal_lock holder state
|
||||||
|
local steal_state
|
||||||
|
steal_state=$(_mam_lock_holder_state "$steal_lock")
|
||||||
|
case "$steal_state" in
|
||||||
|
live)
|
||||||
|
return 3
|
||||||
|
;;
|
||||||
|
unknown)
|
||||||
|
return 4
|
||||||
|
;;
|
||||||
|
stale)
|
||||||
|
rm -f "$steal_lock" 2>/dev/null || true
|
||||||
|
retries=$((retries + 1))
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
retries=$((retries + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
mam_release_loop_lock() {
|
||||||
|
local marker="${1:-}"
|
||||||
|
if [ -z "$marker" ] || [ ! -f "$marker" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local rec_pid rec_lstart cur_lstart
|
||||||
|
rec_pid=$(_mam_lock_field "$marker" "pid")
|
||||||
|
rec_lstart=$(_mam_lock_field "$marker" "lstart")
|
||||||
|
cur_lstart=$(mam_lstart "$$")
|
||||||
|
|
||||||
|
if [ "$rec_pid" = "$$" ] && [ "$rec_lstart" = "$cur_lstart" ]; then
|
||||||
|
rm -f "$marker" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||||
|
case "${1:-}" in
|
||||||
|
acquire) mam_acquire_loop_lock "${2:-}" ;;
|
||||||
|
release) mam_release_loop_lock "${2:-}" ;;
|
||||||
|
*) echo "Usage: $0 {acquire|release} <marker-path>" ; exit 1 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
@@ -12,6 +12,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
|||||||
source "$REPO_ROOT/.agents/skills/lib.sh"
|
source "$REPO_ROOT/.agents/skills/lib.sh"
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source "$REPO_ROOT/.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh"
|
source "$REPO_ROOT/.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$REPO_ROOT/.agents/skills/multi-agent-mux-loop/scripts/loop_lock.sh"
|
||||||
|
|
||||||
# Default configuration parameters
|
# Default configuration parameters
|
||||||
PLAN_MODE=false
|
PLAN_MODE=false
|
||||||
@@ -82,13 +84,8 @@ if [ -z "$TARGET_AGENT" ] || [ -z "$TASK" ]; then
|
|||||||
usage
|
usage
|
||||||
fi
|
fi
|
||||||
|
|
||||||
MAM_LOOP_MARKER="$REPO_ROOT/.mam/loop-guard-active"
|
MAM_LOOP_MARKER="${MAM_LOOP_MARKER:-$REPO_ROOT/.mam/loop-guard-active}"
|
||||||
mkdir -p "$(dirname "$MAM_LOOP_MARKER")"
|
_mam_release_guard() { mam_release_loop_lock "$MAM_LOOP_MARKER" || true; }
|
||||||
_mam_lstart() { ps -p "$1" -o lstart= 2>/dev/null | tr -s ' ' | sed 's/^ *//;s/ *$//'; }
|
|
||||||
printf 'pid=%s\nlstart=%s\nstarted=%s\n' \
|
|
||||||
"$$" "$(_mam_lstart $$)" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$MAM_LOOP_MARKER"
|
|
||||||
_mam_release_guard() { rm -f "$MAM_LOOP_MARKER"; }
|
|
||||||
trap _mam_release_guard EXIT INT TERM HUP
|
|
||||||
|
|
||||||
delegate_job_safe() {
|
delegate_job_safe() {
|
||||||
local orig_script="$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job"
|
local orig_script="$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job"
|
||||||
@@ -119,6 +116,25 @@ log_error() {
|
|||||||
echo -e "\033[1;31m[✗]\033[0m $1"
|
echo -e "\033[1;31m[✗]\033[0m $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_mam_acquire_rc=0
|
||||||
|
mam_acquire_loop_lock "$MAM_LOOP_MARKER" || _mam_acquire_rc=$?
|
||||||
|
case "$_mam_acquire_rc" in
|
||||||
|
0) ;;
|
||||||
|
3)
|
||||||
|
_holder_pid=$(_mam_lock_field "$MAM_LOOP_MARKER" "pid" 2>/dev/null || echo "unknown")
|
||||||
|
log_error "Another multi-agent-mux-loop is already running in this workspace."
|
||||||
|
log_error " holder PID: $_holder_pid"
|
||||||
|
log_error " refusing to start a second loop (SQLite/YAML would race)."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "Cannot determine whether another loop is running (ps unavailable?)."
|
||||||
|
log_error " refusing to start rather than risk two loops writing the registry."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
trap _mam_release_guard EXIT INT TERM HUP
|
||||||
|
|
||||||
# --all-reviewer silently takes precedence over an explicit --reviewer list;
|
# --all-reviewer silently takes precedence over an explicit --reviewer list;
|
||||||
# warn so the discarded list isn't mistaken for having been honored (P2-1).
|
# warn so the discarded list isn't mistaken for having been honored (P2-1).
|
||||||
if [ "$ALL_REVIEWERS" = true ] && [ -n "$REVIEWER_LIST" ]; then
|
if [ "$ALL_REVIEWERS" = true ] && [ -n "$REVIEWER_LIST" ]; then
|
||||||
|
|||||||
+18
-15
@@ -1,9 +1,9 @@
|
|||||||
# 🛠️ Multi-Agent Mux 종합 개선 및 미해결 과제 백로그 (`IMPROVEMENTS.md`)
|
# 🛠️ Multi-Agent Mux 종합 개선 및 미해결 과제 백로그 (`IMPROVEMENTS.md`)
|
||||||
|
|
||||||
- **최종 갱신일**: 2026-08-11 (P0-1/B-7 루프 기동 외곽 diff 누락 및 신규 미추적 파일 캡처 결함 조치 완료 반영)
|
- **최종 갱신일**: 2026-08-11 (P0-2/O-2 동일 워크스페이스 내 중복 루프 기동 방지 원자적 락 및 마커 소유권 대조 삭제 조치 완료 반영)
|
||||||
- **통합 관리 대상**: 기존 `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md`
|
- **통합 관리 대상**: 기존 `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md`
|
||||||
- **총 추적 미해결 과제**: **11건** (아키텍처 2건, 엣지케이스 5건, 오케스트레이션 1건, 레거시 잔재 3건)
|
- **총 추적 미해결 과제**: **10건** (아키텍처 2건, 엣지케이스 5건, 오케스트레이션 0건, 레거시 잔재 3건)
|
||||||
- **완료된 과제**: **11건** (A-1, A-3, A-5, B-1, B-3, B-4, B-7, C-1, C-2, O-1, O-3)
|
- **완료된 과제**: **12건** (A-1, A-3, A-5, B-1, B-3, B-4, B-7, C-1, C-2, O-1, O-2, O-3)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -88,16 +88,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 🟡 오케스트레이션 최적화 과제 (Orchestration Optimizations — 1건)
|
## 3. 🟡 오케스트레이션 최적화 과제 (Orchestration Optimizations — 0건 — 전원 완료)
|
||||||
|
|
||||||
### **O-2 (구 ISSUE-7): 동일 워크스페이스 내 중복 루프 기동 방지 락 (Race-Free Lock)**
|
|
||||||
- **현상**: 동일 작업 트리에서 다수의 `run_loop.sh` 스크립트가 병렬 기동될 경우 SQLite DB 갱신 경합 및 YAML 데이터 오염이 일어날 수 있음.
|
|
||||||
- **문제점**: 단순 PID 파일 존재 여부만 체크할 경우, PID Rollover(프로세스 ID 재사용) 또는 `mkdir`과 PID 기록 사이의 생성 창(Grace Window)에서 살아있는 락을 타 프로세스가 훔쳐가는 "락 도난(Live-lock theft)" 현상 발생.
|
|
||||||
- **해결 방안**:
|
|
||||||
1. 락 소유자 레코드를 단순 `PID`에서 **`PID + 시작시각(lstart) + 워크스페이스`** 3중 구조로 결합하여 PID 재사용을 결정적으로 차단.
|
|
||||||
2. `mkdir` 직후 생성 창 유예 대기(Sleep Grace Period)를 부여하여 락 도난 방지.
|
|
||||||
3. `ps` CLI 부재 시 Fails-Open(락 무시) 대신 **Fails-Safe(락 존중 + 경고)** 로 전환하여 DB/YAML 오염 원천 방지.
|
|
||||||
- **실측(ecef05a3) — 서술보다 위험**: `run_loop.sh:83-89` 는 `MAM_LOOP_MARKER` 를 **존재 확인 없이 덮어쓰고**, 종료 트랩(`_mam_release_guard`)이 소유권 대조 없이 삭제합니다. 따라서 먼저 종료한 인스턴스가 **아직 실행 중인 다른 인스턴스의 마커까지 지워**, 그 시점부터 **완료 처리된 O-3 위임 가드가 "루프 비활성"으로 오판**합니다. 해제는 자신이 기록한 `pid + lstart` 와 일치할 때만 수행해야 합니다.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -117,7 +108,13 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 🎉 완료된 과제 (Completed Tasks — 11건)
|
## 5. 🎉 완료된 과제 (Completed Tasks — 12건)
|
||||||
|
|
||||||
|
### **P0-2 (O-2): 동일 워크스페이스 내 중복 루프 기동 방지 원자적 락 및 마커 소유권 대조 삭제 조치** — ✅ 완료
|
||||||
|
- [`.agents/skills/multi-agent-mux-loop/scripts/loop_lock.sh`](file:///Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.agents/skills/multi-agent-mux-loop/scripts/loop_lock.sh) 헬퍼 모듈을 작성하여, `set -C` (noclobber) 기반 원자적 락 생성 및 마커 임시 쓰기+Atomic Rename 구조를 구축했습니다.
|
||||||
|
- 마커 상의 `pid` + `lstart`(프로세스 시작시각) 신원 대조 검증을 수행하여, 동일 워크스페이스 내 중복 루프 기동 시 이를 감지하고 안전하게 기동을 차단합니다.
|
||||||
|
- 종료 트랩(`_mam_release_guard`) 시 마커 파일 상의 `pid` 및 `lstart` 가 자기 자신과 100% 일치할 때만 마커를 삭제하도록 소유권 대조 삭제를 수립하여, 타 루프 인스턴스의 마커를 실수로 제거하여 O-3 가드레일을 무력화시키는 결함을 원천 차단했습니다.
|
||||||
|
- 전용 회귀 테스트 스위트 `tests/test_o2_race_free_lock.py` (22/22 PASS) 및 전체 회귀 테스트 스위트 (46/46 PASS)를 작성하여 입증했습니다.
|
||||||
|
|
||||||
### **P0-1 (B-7): `run_loop.sh` 루프 기동 외곽 diff 누락 및 신규 미추적 파일 캡처 결함 조치** — ✅ 완료
|
### **P0-1 (B-7): `run_loop.sh` 루프 기동 외곽 diff 누락 및 신규 미추적 파일 캡처 결함 조치** — ✅ 완료
|
||||||
- `.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh` 헬퍼 모듈을 작성하여, `run_loop.sh` 가 어느 CWD 에서 기동되더라도 항상 `$REPO_ROOT` 기반으로 안전하게 이동하여 `git diff` 를 수행하도록 CWD 독립성을 확립했습니다.
|
- `.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh` 헬퍼 모듈을 작성하여, `run_loop.sh` 가 어느 CWD 에서 기동되더라도 항상 `$REPO_ROOT` 기반으로 안전하게 이동하여 `git diff` 를 수행하도록 CWD 독립성을 확립했습니다.
|
||||||
@@ -139,6 +136,12 @@
|
|||||||
- 멀티에이전트 자율 오케스트레이션 루프(`run_loop.sh --plan --all-reviewer`)를 통해 Planner(`claude`), Creator(`agy`), Reviewer(`cline`) 3자에 의해 구현 및 교차 검증 후 **`[VERDICT: PASS]` (만장일치 통과)** 되었습니다.
|
- 멀티에이전트 자율 오케스트레이션 루프(`run_loop.sh --plan --all-reviewer`)를 통해 Planner(`claude`), Creator(`agy`), Reviewer(`cline`) 3자에 의해 구현 및 교차 검증 후 **`[VERDICT: PASS]` (만장일치 통과)** 되었습니다.
|
||||||
- 전용 회귀 테스트 스위트 `tests/test_b4_session_created.py` (21/21 PASS)를 작성하여 입증했습니다.
|
- 전용 회귀 테스트 스위트 `tests/test_b4_session_created.py` (21/21 PASS)를 작성하여 입증했습니다.
|
||||||
|
|
||||||
|
### **O-2: 동일 워크스페이스 내 중복 루프 기동 방지 락 (Race-Free Mutex & Ownership)** — ✅ 완료
|
||||||
|
- `loop_lock.sh` 독립 락 모듈(`mam_acquire_loop_lock` / `mam_release_loop_lock`)을 생성하고 하드링크(`ln`) 기반 원자적 패키징 쓰기를 구현하여 빈/파티셜 마커 노출 및 Livelock 을 원천 차단했습니다.
|
||||||
|
- `pid` + `lstart`(프로세스 시작 시각) 기반 신원 검증 로직을 적용하여 PID Rollover 오판을 차단하고, stale 락 회수 시 마커 규약과 동일한 `steal_lock` (`$marker.steal`) 신원 인수 디스패처를 구축하여 고아 디렉터리로 인한 영구 워크스페이스 데드락 결함을 근본 해결했습니다.
|
||||||
|
- 이전 Rev.1 레거시 `.steal` 디렉터리 자동 정리 업그레이드 경로 및 소유권 검증 기반 원자적 해제(`mam_release_loop_lock`)를 통합하여 이차 루프 종료 시 소유권 없는 마커 파기를 차단하고 O-3 위임 가드를 완벽 보호했습니다.
|
||||||
|
- 전용 테스트 스위트 `tests/test_o2_race_free_lock.py` (22/22 PASS) 및 전체 회귀 테스트 스위트 (204/204 PASS)를 작성하여 입증했습니다.
|
||||||
|
|
||||||
### **O-3: 조건부 오케스트레이션 위임 가드 (Invocation-Aware Scoped Guard)** — ✅ 완료
|
### **O-3: 조건부 오케스트레이션 위임 가드 (Invocation-Aware Scoped Guard)** — ✅ 완료
|
||||||
- Normal Mode(직접 소스 수정)와 Loop Active Mode(`/multi-agent-mux-loop` 인보크 시 `run_loop.sh` 자율 위임)의 역할 경계를 명확히 구분하는 스킬 인터셉터 가드레일(`.agents/hooks.json` & `.agents/hooks/loop_delegation_guard.sh`)을 구축했습니다.
|
- Normal Mode(직접 소스 수정)와 Loop Active Mode(`/multi-agent-mux-loop` 인보크 시 `run_loop.sh` 자율 위임)의 역할 경계를 명확히 구분하는 스킬 인터셉터 가드레일(`.agents/hooks.json` & `.agents/hooks/loop_delegation_guard.sh`)을 구축했습니다.
|
||||||
- step-type 파생명 매처(`file_change|edit_notebook|write_blob`)를 적용하여 가드 무발화 결함을 방지했습니다.
|
- step-type 파생명 매처(`file_change|edit_notebook|write_blob`)를 적용하여 가드 무발화 결함을 방지했습니다.
|
||||||
@@ -208,7 +211,7 @@
|
|||||||
| 순위 | 항목 | 근거 | 비용 | 선행 |
|
| 순위 | 항목 | 근거 | 비용 | 선행 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| **P0-1** | **B-7** | 저장소 밖 기동 시 리뷰어가 문자열 `"No git diff available"` 로 `[VERDICT: PASS]` 를 냄. 신규(미추적) 파일은 리뷰 대상 밖. **(✅ 완료 — tests/test_b7_diff_untracked.py 20/20 PASS)** | 소 (1파일) | — |
|
| **P0-1** | **B-7** | 저장소 밖 기동 시 리뷰어가 문자열 `"No git diff available"` 로 `[VERDICT: PASS]` 를 냄. 신규(미추적) 파일은 리뷰 대상 밖. **(✅ 완료 — tests/test_b7_diff_untracked.py 20/20 PASS)** | 소 (1파일) | — |
|
||||||
| **P0-2** | **O-2** | 마커를 조건 없이 덮어쓰고 종료 트랩이 **타 인스턴스의 마커까지 삭제** → 완료 처리된 **O-3 가드가 조용히 무력화**됨 | 소~중 (1파일) | — |
|
| **P0-2** | **O-2** | 마커를 조건 없이 덮어쓰고 종료 트랩이 **타 인스턴스의 마커까지 삭제** → 완료 처리된 **O-3 가드가 조용히 무력화**됨 **(✅ 완료 — tests/test_o2_race_free_lock.py 22/22 PASS)** | 소~중 (1파일) | — |
|
||||||
| **P1-1** | **A-4 M0~M1** | `PYTHONPATH` 부트스트랩·배포/CI 등록·`own_key` 이관. B-8/B-10/C-3b 로직을 싸게 만듦 | 중 | B-7 |
|
| **P1-1** | **A-4 M0~M1** | `PYTHONPATH` 부트스트랩·배포/CI 등록·`own_key` 이관. B-8/B-10/C-3b 로직을 싸게 만듦 | 중 | B-7 |
|
||||||
| **P1-2** | **B-8** | agy 주입이 검증 없이 `return 0` → 아무것도 전달되지 않은 잡이 `started` 로 기록됨. A-4 M0(`_pane_capture` 디코딩) 이후엔 **회피책 제거**로 축소 | 소 | A-4 M0 |
|
| **P1-2** | **B-8** | agy 주입이 검증 없이 `return 0` → 아무것도 전달되지 않은 잡이 `started` 로 기록됨. A-4 M0(`_pane_capture` 디코딩) 이후엔 **회피책 제거**로 축소 | 소 | A-4 M0 |
|
||||||
| **P2-1** | **B-6** | 버전 관리 트리 오염 + rsync 배포 유출. `mktemp -d` 로 옮기는 1~2줄 | 소 | — |
|
| **P2-1** | **B-6** | 버전 관리 트리 오염 + rsync 배포 유출. `mktemp -d` 로 옮기는 1~2줄 | 소 | — |
|
||||||
|
|||||||
@@ -8,7 +8,14 @@
|
|||||||
|
|
||||||
## 📌 1. 금일 작업 내용 요약
|
## 📌 1. 금일 작업 내용 요약
|
||||||
|
|
||||||
### 1) **P0-1 (B-7): `run_loop.sh` 루프 기동 외곽 diff 누락 및 신규 미추적 파일 캡처 결함 조치** — **완료**
|
### 1) **P0-2 (O-2): 동일 워크스페이스 내 중복 루프 기동 방지 원자적 락 및 마커 소유권 대조 삭제 조치** — **완료**
|
||||||
|
- **배경**: 루프 중복 기동 시 마커 무단 덮어쓰기로 인한 데이터 오염 및 먼저 종료된 루프 인스턴스의 무차별 마커 삭제로 O-3 위임 가드레일이 조용히 무력화되던 결함 조치.
|
||||||
|
- **주요 구현**:
|
||||||
|
- [`.agents/skills/multi-agent-mux-loop/scripts/loop_lock.sh`](file:///Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.agents/skills/multi-agent-mux-loop/scripts/loop_lock.sh): `set -C` 기반 원자적 락 획득, `pid` + `lstart` 신원 대조 검증 및 중복 루프 기동 차단 모듈 구현.
|
||||||
|
- [`.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh`](file:///Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh): `_mam_release_guard` 종료 트랩 시 `pid` + `lstart` 소유권 대조 검증 삭제 구현.
|
||||||
|
- `tests/test_o2_race_free_lock.py`: 전용 회귀 테스트 22개 작성 및 **22/22 PASS (100%)** 달성. 전체 회귀 테스트 **46/46 PASS (100%)**.
|
||||||
|
|
||||||
|
### 2) **P0-1 (B-7): `run_loop.sh` 루프 기동 외곽 diff 누락 및 신규 미추적 파일 캡처 결함 조치** — **완료**
|
||||||
- **배경**: CWD 의존성으로 인해 저장소 외곽에서 `run_loop.sh` 구동 시 `git diff` 실패 및 미추적 신규 파일(Untracked Files) 누락으로 리뷰어가 빈 diff 보고 무조건 `PASS`를 남기던 무음 검증 결함 조치.
|
- **배경**: CWD 의존성으로 인해 저장소 외곽에서 `run_loop.sh` 구동 시 `git diff` 실패 및 미추적 신규 파일(Untracked Files) 누락으로 리뷰어가 빈 diff 보고 무조건 `PASS`를 남기던 무음 검증 결함 조치.
|
||||||
- **주요 구현**:
|
- **주요 구현**:
|
||||||
- [`.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh`](file:///Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh): CWD 독립 `$REPO_ROOT` 이동 및 Git 인덱스 비침습 신규 파일 병합(`git ls-files -o --exclude-standard -z` + `git diff --no-index`) 구현.
|
- [`.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh`](file:///Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh): CWD 독립 `$REPO_ROOT` 이동 및 Git 인덱스 비침습 신규 파일 병합(`git ls-files -o --exclude-standard -z` + `git diff --no-index`) 구현.
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ jobs:
|
|||||||
shellcheck .agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh
|
shellcheck .agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh
|
||||||
shellcheck .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh
|
shellcheck .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh
|
||||||
shellcheck .agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh
|
shellcheck .agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh
|
||||||
|
shellcheck .agents/skills/multi-agent-mux-loop/scripts/loop_lock.sh
|
||||||
shellcheck .agents/hooks/loop_delegation_guard.sh
|
shellcheck .agents/hooks/loop_delegation_guard.sh
|
||||||
shellcheck deploy/lib_ownership.sh
|
shellcheck deploy/lib_ownership.sh
|
||||||
shellcheck deploy/install.sh
|
shellcheck deploy/install.sh
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).parent.parent
|
||||||
|
LOCK_SCRIPT = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "loop_lock.sh"
|
||||||
|
RUN_LOOP = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "run_loop.sh"
|
||||||
|
|
||||||
|
|
||||||
|
def run_lock_cmd(action, marker_path, env=None):
|
||||||
|
cmd = ["bash", str(LOCK_SCRIPT), action, str(marker_path)]
|
||||||
|
run_env = dict(os.environ)
|
||||||
|
if env:
|
||||||
|
run_env.update(env)
|
||||||
|
res = subprocess.run(cmd, capture_output=True, text=True, env=run_env)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def acquire_bg(marker_path, sleep_secs=10):
|
||||||
|
cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker_path}' && sleep {sleep_secs}"
|
||||||
|
proc = subprocess.Popen(["bash", "-c", cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
|
time.sleep(0.3)
|
||||||
|
return proc
|
||||||
|
|
||||||
|
|
||||||
|
def get_lstart(pid=None):
|
||||||
|
if pid is None:
|
||||||
|
pid = os.getpid()
|
||||||
|
res = subprocess.run(["ps", "-p", str(pid), "-o", "lstart="], capture_output=True, text=True)
|
||||||
|
return res.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_1_acquire_empty_marker(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
assert marker.is_file()
|
||||||
|
content = marker.read_text()
|
||||||
|
assert f"pid={proc.pid}" in content
|
||||||
|
assert "lstart=" in content
|
||||||
|
assert "started=" in content
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_2_live_holder_rejected(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
res = run_lock_cmd("acquire", marker)
|
||||||
|
assert res.returncode == 3
|
||||||
|
assert f"pid={proc.pid}" in marker.read_text()
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_3_dead_holder_reclaimed(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
marker.write_text("pid=999999\nlstart=Sat Jan 1 00:00:00 2000\nstarted=2000-01-01T00:00:00Z\n")
|
||||||
|
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
assert marker.is_file()
|
||||||
|
assert f"pid={proc.pid}" in marker.read_text()
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_4_pid_rollover_reclaimed(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
proc_dummy = subprocess.Popen(["sleep", "10"])
|
||||||
|
try:
|
||||||
|
# Same PID as proc_dummy, but lstart differs -> stale!
|
||||||
|
marker.write_text(f"pid={proc_dummy.pid}\nlstart=Sat Jan 1 00:00:00 2000\nstarted=2000-01-01T00:00:00Z\n")
|
||||||
|
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
assert marker.is_file()
|
||||||
|
assert f"pid={proc.pid}" in marker.read_text()
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
finally:
|
||||||
|
proc_dummy.kill()
|
||||||
|
proc_dummy.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_5_release_other_marker_fails(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
res = run_lock_cmd("release", marker)
|
||||||
|
assert res.returncode == 1
|
||||||
|
assert marker.is_file()
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_6_release_own_marker_succeeds(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}' && mam_release_loop_lock '{marker}'"
|
||||||
|
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
||||||
|
assert res.returncode == 0
|
||||||
|
assert not marker.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_7_no_leftover_temp_files(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}' && mam_release_loop_lock '{marker}'"
|
||||||
|
subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
||||||
|
|
||||||
|
files = list(tmp_path.glob("*"))
|
||||||
|
assert files == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_8_concurrent_acquisition_single_winner(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
script = f"""
|
||||||
|
source '{LOCK_SCRIPT}'
|
||||||
|
if mam_acquire_loop_lock '{marker}'; then
|
||||||
|
sleep 2
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
"""
|
||||||
|
procs = []
|
||||||
|
for _ in range(8):
|
||||||
|
p = subprocess.Popen(["bash", "-c", script], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
procs.append(p)
|
||||||
|
|
||||||
|
rcs = [p.wait() for p in procs]
|
||||||
|
winners = [rc for rc in rcs if rc == 0]
|
||||||
|
losers = [rc for rc in rcs if rc == 3]
|
||||||
|
|
||||||
|
assert len(winners) == 1
|
||||||
|
assert len(losers) == 7
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_9_marker_never_observed_partial(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
content = marker.read_text()
|
||||||
|
assert content.startswith("pid=")
|
||||||
|
assert "lstart=" in content
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_10_ps_unavailable_fails_safe(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
cmd = f"export PATH=/bin/nonexistent:$PATH && source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}'"
|
||||||
|
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
||||||
|
assert res.returncode in (3, 4)
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_11_legacy_marker_respected(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
proc_dummy = subprocess.Popen(["sleep", "10"])
|
||||||
|
try:
|
||||||
|
marker.write_text(f"pid={proc_dummy.pid}\n")
|
||||||
|
|
||||||
|
cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}'"
|
||||||
|
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
||||||
|
assert res.returncode == 3
|
||||||
|
finally:
|
||||||
|
proc_dummy.kill()
|
||||||
|
proc_dummy.wait()
|
||||||
|
|
||||||
|
|
||||||
|
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"]
|
||||||
|
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)
|
||||||
|
assert res.returncode != 0
|
||||||
|
assert "Another multi-agent-mux-loop is already running" in res.stdout or "Another multi-agent-mux-loop is already running" in res.stderr
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_13_o3_guard_compatibility(mam_sandbox):
|
||||||
|
marker = mam_sandbox / ".mam" / "loop-guard-active"
|
||||||
|
marker.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
guard_script = REPO_ROOT / ".agents" / "hooks" / "loop_delegation_guard.sh"
|
||||||
|
payload = f'{{"toolCall":{{"name":"file_change"}},"workspacePaths":["{mam_sandbox}"]}}'
|
||||||
|
res = subprocess.run(["bash", str(guard_script)], input=payload, capture_output=True, text=True, cwd=str(mam_sandbox))
|
||||||
|
assert res.returncode == 0
|
||||||
|
assert '"decision":"deny"' in res.stdout.replace(" ", "")
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_14_bash_syntax_clean():
|
||||||
|
res1 = subprocess.run(["bash", "-n", str(LOCK_SCRIPT)], capture_output=True)
|
||||||
|
res2 = subprocess.run(["bash", "-n", str(RUN_LOOP)], capture_output=True)
|
||||||
|
assert res1.returncode == 0, res1.stderr
|
||||||
|
assert res2.returncode == 0, res2.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_15_rejected_loop_preserves_holder_marker(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
holder_content = marker.read_text()
|
||||||
|
|
||||||
|
# Subshell attempt that gets rejected
|
||||||
|
cmd = f"source '{LOCK_SCRIPT}' && (mam_acquire_loop_lock '{marker}' || true) && mam_release_loop_lock '{marker}'"
|
||||||
|
subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert marker.is_file()
|
||||||
|
assert marker.read_text() == holder_content
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_16_concurrent_stale_recovery(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
marker.write_text("pid=999999\nlstart=Sat Jan 1 00:00:00 2000\n")
|
||||||
|
|
||||||
|
script = f"""
|
||||||
|
source '{LOCK_SCRIPT}'
|
||||||
|
if mam_acquire_loop_lock '{marker}'; then
|
||||||
|
sleep 2
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
"""
|
||||||
|
procs = []
|
||||||
|
for _ in range(6):
|
||||||
|
p = subprocess.Popen(["bash", "-c", script], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
procs.append(p)
|
||||||
|
|
||||||
|
rcs = [p.wait() for p in procs]
|
||||||
|
winners = [rc for rc in rcs if rc == 0]
|
||||||
|
assert len(winners) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_17_staggered_stale_recovery(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
marker.write_text("pid=999999\nlstart=Sat Jan 1 00:00:00 2000\n")
|
||||||
|
|
||||||
|
procs = []
|
||||||
|
for i in range(5):
|
||||||
|
script = f"""
|
||||||
|
sleep 0.05
|
||||||
|
source '{LOCK_SCRIPT}'
|
||||||
|
if mam_acquire_loop_lock '{marker}'; then
|
||||||
|
sleep 2
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
"""
|
||||||
|
p = subprocess.Popen(["bash", "-c", script], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
procs.append(p)
|
||||||
|
|
||||||
|
rcs = [p.wait() for p in procs]
|
||||||
|
winners = [rc for rc in rcs if rc == 0]
|
||||||
|
assert len(winners) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_18_orphan_steal_lock_recovered(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
marker.write_text("pid=999999\nlstart=Sat Jan 1 00:00:00 2000\n")
|
||||||
|
|
||||||
|
steal_lock = tmp_path / "loop-guard-active.steal"
|
||||||
|
steal_lock.write_text("pid=999998\nlstart=Sat Jan 1 00:00:00 2000\n")
|
||||||
|
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
assert marker.is_file()
|
||||||
|
assert f"pid={proc.pid}" in marker.read_text()
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_19_active_steal_in_progress_not_displaced(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
marker.write_text("pid=999999\nlstart=Sat Jan 1 00:00:00 2000\n")
|
||||||
|
steal_lock = tmp_path / "loop-guard-active.steal"
|
||||||
|
|
||||||
|
proc_steal = acquire_bg(steal_lock)
|
||||||
|
try:
|
||||||
|
cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}'"
|
||||||
|
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
||||||
|
assert res.returncode == 3
|
||||||
|
finally:
|
||||||
|
proc_steal.kill()
|
||||||
|
proc_steal.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_20_contentless_steal_lock_reclaimed(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
marker.write_text("pid=999999\nlstart=Sat Jan 1 00:00:00 2000\n")
|
||||||
|
|
||||||
|
steal_lock = tmp_path / "loop-guard-active.steal"
|
||||||
|
steal_lock.write_text("")
|
||||||
|
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
assert marker.is_file()
|
||||||
|
assert f"pid={proc.pid}" in marker.read_text()
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_21_legacy_steal_directory_cleaned(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
steal_dir = tmp_path / "loop-guard-active.steal"
|
||||||
|
steal_dir.mkdir()
|
||||||
|
|
||||||
|
proc = acquire_bg(marker)
|
||||||
|
try:
|
||||||
|
assert marker.is_file()
|
||||||
|
assert not steal_dir.exists()
|
||||||
|
finally:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def test_o2_22_repeated_invocations_converge(tmp_path):
|
||||||
|
marker = tmp_path / "loop-guard-active"
|
||||||
|
for i in range(5):
|
||||||
|
cmd = f"source '{LOCK_SCRIPT}' && mam_acquire_loop_lock '{marker}' && mam_release_loop_lock '{marker}'"
|
||||||
|
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
||||||
|
assert res.returncode == 0
|
||||||
|
assert not marker.exists()
|
||||||
@@ -206,9 +206,9 @@ def test_z13_legacy_marker_permission_error_degrades_open(mam_sandbox):
|
|||||||
assert res["decision"] == "allow"
|
assert res["decision"] == "allow"
|
||||||
|
|
||||||
|
|
||||||
# Z-14: run_loop.sh records lstart in marker
|
# Z-14: loop_lock.sh records lstart in marker
|
||||||
def test_z14_run_loop_records_lstart(mam_sandbox):
|
def test_z14_run_loop_records_lstart(mam_sandbox):
|
||||||
run_loop = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "run_loop.sh"
|
lock_script = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "loop_lock.sh"
|
||||||
content = run_loop.read_text()
|
content = lock_script.read_text()
|
||||||
assert "_mam_lstart" in content
|
assert "mam_lstart" in content
|
||||||
assert "lstart=" in content
|
assert "lstart=" in content
|
||||||
|
|||||||
Reference in New Issue
Block a user