docs(issue-5): phase 2a contracts, scenarios and task checklist

Records what phase 2a actually delivers and, as importantly, what it does not:
the shadow runner decides and logs, and nothing commits an inferred completion
until 2b.

state-transitions.md gains an implementation mapping so each contract row names
the code and the test that hold it up, with the inferred-completed row marked as
judged-but-not-committed.

phase2a-shadow.md is the operations note: the rollback switch, the lock
discipline and why transition_seq exists, the herdr wait rules that are easy to
get wrong (milliseconds, always name --until, errors on stderr, a timeout is a
healthy answer), the fail-closed decision order, and the 2b handoff.

task.md records P2A-00..04 with commit evidence, including two corrections to my
own plan: pick_pending was already a correct swap rather than a hole, and the
engine had to live with the registry writers because putting it in lib_py/loop
would have made delegation import a sibling and mix trees under a frozen loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-06 18:58:01 +09:00
1 parent 964abc6965
commit 17a2dcd988
4 files changed
+95 -9

No files matched your search

@@ -23,7 +23,7 @@
|---|---|---|---|---|
| **Phase 0**<br>*(현재)* | **사전 계약 고정 및 Fixture 수집** | • CLI, 이벤트, 리포트, 락 계약 기준선 문서화<br>• Herdr 0.8.2 네이티브 캡처 및 복합 Watcher 시나리오 2계층 분리<br>• 6개 백엔드 실측 증거 행렬 수집 | • 510개 전체 테스트 수집 및 회귀 0-Fail (100% 통과)<br>• Fixture 스키마 및 증거 경계 검증 통과<br>• 프로덕션 코드 무변경 diff 검증 | 미추적 산출물 즉시 삭제 (`rm -rf docs/contracts/issue-5 tests/fixtures/issue_5 tests/test_issue5_phase0_contracts.py`) |
| **Phase 1**<br>*(완료 v4.3.0)* | **`lib_py/delegation/` 추출** | • `lib.sh` L1833 이하 위임 헬퍼를 파이썬 서비스 클래스로 추출<br>• `start_watchdog`을 내부 명칭 `ensure_subscriber_daemon`으로 명명(기존 함수 별칭 보존)<br>• 장수 프로세스 단일 MQTT 연결 재사용 프로토타입 | • CLI/non-fatal/HMAC/seq/디스크 폴백 회귀 100% 통과<br>• 데몬 수명 및 PID 계약 일치 확인<br>• **자동완료 정책 변경 없음** | `MAM_DELEGATION_PY=0`으로 보존된 레거시 인라인 본체(`_*_legacy`)로 즉시 분기 (재배포·revert 불필요; 레거시 본체 제거는 Phase 4) |
| **Phase 2a** | **`lib_py/loop/` 코어 & Shadow Watcher** | • `engine.py` (단일 전이 주체), `watcher.py` (`herdr agent wait` 래핑) 구현<br>• 모든 Writer에 대한 단일 CAS 트랜잭션 경계 구현<br>• 자동완료를 수행하지 않고 "Would-Complete" 로깅만 수행하는 Shadow 모드 가동 | • 락 내 비동기/I/O 배제 검증<br>• 소켓 단절/스냅샷 재연결 백오프 검증<br>• Shadow 모드가 레지스트리 상태를 쓰지 않음을 검증 | 1) Shadow 모니터 프로세스 종료 및 인메모리 큐 폐기<br>2) CAS 서비스는 하위 호환 모드로 유지하거나 `MAM_ENGINE_CAS=0` 플래그로 비활성화하여 레거시 무조건 필드 병합(`update_job_status`)으로 복귀<br>3) 기존 셸 루프 상태 및 레코드 불변 유지 |
| **Phase 2a**<br>*(완료, shadow-only)* | **`lib_py/loop/` 코어 & Shadow Watcher** | • `engine.py` (단일 전이 주체), `watcher.py` (`herdr agent wait` 래핑) 구현<br>• 모든 Writer에 대한 단일 CAS 트랜잭션 경계 구현<br>• 자동완료를 수행하지 않고 "Would-Complete" 로깅만 수행하는 Shadow 모드 가동 | • 락 내 비동기/I/O 배제 검증<br>• 소켓 단절/스냅샷 재연결 백오프 검증<br>• Shadow 모드가 레지스트리 상태를 쓰지 않음을 검증 | 1) Shadow 모니터 프로세스 종료 및 인메모리 큐 폐기<br>2) CAS 서비스는 하위 호환 모드로 유지하거나 `MAM_ENGINE_CAS=0` 플래그로 비활성화하여 레거시 무조건 필드 병합(`update_job_status`)으로 복귀<br>3) 기존 셸 루프 상태 및 레코드 불변 유지 |
| **Phase 2b** | **이중 감지 자동완료 활성화 (Flag-Gated)** | • 4중 실행 귀속 + 리포트 확정 계약 + 단일 CAS 전이 게이트 연동<br>• Pending Notification 지연 알림 및 크래시 복구 구현<br>• 검증된 백엔드/작업에 한해 opt-in 플래그로 활성화 (기본값 OFF) | • 부수효과 작업 예외 및 무판정 fail-closed 테스트 완료<br>• 경쟁/재시작 시나리오 통과<br>• 플래그 OFF 시 기존 explicit event 경로 100% 보존 | 환경변수/플래그 `MAM_AUTO_COMPLETE=0` 설정; 이미 확정된 terminal 레코드는 보존하고 신규 자동완료만 중단 |
| **Phase 3** | **`run_loop.sh` Thin Wrapper 전환 및 전면 회귀** | • `run_loop.sh`를 인자 전달 래퍼(`exec python3 -m lib_py.loop "$@"`)로 전환<br>• 플래너/리뷰어/반론/승격 오케스트레이션 로직 완전 이식<br>• 엄격한 판정 파서(Strict Parser) 적용 | • 5개 subprocess 전용 테스트(~1,621줄) 포함 전체 481개+ 테스트 0-Fail<br>• 시그널, 루프 가드, 임의 CWD 및 공백 경로 검증 통과 | **우아한 핸드오프 시퀀스**:<br>1) Quiesce: 실행 중인 Python 루프 프로세스에 종료 신호를 전달하여 신규 디스패치 중단<br>2) Checkpoint: 현재 루프 회차, 활성 Job ID 및 리뷰 상태를 `.mam/loop-checkpoint.json`에 원자적 기록<br>3) Exit: Python 루프 정상 종료 및 가드 해제<br>4) Resume: 백업된 셸 원본 스크립트로 `run_loop.sh` 복원 후 체크포인트 기반 재개 검증 |
| **Phase 4** | **불변 런타임 검증 및 Freeze 제거** | • 기동 시 전체 모듈 상주(Eager Import) 및 자식 템플릿 불변성 검증<br>• 작업 도중 스킬 파일 교체 및 늦은 경로 스트레스 테스트 통과<br>• `/tmp/mam-loop-freeze.XXXXXX` 우회책 완전 제거 | • 런타임 자기수정 크래시(B-13) 재발 방지 검증<br>• Live Helper 버전 호환 게이트 검증 통과<br>• 운영 문서 및 배포 스크립트 갱신 | Freeze 복사 로직(`run_loop.sh:112-127`) 재활성화 및 라이브 헬퍼 버전 검사 복구 |
+63
View File
@@ -0,0 +1,63 @@
# 📑 Issue #5 Phase 2a — Shadow Watcher 운영 노트
- **문서 버전**: Rev.1
- **상태**: Phase 2a 완료 (**shadow-only**: 레지스트리 무기록)
---
## 1. 이 단계가 하는 일과 하지 않는 일
| 한다 | 하지 않는다 |
|---|---|
| `herdr agent wait`로 에이전트 상태를 관측 | 관측으로 상태를 바꾸는 일 |
| 증거를 정책에 넣어 "완료 후보인가"를 **판정** | 판정 결과를 레지스트리에 **커밋** |
| 판정을 `.mam/shadow/<job_id>.ndjson`에 기록 | job 레코드 필드를 추가/변경 |
| 4중 귀속 증거를 shadow 저장소에 보관 | 레코드 스키마 변경 (Phase 2b 결정) |
**자동완료는 Phase 2b다.** 본 단계의 `candidate_approved`는 "커밋했을 것"이라는 로그일 뿐이다.
## 2. 운영 스위치
| 변수 | 기본 | 효과 |
|---|---|---|
| `MAM_ENGINE_CAS` | `1` | `0`이면 `update_job_status`/`pick_pending`이 Phase 2a 이전의 보존된 본체로 분기한다. 재배포·revert 불필요. |
Shadow 러너는 애초에 쓰기 경로가 없으므로 **프로세스 종료만으로 흔적이 사라진다.** `.mam/shadow/`는 레지스트리와 분리되어 있어 삭제해도 job 상태에 영향이 없다.
## 3. 락 규율 (P2A-01)
```
with registry_lock: 판정 + 레코드 1회 쓰기 + 알림 큐잉 ← 블로킹 호출 금지
락 해제 후: 감사 로그 flush, 발행 등 느린 작업
```
감사 mirror가 락 밖으로 나가면서 순서 보장을 잃을 수 있으므로, 전이마다 단조 증가하는 `transition_seq`를 레코드와 알림에 함께 실어 늦게 도착한 flush도 순서를 복원할 수 있게 했다. 이 형태는 새로 만든 것이 아니라 `pick_pending`이 이미 쓰던 패턴을 모든 전이로 일반화한 것이다.
## 4. `herdr agent wait` 사용 규칙
- `--timeout`은 **밀리초**다. 코드의 파라미터명은 `timeout_ms`이며 초 단위 형태는 제공하지 않는다.
- `--until`은 **항상 명시**한다. 생략 시 기본 매칭이 `idle,done,blocked`라 `working`이 빠진다.
- `exit 1`의 페이로드는 **stderr**에 있다. stdout은 비어 있으므로 stdout에서 에러를 읽으려 하면 안 된다.
- **타임아웃은 건강한 응답이다.** 소켓이 답했고 상태가 안 바뀐 것뿐이다. 전송 실패와 구분해야 한다.
- 전송 실패 시 백오프는 1→2→4→8→16초 상한이며, **캐시된 스냅샷을 폐기**한다. 단절 구간 이전의 상태로 추론하면 단절이 완료처럼 보인다.
## 5. 판정 순서 (fail-closed)
1. 전송 이상 → `hold_reconnect` (증거 자체가 무효)
2. 다른 세대/다른 pane 신호 → `reject_stale`
3. `blocked` → `hold_blocked` (사람 대기 중 완료 금지)
4. 4중 귀속 + 실행 앵커 미충족 → `hold`
5. 비-quiescent 또는 도구 실행 중 → `hold`
6. 리포트 미확정(매니페스트/해시) → `hold`
7. 전부 통과 → `candidate_approved`
## 6. 시나리오 계약
Phase 0의 7종 + Phase 2a의 5종 = **12종**이 `expected_policy.decision`으로 판정을 고정한다. 신규 5종도 Phase 0의 `validate_watcher_scenario`를 통과해야 하므로 증거 스키마가 계속 강제된다.
## 7. Phase 2b 인계
1. `attempt_id` 등 귀속 필드를 레코드로 승격할지 결정 (현재는 shadow 저장소).
2. `policy.decide`의 `candidate_approved`를 `engine.transition("completed", expected_attempt=...)`에 연결. 엔진과 CAS 표는 이미 준비되어 있다.
3. Pending Notification 지연 알림·크래시 복구 (`state-transitions.md` §5).
4. `preexec_fn`: Phase 2a는 데몬을 스폰하지 않아 미노출. 2b가 스폰을 도입하면 스레드 안전한 방식으로 교체해야 한다.
+15 -1
View File
@@ -1,6 +1,6 @@
# 📑 Target State Transitions and CAS Specification
- **Document Version**: Rev.2
- **Document Version**: Rev.3 (Phase 2a 구현 매핑 추가)
- **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
- **Scope**: Target Compare-And-Swap (CAS) State Machine & Attribution Contract
@@ -52,6 +52,20 @@ $$\text{Attribution Tuple} = (\text{job\_id}, \text{attempt\_id}, \text{agent\_s
---
### 3-1. 구현 매핑 (Phase 2a)
| 계약 | 구현 | 테스트 |
|---|---|---|
| §3 CAS 전이표 | `lib_py/delegation/engine.py::_decide` (순수 함수, 락 안에서 호출) | `test_engine_*` |
| 단일 전이 주체 | `engine.transition` — 유일한 상태 writer. `update_job_status`·`pick_pending`이 이를 경유 | `test_g6_every_record_writer_*` |
| 락 내 I/O 배제 | 락 = 판정 + 레코드 1회 쓰기. 감사 mirror는 큐잉 후 락 밖 flush | `test_g5_no_blocking_io_*` |
| §2 4중 귀속 | `lib_py/loop/attribution.py::check` | `test_attribution_*` |
| 롤백 | `MAM_ENGINE_CAS=0` → 보존된 `_*_legacy` 본체 | 전 특성화 테스트 parametrize |
> 📌 **Phase 2a는 추론 완료를 커밋하지 않는다.** `inferred completed` 행은 `lib_py/loop/policy.py`가 `candidate_approved`로 **판정만** 하고, shadow 러너가 이를 로그로 남긴다. 실제 CAS 커밋 연동은 Phase 2b다.
---
## 4. 리포트 확정 단계 (Report Finalization Lifecycle)
자동완료 추론 시 파일 쓰기 도중 발생하는 불완전 리포트 채택을 방지하기 위해 3단계 확정 계약을 수립합니다:
+16 -7
View File
@@ -1,8 +1,8 @@
# 📑 Issue #5 Task Checklist & Verification Evidence
- **Document Version**: Rev.6 (Phase 1 실행 완료 — P1-01~P1-03 증거 기록)
- **Document Version**: Rev.7 (Phase 2a 실행 완료 — P2A-01~P2A-04 증거 기록)
- **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
- **Status**: Phase 0 Complete / **Phase 1 Complete (v4.3.0)** / Phase 2–4 Staged
- **Status**: Phase 0 Complete / **Phase 1 Complete (v4.3.0)** / **Phase 2a Complete (shadow-only)** / Phase 2b–4 Staged
---
@@ -73,11 +73,20 @@
---
## Phase 2a: `lib_py/loop/` 코어 & Shadow Watcher (후속)
- [ ] **P2A-01** 단일 engine/checkpoint와 모든 writer 공통 CAS 경계 구현.
- [ ] **P2A-02** 4중 귀속/dispatch 증거/재연결 resnapshot/timeout/queue cancellation 처리.
- [ ] **P2A-03** shadow-only watcher가 어떤 terminal도 쓰지 않음을 검증.
- [ ] **P2A-04** race/attempt 재사용/부분 report/도구 실행/blocked/unknown/재시작 fixture 시나리오 실행.
## Phase 2a: `lib_py/loop/` 코어 & Shadow Watcher (완료, shadow-only)
- [x] **P2A-00** 이동 전 writer 특성화 선행 (X-01~X-04, `MAM_ENGINE_CAS` 0/1 parametrize).
- *증거*: 커밋 `9389d7e`. X-02가 계획서의 초기 가정을 정정한다 — `pick_pending`은 락 안에서 `pending`을 필터하므로 **이미 올바른 CAS**이며, 문제는 정확성이 아니라 **중복**이다.
- [x] **P2A-01** 단일 전이 경계 구현 및 writer 2종 라우팅.
- *증거*: 커밋 `2532e78`. `engine.transition`이 유일한 상태 writer. `state-transitions.md` §3 표 전 항목(claim/비terminal/explicit terminal/멱등 재수신/상충 거부+감사/stale attempt/deadline) 테스트. `MAM_ENGINE_CAS=0` 레거시 본체 보존.
- *계획 대비 구조 정정*: 계획서는 `engine.py`를 `lib_py/loop/`에 두었으나, 그러면 `lib_py.delegation`이 **형제 패키지를 임포트**하게 되어 freeze 하에서 트리가 섞인다(Phase 1이 명문화한 규칙). Phase 1의 기존 가드 테스트가 이를 즉시 검출했고, 엔진을 레지스트리 writer들이 있는 `lib_py/delegation/`으로 옮겼다.
- *락 규율*: 락은 판정 + 레코드 1회 쓰기만 덮고, 감사 mirror는 큐잉 후 락 해제 뒤 flush한다. 순서는 `transition_seq`로 복원한다. 락 보유 중 `subprocess`/`socket`/`sleep` 호출 0건을 테스트로 감시한다.
- [x] **P2A-02** watcher·귀속·dispatch 증거·재연결 백오프.
- *증거*: 커밋 `b49af56`. `--timeout`은 **밀리초**(파라미터명 `timeout_ms`), `--until` 항상 명시(생략 시 `working` 누락), exit 1은 **stderr**에서 파싱. 백오프 1→2→4→8→16s 상한, 실패 시 캐시 스냅샷 폐기. 귀속 증거는 **레지스트리가 아닌 shadow 전용 저장소**(`.mam/shadow/`)에 둔다 — 레코드에 3개 필드가 전부 없고 Phase 1이 스키마를 동결했기 때문.
- [x] **P2A-03** shadow-only가 terminal을 쓰지 않음을 검증.
- *증거*: 커밋 `964abc6`. `ShadowRunner`는 engine 참조를 **갖지 않는다**(구조적 차단, AST 단정). 측정: 전체 시나리오 실행 전후 **레지스트리 디렉터리 전 파일 SHA-256 동일**.
- [x] **P2A-04** race/attempt 재사용/부분 report/도구 실행/blocked/unknown/재시작 시나리오.
- *증거*: 신규 5종(`race_double_dispatch`, `attempt_reuse`, `partial_report`, `unknown_status`, `restart_midflight`)을 Phase 0 스키마로 작성해 **Phase 0의 `validate_watcher_scenario`를 통과**시켰고, 기존 7종과 합쳐 **12종 전부 `expected_policy.decision`과 일치**함을 단정한다.
---