Files
multi-agent-mux/.agents/reports/creator-claude-01/report-aa7f2b5f.md
T
GodopuandClaude Opus 5.5 3563a6fc17 release(v4.8.2): bump framework and eight skills to 4.8.2 in lockstep; archive the follow-up loop's reports
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
2026-10-03 00:27:24 +09:00

16 KiB
Raw Blame History

🛠️ 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 · base 0051fd6 → head 95eabc6 (커밋 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.py RUNNER_INT_KNOBS):

    노브 최솟값 기본 근거
    MAM_WAIT_GRACE_S 0 300 0 = 예산만큼만 대기
    MAM_CHILD_WAIT_S 0 예산 + grace 0 = 하위 잡을 한 번만 확인(test_issue34 가 0 사용)
    MAM_JOB_TREE_MAX 1 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_GIVEUP lib.sh wait_for_tui_ready 1 5 0 은 1 과 같게 동작
    SKS_EMPTY_GIVEUP lib.sh _pane_quiescent 1 3 같음
    SKS_IDLE_RETRIES lib.sh wait_for_agent_idle 0 2 0 = 재시도 없음
    SKS_IDLE_RETRY_BACKOFF_S 같음(sleep) 0 10 test_issue6 가 0 사용; 소수 사용처 없음
    MAM_LOCK_STEAL_RETRIES loop_lock.sh 0 3 0 = 한 번 시도
    MAM_DIFF_MAX_BYTES/_LINES diff_collect.sh 1 200000/4000 0 은 모든 diff 를 자름
    RECONCILE_POLL_INTERVAL reconcile.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_MAX 1 이상, 정수 규칙.

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 에서 에이전트만 죽이고 watcher w 는 정리하지 않음 — 기존 테스트 구조). 그 watcher 는 basetemp 의 mock herdr·레지스트리만 상대했다. TERM 으로 종료했고, 이후 scratchpad 경로를 가진 프로세스 0개. 실제 모니터 데몬·세션은 건드리지 않았다(남은 -m lib_py.loop 1개는 오케스트레이터의 루프 드라이버).
  • 실제 .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.env export 지침(공개 브로커 시절)은 이 트리에서 틀리다 — 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