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
141 lines
16 KiB
Markdown
141 lines
16 KiB
Markdown
# 🛠️ 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
|
||
```
|