PR #41/#43 review follow-ups F1-F6, implemented by creator-claude-01 in a
loop and reviewed by claude, agy, grok and codex: round 1 (@95eabc6)
codex NOT PASS (a failed env load cached as loaded), fixed in 75f364e;
round 2 4/4 PASS. Full suite at 75f364e 1380 passed / 0 failed. Reviewer
reports (round 2 with identity sidecars) and the creator's reports go to
.agents/reports/<session>/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
16 KiB
🛠️ Creator 리포트 — PR #41·#43 후속 F1~F6 (job aa7f2b5f)
- Author:
creator-claude-01(role: creator) · 2026-10-02 - 작업 트리:
/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/followups· 브랜치fix/followups-41-43· base0051fd6→ head95eabc6(커밋 8개, 미커밋 변경 없음, ignored 는.venv/뿐) - 범위 준수: 본 체크아웃(
main @ 0051fd6)은 읽기만 했다. push·merge·PR·이슈 변경 없음, 버전 bump 없음(4.8.1 그대로). #17 항목 4·#39·#40·#42 범위는 건드리지 않았다. 실제.mam(이 리포트 파일 제외)·.mam.env(읽지도 출력하지도 않음)·모니터·에이전트 세션·herdr 서버는 손대지 않았고.mam.env는 export 하지 않았다(테스트는 conftest 의 로컬 브로커). basetemp·JUnit·로그·역검증용 임시 워크트리는 모두 작업 트리 밖 scratchpad 에 두었고 임시 워크트리는 제거했다.
1. 커밋
| 커밋 | 항목 | 요지 |
|---|---|---|
75936f1 |
F1 | A8 이 마커 lstart 를 loop_lock.sh mam_lstart 와 같은 정규화(공백 압축·양끝 trim)로 기록. 두 칸 공백 ps 심 변형으로 날짜와 무관하게 고정. 제품 코드 무변경 |
5edf913 |
F2 | mqtt_common.load_env_or_report() — publish_event·registry·job_subscriber·cli·bench·heartbeat·remote 의 main 이 ERROR: <경로>: cannot read (<예외 종류>) 한 줄 + rc 1. ensure_env_loaded() 는 여전히 예외(라이브러리는 exit 하지 않음) |
95eabc6 |
F2 보정 | job_subscriber 는 종료 코드를 이름으로 돌려준다(test_issue5 스냅숏): return RC_ERROR(값 1 그대로) |
71ea809 |
F3 | (a) 공통 파서를 utf-8-sig 로(BOM 제거, 두 로더 공통) (b) 중복 키 첫 값 우선을 .mam.env.example FORMAT·envfile.py 독스트링에 명시(동작 그대로) (c) MESSAGING.md:369 를 실제 동작(없으면 조용히 건너뜀·주변 탐색 안 함, 있는데 못 읽으면 EnvFileError → ERROR 한 줄 rc 1)으로 |
8b04639 |
F4a·F4c | 러너 _int_knob = _positive_int 규칙 + 잘못된 값 경고 1회 후 기본값. loop SKILL.md 예산 행·하위 잡 문단에 MAM_WAIT_GRACE_S 와 정수 규칙 |
fb1b93c |
F4b | bash 노브 8개를 mam_int_knob 으로. mam_int/mam_int_knob 를 lib_int.sh 로 그대로 이동(lib.sh 가 source, lib.sh 없이 도는 loop_lock.sh·diff_collect.sh 는 함수가 없을 때 직접 source) |
c252ac4 |
F5 | stall_watch.py 의 MAM_TEST_UNRELIABLE_AGENT 분기 삭제, S13 은 어댑터 seam 으로 |
1e92ddc |
F6 | delegate-job SKILL.md <= 60 s 를 herdr wait 에 한정, "대기 중 래퍼 소멸" 테스트를 두 번째 wait 안에서 소멸하도록 |
2. 항목별 내용과 완료 조건
F1 — 날짜 의존 A8
- 원인:
ps -o lstart=는 한 자리 날짜를Oct 2로 두 칸 패딩한다. 제품mam_lstart는tr -s ' '로 줄여 비교하는데 A8 은.strip()만 한 원값을 기록 → 살아 있는 holder 가 stale 로 보여 두 번째 루프가 거부되지 않음(매달 1~9일). - 수정(
tests/test_issue21_acceptance.py):_marker_lstart()가" ".join(out.split())(=tr -s ' '+ trim)로 기록.@pytest.mark.parametrize("ps", ["real", "padded"])—padded는 PATH 앞의ps심이 lstart 요청에 항상Thu Oct 1 08:02:03 2026을 돌려주므로 날짜와 무관하게 두 칸 공백 경로를 지난다(심이 실제로 쓰였는지Thu Oct 1 …로 확인). - 같은 패턴 조사: 살아 있는 lstart 를 마커에 쓰는 테스트는
test_o3_scoped_guard._get_lstart(이미" ".join(split()))뿐.test_o2_race_free_lock의 마커는 고정된 stale 값이고, 그 파일의get_lstart()는 정의만 있고 쓰이지 않는다(죽은 코드 — 손대지 않음). 제품 쪽 다른 소비자:loop_delegation_guard.sh는 양쪽을 split-join,lib.sh의ps -o pid=,lstart=는strptime이라 두 칸에 무관.
F2 — Python 진입점의 EnvFileError
mqtt_common.load_env_or_report() -> bool:ensure_env_loaded()를 부르고EnvFileError면print(f"ERROR: {e}", file=sys.stderr)후 False. 각 main 은if not …: return 1(job_subscriber 는RC_ERROR). 메시지는envfile의"{path}: cannot read ({type})"그대로라 값·내용이 들어가지 않는다.- 범위: 브리프가 든 7개 main. reconcile.sh 의 임베디드 구독자도
ensure_env_loaded()를 부르지만 main 이 아니라 목록 밖이라 그대로 두었다(트레이스백 유지) — 필요하면 후속. - 리뷰어 참고: job_subscriber 의 rc 1 은 원래 "감시한 잡이 error 로 끝남"(
RC_ERROR)과 같은 값이다. 수정 전에도 트레이스백으로 rc 1 이었으므로 값은 바뀌지 않았고, 래퍼는 같은 파일을 먼저 읽고 스스로 rc 1 로 끝나 이 구독자를 띄우지 않는다. 별도 코드(예: 3 fatal)로 바꾸는 것은 계약 변경이라 하지 않았다.
F3 — .mam.env 세부
- (a)
envfile.parse가encoding="utf-8-sig"— 파일 맨 앞 BOM 만 벗기며, 래퍼(envfile.py --shell)와mqtt_common이 같은 함수를 쓰므로 두 로더 공통. - (b)
apply()가 이미 있는 키를 건너뛰므로 첫 값이 이긴다(동작 무변경) — FORMAT 절에a key given twice keeps its FIRST value와 BOM 규칙 추가. - (c) MESSAGING 2번 항목을 실제로: 명시 파일이 없으면 조용히 건너뛰고 주변 탐색을 하지 않는다(환경변수와 기본값으로 동작), 있는데 못 읽으면
EnvFileError→ ERROR 한 줄 rc 1. 기존 문장의 "error is logged"·"fail-closed (RuntimeError)" 는 코드에 근거가 없어 지웠다. - 참고(범위 밖, 손대지 않음): 같은 블록 3번 항목의 탐색 순서(
MAM_REAL_ROOT·WORKSPACE_ROOT·cwd)는mqtt_common._load_dotenv(모듈 위치에서 위로.agents/.git탐색)와 다르다.
F4 — 정수 노브
-
(a)
runner._int_knob(name, minimum)→ None(미설정·잘못된 값, 후자는 노브당 경고 1회) 이면 호출부 기본값. 최솟값과 근거(tests/test_issue26_ints.pyRUNNER_INT_KNOBS):노브 최솟값 기본 근거 MAM_WAIT_GRACE_S0 300 0 = 예산만큼만 대기 MAM_CHILD_WAIT_S0 예산 + grace 0 = 하위 잡을 한 번만 확인(test_issue34 가 0 사용) MAM_JOB_TREE_MAX1 50 0 은 모든 트리를 "over" 로 만든다; 기다리지 않기는 MAM_CHILD_WAIT_S=0동작 변화:
MAM_CHILD_WAIT_S는 전에float()이라0.5·1e3을 받았고abc는 ValueError,MAM_JOB_TREE_MAX는abc·빈 값이 ValueError,MAM_WAIT_GRACE_S는" 30 "도 조용히 300 이었다 → 이제 모두 10진 정수 규칙(경고 후 기본값). -
(b) bash 노브(
INT_KNOBS에 추가,NOT_ARITHMETIC에서 제거):노브 위치 최솟값 기본 근거 MAM_READY_EMPTY_GIVEUPlib.sh wait_for_tui_ready1 5 0 은 1 과 같게 동작 SKS_EMPTY_GIVEUPlib.sh _pane_quiescent1 3 같음 SKS_IDLE_RETRIESlib.sh wait_for_agent_idle0 2 0 = 재시도 없음 SKS_IDLE_RETRY_BACKOFF_S같음(sleep) 0 10 test_issue6 가 0 사용; 소수 사용처 없음 MAM_LOCK_STEAL_RETRIESloop_lock.sh 0 3 0 = 한 번 시도 MAM_DIFF_MAX_BYTES/_LINESdiff_collect.sh 1 200000/4000 0 은 모든 diff 를 자름 RECONCILE_POLL_INTERVALreconcile.sh(sleep) 1 15 0 은 바쁜 루프; 소수 사용처 없음 SKS_QUIESCENT_INTERVAL은 기본값이 0.5 초라 소수가 실제 의미 → 그대로 두고NOT_ARITHMETIC에 "sleep, fractional by default (0.5) - left raw" 로 사유를 남겼다. -
lib_int.sh를 만든 이유:diff_collect.sh는 Python 러너가 lib.sh 없이source하고(runner.collect_changes_diff),loop_lock.sh도 테스트가 단독 source 한다. lib.sh 를 source 하면 herdr 심 설치 등 부작용이 있어 함수만 분리했다. 함수 본문은 한 글자도 바꾸지 않았다. 연쇄 변경:deploy/remove.sh의 매니페스트 없는 폴백 목록에lib_int.sh추가(설치는.agents/skills의 모든 파일을 복사하므로 그대로 배포됨),test_issue21_loop_b::l15가 가짜 제어 루트에diff_collect.sh만 두던 것을lib_int.sh도 함께 두게. -
(c) loop SKILL.md: 예산 행에
MAM_WAIT_GRACE_S(기본 300, 0 이상, 잘못된 값 경고 후 300, 덮는 꼬리 설명), 하위 잡 문단의MAM_CHILD_WAIT_S기본을 "예산 +MAM_WAIT_GRACE_S", 0 의 의미,MAM_JOB_TREE_MAX1 이상, 정수 규칙.
F5 — 제품 코드의 테스트 훅
StallWatch.__init__의MAM_TEST_UNRELIABLE_AGENT두 줄 삭제(이제.agents/skills전체에MAM_TEST_없음).- S13:
_watch(..., unreliable=True)가python -c _UNRELIABLE_MAIN claude <args>로 실제stall_watch.main()을 실행하되, 그 프로세스에서type(get_adapter("claude")).agent_status_reliable = property(lambda self: False). 검증 내용은 그대로(5 초 동안 idle 알림·nudge 없음 → blocked 알림stalled:blocked Do you want to proceed?→ 로그idle judgement off for claude).
F6 — 문서 정밀도와 "대기 중 래퍼 소멸"
- SKILL.md: "한 herdr wait(<= 60 s) — 또는 알림 발행 중에 래퍼가 사라지면 그 발행이 끝날 때까지" 로 한정.
- 테스트:
_during_notice에in_wait(tick)훅(각 가짜 herdr wait 안에서 호출). 래퍼는 두 번째 wait 안에서 사라지고, 테스트는ticks == [1, 2](두 번째 wait 에 실제로 들어감)와 그때까지 alive 가 참이었음을 확인한 뒤 알림·키 0 을 단언한다.
3. 회귀 테스트가 수정 전 코드에서 실패함을 확인한 방법
git worktree add --detach <scratch>/base 0051fd6(작업 트리 밖)에 새 테스트 파일을 복사해 돌리고, 확인 후 git checkout 으로 되돌렸다. 마지막에 git worktree remove --force 로 제거(남은 워크트리 2개: 본 체크아웃·followups).
| 항목 | base(0051fd6)에서 |
수정 후 |
|---|---|---|
| F1 | 원래 A8: 실패(오늘 10월 2일, already running 없음). 새 테스트에서 _marker_lstart 를 옛 .strip() 으로 되돌리면 real·padded 둘 다 실패 — padded 는 날짜와 무관 |
2 passed |
| F2 | 14 케이스(7 진입점 × 000 권한·UTF-8 깨짐) 14 실패(트레이스백) | 14 passed |
| F3 | A7 3개 3 실패: BOM 이 FIRST 를 버림 / FORMAT 문구 없음 / MESSAGING 의 옛 문장 |
3 passed |
| F4a | 새 케이스 중 15 실패: grace 의 잘못된 값 6(경고 없이 300, " 30 " 은 300), child-wait abc·-5·0.5·9999999999 4(ValueError 또는 받아들임), tree-max 0·abc·-1·빈 값 4(ValueError 또는 0 수용), 정적 검사 1. 통과한 14 는 결과가 같은 유효 값 |
44 passed |
| F4b | 14 실패: 표 정적 검사 2, lib.sh 없이 source 한 스크립트 11(abc·0900·" 7 "·-1·1.5, diff 는 0 도), _pane_quiescent + SKS_EMPTY_GIVEUP=abc([ 오류) 1. 통과 3 은 유효 값 |
통과 |
| F5 | 정적 검사(MAM_TEST_ 없음) 실패. 추가로 수정 후 트리에서 seam 을 빼면(unreliable=True 제거) S13 이 실패 → seam 이 unreliable 상황을 만든다는 증거 |
2 passed |
| F6 | 테스트만 바뀐 항목이라 두 가지로 확인: (1) 새 테스트에 옛 호출 횟수 대역을 넣으면 assert [1] == [1, 2] 로 실패 — 옛 테스트는 두 번째 wait 에 들어가지도 않고 루프 머리에서 끝났다. (2) 제품의 wait 직후 _wrapper_gone() 검사를 지운 변이체에서 새 테스트가 notes == ['stalled:idle'] 로 실패(수정 안 한 제품에서는 통과) |
1 passed |
4. 테스트 결과
명령(브리프 그대로, .mam.env export 없음):
AGENT_PYTHON_BIN=<wt>/.venv/bin/python PYTHONDONTWRITEBYTECODE=1 <wt>/.venv/bin/python -m pytest -p no:cacheprovider -q --basetemp=<scratch>/bt-final --junitxml=<scratch>/final.xml <파일들>
최종 실행 @ 1e92ddc — 605 passed, 1 failed, 2 skipped (885 s). 실패 1건은 내 F2 회귀였다(test_issue5_phase1_delegation::test_job_subscriber_cli_contract_snapshot — job_subscriber 의 맨 숫자 return 1). 95eabc6 로 고친 뒤 test_issue5_phase1_delegation.py + test_issue26_envfile.py 재실행 103 passed. 95eabc6 의 변경은 같은 값(1)을 이름(RC_ERROR)으로 바꾼 것뿐이고 그 분기는 env 파일 실패에서만 탄다.
| 파일 | 통과 | 실패 | 스킵 |
|---|---|---|---|
| test_issue26_envfile.py | 37 | 0 | 0 |
| test_issue26_ints.py | 93 | 0 | 0 |
| test_issue21_acceptance.py | 10 | 0 | 0 |
| test_issue30_stall_watch.py | 48 | 0 | 1 (/proc 전용) |
| test_issue30_stall_watch_stop.py | 11 | 0 | 0 |
| test_tier1_unit.py | 67 | 0 | 0 |
| test_version_consistency.py | 3 | 0 | 0 |
| test_issue33_budget.py | 20 | 0 | 0 |
| test_issue34_job_tree.py | 20 | 0 | 0 |
| test_issue21_loop_b.py | 34 | 0 | 0 |
| test_issue5_phase1_delegation.py | 65 → 재실행 66 | 1 → 0 | 0 |
| test_issue18_liveness_contract.py | 31 | 0 | 0 |
| test_o2_race_free_lock.py | 22 | 0 | 0 |
| test_o3_scoped_guard.py | 32 | 0 | 0 |
| test_b7_diff_untracked.py | 20 | 0 | 0 |
| test_issue36_prompt_transport.py | 23 | 0 | 1 (Linux MAX_ARG_STRLEN) |
| test_issue6_turn_handshake.py | 14 | 0 | 0 |
| test_c1_tui_readiness.py | 23 | 0 | 0 |
| test_b19_headless_reconcile_fixes.py | 16 | 0 | 0 |
| test_issue19_subscriber_daemon_lifecycle.py | 16 | 0 | 0 |
브리프 지정 7개 파일 + 바꾼 모듈의 테스트(러너: issue33/34·loop_b, env/진입점: issue5·issue18, bash 노브: o2·o3·b7·issue36·issue6·c1·b19·issue19). 전체 스위트(~60분)는 돌리지 않았다(오케스트레이터 몫). 정적 검사: bash -n 으로 바꾼 셸 파일 6개 문법 OK; shellcheck·pyflakes 는 이 호스트에 없다.
5. 위생
- 작업 트리: 최종
git status --short --ignored=!! .venv/만. 단, 최종 실행 중.mam/shim/herdr가 작업 트리에 생겼다 → 파일별로 나눠 돌려 원인을 찾았다:tests/test_version_consistency.py::_get_lib_sh_mam_version이WORKSPACE_ROOT없이lib.sh를 source →_init_herdr_isolation이<저장소>/.mam/shim/herdr를 쓴다. base 에서도 똑같이 생긴다(내 변경과 무관, 기존 동작). 내용이 재생성되는 심 하나뿐임을 확인하고 지웠다. ⚠️ 본 체크아웃에서 이 테스트를 돌리면 실제.mam/shim/herdr를 덮어쓴다 — 후속 후보(테스트가WORKSPACE_ROOT를 샌드박스로 주기). - 프로세스: 내 F5 확인(seam 을 일부러 뺀 S13 실패 실행)에서 샌드박스 stall_watch 1개가 남았다(S13 은 단언 실패 시
finally에서 에이전트만 죽이고 watcherw는 정리하지 않음 — 기존 테스트 구조). 그 watcher 는 basetemp 의 mock herdr·레지스트리만 상대했다. TERM 으로 종료했고, 이후 scratchpad 경로를 가진 프로세스 0개. 실제 모니터 데몬·세션은 건드리지 않았다(남은-m lib_py.loop1개는 오케스트레이터의 루프 드라이버). - 실제
.mam/jobs의 레코드는 읽기·쓰기 하지 않았다(이 리포트 파일만 작성).
6. 리뷰어 참고
lib.sh변경은mam_int/mam_int_knob블록을lib_int.sh로 옮기고 그 자리에서source "$(dirname "${BASH_SOURCE[0]}")/lib_int.sh". 생성되는 herdr 심(heredoc, 151–1367행)은mam_int를 쓰지 않는다.local x="$(mam_int_knob …)"형태는 B5 정적 검사 정규식(mam_int_knob NAME default min))이 잡도록 기존 호출부 모양을 따랐다. B5 가 이제 래퍼와 모든.sh를 읽는다.- 메모: 오래된
.mam.envexport 지침(공개 브로커 시절)은 이 트리에서 틀리다 — conftest 가 세션 로컬 브로커를 띄우고 인증 변수를 지운다.
7. 교차 확인 명령
cd /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/followups
git log --oneline 0051fd6..HEAD # 8 commits, head 95eabc6
git diff --stat 0051fd6..HEAD
grep -rn MAM_TEST_ .agents/skills # nothing
AGENT_PYTHON_BIN=$PWD/.venv/bin/python PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -m pytest -p no:cacheprovider -q \
--basetemp=/tmp/<somewhere> tests/test_issue26_envfile.py tests/test_issue26_ints.py tests/test_issue21_acceptance.py \
tests/test_issue30_stall_watch.py tests/test_issue30_stall_watch_stop.py tests/test_tier1_unit.py tests/test_version_consistency.py