Files
multi-agent-mux/docs/contracts/issue-5/baseline.md
T
Godopu fc0bd96d8a test(issue-5): harden watcher evidence validation and add codex pass review report
- Enforce evidence-root confinement for capture_ref and 1:1 capture citation uniqueness
- Add pane identity and backend identity cross-validation
- Add bidirectional agreement checks between watcher/herdr manifests and scenario fixtures
- Expand contract test suite to 41 tests (100% pass)
- Document evidence confinement rules in herdr-runtime.md and update task.md
- Archive Codex 4th review report (Job 1bd561e6: [VERDICT: PASS])
2026-09-06 14:31:41 +09:00

142 lines
14 KiB
Markdown

# 📑 Issue #5 Baseline Contract Specification
- **Document Version**: Rev.3 (Post Cross-Review Corrections)
- **Issue**: [Gitea Issue #5 — `[Refactor] lib.sh 작업 위임 로직 및 run_loop.sh 오케스트레이터의 Python 모듈화 (lib_py 승격)`](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
- **Baseline Commit**: `47640e2fd7b108be29446a9384a343727e87edf8`
- **Environment Baseline**: Python 3.9.6, Herdr 0.8.2, macOS Darwin 24.3.0
- **Test Suite Baseline**: 469 tests collected initially; 510 tests collected with Phase 0 contract suite (+41 contract & mutation tests)
- **Status**: Phase 0 Baseline Contract (Observed vs. Proposed vs. Gap)
---
## 1. 개요 및 계약 정의 원칙
본 문서는 Issue #5(Python 모듈화 및 `lib_py` 승격)의 착수 기준선(Baseline)으로서, 현재 코드베이스(`run_loop.sh`, `lib.sh`, `delegate-job` 도구군)에 구현되어 동작 중인 CLI, 이벤트, 리포트, 락(Lock) 및 상태 전이 계약을 엄밀하게 분류하여 명문화합니다.
각 항목은 다음 세 범주로 구분하여 기록합니다:
- **`[Observed]`**: 현재 소스 코드 및 단위 테스트에서 실제로 확인된 동작.
- **`[Proposed]`**: 향후 Phase 1–4에서 구현하고자 하는 목표 동작.
- **`[Gap]`**: 현행 동작과 목표 동작 사이의 불일치, 결함 또는 미구현 사항.
---
## 2. CLI 인터페이스 계약 (Orchestrator & Delegation)
### 2-1. `run_loop.sh` CLI 옵션 및 인수 파싱 계약
| 옵션 | 기본값 | 제약 및 검증 규칙 | 현행 소스 위치 | 대응 테스트 | 범주 |
|---|---|---|---|---|---|
| `--creator <session>` | 없음 (필수) | 비어있지 않은 문자열 필수(L95). 세션 레지스트리(`.mam/agent-sessions.yaml` / `.db`) 등록 및 실행 중 검증은 L160–185에서 수행 | `run_loop.sh:79, 95-98, 160-185` | `test_loop_cli.py:test_creator_parses_and_reaches_session_check` | `[Observed]` |
| `--task <goal-text>` | 없음 (필수) | 작업 목표 문자열. 누락 시 fast-fail 및 `usage` 출력 후 exit 1 | `run_loop.sh:85, 95-98` | `test_loop_cli.py:test_missing_creator_and_task_fail_fast` | `[Observed]` |
| `--target-agent <name>` | 제거됨 | 사용 즉시 에러 메시지 출력 후 exit 1 반환 (하위 호환 폐기) | `run_loop.sh:80-83` | `test_loop_cli.py:test_target_agent_is_rejected` | `[Observed]` |
| `--plan` | `false` | 플래너 개입 및 설계 단계 활성화 플래그 | `run_loop.sh:56` | `test_loop_cli.py:test_planner_without_plan_fail_fast` | `[Observed]` |
| `--planner <session>` | 자동 조회 | `--plan` 플래그 없이 지정 시 즉시 exit 1 반환. 미지정 시 등록된 planner 역할 세션 자동 선택 | `run_loop.sh:84, 100-105` | `test_loop_cli.py:test_explicit_planner_unregistered`, `test_explicit_planner_skips_auto_resolve` | `[Observed]` |
| `--plan-talk <N>` | `1` | 숫자 regex(`^[0-9]+$`) 검증. 에러 메시지는 양수를 요구하지만 regex는 **`0`을 허용**함. 0 지정 시 토론 루프(`$turn -le $PLAN_TALK_TURNS`)를 완전히 건너뜀 | `run_loop.sh:57-62, 442` | `test_loop_cli.py` (regex 파싱) | `[Observed]` |
| `--reviewer <A,B>` | 없음 | 쉼표 구분 리뷰어 세션 목록 | `run_loop.sh:63` | `test_loop_cli.py` | `[Observed]` |
| `--all-reviewer` | `false` | 모든 활성 리뷰어 세션 강제. `--reviewer`와 동시 지정 시 **`--all-reviewer`가 우선**하며 `--reviewer` 목록은 무시되고 경고 로그 출력 | `run_loop.sh:64, 192-196` | 현행 전용 CLI 테스트 부재 (`[Gap]`) | `[Observed]` |
| `--max-loop <N>` | `3` | 양수 정수 검증(`^[0-9]+$` and `N > 0`). 0 이하 시 에러 반환 | `run_loop.sh:65-70` | `test_loop_cli.py` | `[Observed]` |
| `--max-rebut <N>` | `1` | 비음수 정수 검증(`^[0-9]+$`). 0 지정 시 반론(Rebuttal) 완전 비활성화 | `run_loop.sh:71-76` | `test_o1_rebuttal.py:test_v3_rebut_option_parsing_and_budget` | `[Observed]` |
| `REBUT_TOTAL_BUDGET` | `MAX_REBUT * MAX_LOOP` | 전체 실행 기간 동안의 총 반론 허용 상한(Run-wide budget). 조기 소진 시 추가 반론 금지 | `run_loop.sh:93, 762-766` | `test_o1_rebuttal.py:test_v3_rebut_option_parsing_and_budget` | `[Observed]` |
| `--verbose` | `false` | 상세 실행 타임라인 추적 출력 활성화 플래그 | `run_loop.sh:77` | N/A | `[Observed]` |
| `--cleanup` | `false` | 루프 성공 종료 시 생성된 임시 작업 디렉터리(`.mam/jobs/<job>`) 및 가입자 출력 파일 삭제 | `run_loop.sh:78, 921-930` | N/A | `[Observed]` |
| `-h`, `--help` | N/A | 도움말 출력 후 **exit 1**로 비정상 종료 (현재 코드의 관행적 동작) | `run_loop.sh:50, 86` | `test_loop_cli.py:test_help_lists_creator_not_target_agent` | `[Observed]` |
> ⚠️ **Help Exit Code Gap (`[Gap]`)**:
> 현재 `run_loop.sh:50`은 `usage()` 내부에서 `exit 1`을 호출하여 `--help` 요청 시에도 exit 1을 반환합니다. 표준 CLI 관례(exit 0)와 다르나, 기존 서브프로세스 테스트들이 `exit 1`에 의존하므로 Phase 0에서는 현행 동작을 유지하며 의도적 변경은 Phase 3 wrapper 승격 시 별도 Decision으로 다룹니다.
### 2-2. 환경, 경로 및 수명주기 계약
1. **파이썬 인터프리터 탐색 (`_delegate_py_bin`)**:
- `AGENT_PYTHON_BIN` ➔ `VIRTUAL_ENV/bin/python` ➔ 상위 디렉터리 `.venv/bin/python` 탐색 ➔ 시스템 `python3` 순으로 캐싱 및 결정 (`lib.sh:1841-1861`). `[Observed]`
2. **런타임 고정 복사본 (B-13 Freeze Snapshot)**:
- 루프 기동 시 `.agents/skills/` 트리를 `/tmp/mam-loop-freeze.XXXXXX`로 복사 후 해당 위치의 `run_loop.sh`로 `exec` 재실행 (`run_loop.sh:112-127`). `[Observed]`
- 단, `multi-agent-mux-create`, `resume`, `stop` 등 1회성 스크립트는 Live Tree를 참조 (`[Observed]`).
3. **루프 가드 및 시그널 정리**:
- `MAM_LOOP_MARKER`(`.mam/loop-guard-active`)에 `pid`, `lstart`, `started` 타임스탬프를 원자적 하드링크로 기록 (`loop_lock.sh:69-75`). `[Observed]`
- `trap _mam_release_guard EXIT INT TERM HUP`을 통해 비정상 종료 시에도 안전하게 가드 파일 및 Freeze 임시 디렉터리 정리 (`run_loop.sh:189`). `[Observed]`
4. **작업 위임 함수 계약**:
- `delegate_submit_job`: `registry.py register`를 실행하고 생성된 32-bit hex JID를 stdout으로만 출력 (`lib.sh:1876-1889`). `[Observed]`
- `delegate_publish_event`: Non-fatal 계약 (`|| true`). 누락된 JID, 스크립트 부재, 브로커 전송 실패 시에도 0을 반환하여 호출자를 중단시키지 않음 (`lib.sh:1897-1905`). `[Observed]`
- `start_watchdog`: 백그라운드에서 `reconcile.sh --subscribe --idle-timeout 0`를 실행하고 해당 데몬 PID를 stdout으로 반환 (`lib.sh:1908-1938`). `[Observed]`
---
## 3. 이벤트 스키마 및 수명주기 계약 (MQTT & Registry)
### 3-1. 이벤트 스키마 v1 명세
- 필수 필드: `schema_version` (1), `seq` (단조 증가 양의 정수), `job_id` (JID), `event` (이벤트명), `timestamp` (ISO-8601 UTC `YYYY-MM-DDTHH:MM:SSZ`), `detail` (문자열 설명).
- 확장 필드: `data` (딕셔너리), `data.hmac_sig` (HMAC-SHA256 16진수 서명).
### 3-2. 이벤트와 레지스트리 상태(Status) 매핑 현황
```mermaid
stateDiagram-v2
[*] --> pending: register_job
pending --> running: pick_pending / event 'started'
running --> running: event 'progress' (상태 변경 없음)
running --> running: event 'permission_required' (상태 변경 없음)
running --> completed: event 'completed' (Terminal)
running --> error: event 'error' (Terminal)
running --> cancelled: event 'cancelled' / stop_session (Terminal)
```
| 이벤트 이름 | 레지스트리 상태 전이 | 지속성(Retain) | 설명 | 범주 |
|---|---|---|---|---|
| `started` | `running` | `false` | 작업자가 작업을 수락하고 실행을 시작함 | `[Observed]` |
| `progress` | 전이 없음 (`running` 유지) | `false` | 중간 진행 경과 보고. 레지스트리 status는 미변경 | `[Observed]` |
| `permission_required` | 전이 없음 (`running` 유지) | `false` | 사용자 권한 승인 대기. 레지스트리 status는 미변경 | `[Observed]` |
| `completed` | `completed` | `true` | 작업 성공 완료. Terminal 상태 | `[Observed]` |
| `error` | `error` | `true` | 작업 실패 종료. Terminal 상태 | `[Observed]` |
| `cancelled` | `cancelled` | `true` | 세션 중단 등에 의한 취소. Terminal 상태 | `[Observed]` |
> ⚠️ **이벤트-상태 간극 Gap (`[Gap]`)**:
> 1. `progress`와 `permission_required`는 이벤트로 발행되지만 레지스트리 `status` enum(`VALID_STATUSES = ('pending', 'running', 'completed', 'error', 'cancelled')`)에 포함되지 않아, 레지스트리 파일만으로는 권한 승인 대기 여부를 직접 알 수 없습니다 (`registry.py:38`).
> 2. `publish_event.py`의 Terminal 이벤트는 `("completed", "error", "cancelled")`이지만, `job_subscriber.py:46`의 Terminal 이벤트는 `("completed", "error")`로 정의되어 있습니다. 가입자가 디스크 폴백(`_check_disk_fallback`)을 수행할 때 비로소 `cancelled` 상태를 감지하여 가상 `error` 이벤트(`synth_event="error"`)로 변환하여 처리합니다 (`job_subscriber.py:80`).
> 3. **Terminal Non-reentry (`[Proposed]` vs `[Gap]`)**: 한 번 Terminal 상태에 도달한 후 중복/상충 이벤트에 의해 상태가 역행하지 않는 불변식은 Phase 2a의 목표 계약(`[Proposed]`)이며, 현행 `update_job_status`는 락 내에서 단순 필드 덮어쓰기(`record.update(fields)`)를 수행하므로 완벽한 비역행이 보장되지 않는 간극(`[Gap]`)이 있습니다.
### 3-3. 보안 및 재생 공격 방어
1. **인증 모드 분리**: `auth_token == null`일 때는 HMAC 검증 통과 (PoC 모드). `auth_token != null`일 때는 `data.hmac_sig` 검증 실패 시 즉시 폐기 (`job_subscriber.py:122-126`). `[Observed]`
2. **단조 시퀀스 검증**: 가입자는 수신된 페이로드의 `seq`가 이전 `last_seq`보다 큰 경우에만 처리하고, 같거나 작은 패킷은 replay 공격으로 간주하여 drop (`job_subscriber.py:127-136`). `[Observed]`
3. **로컬 감사 추적(Audit Log)**: MQTT 전송 성공/실패와 무관하게 `append_event`를 통해 `.mam/delegate_job_logs/<job_id>/{meta,events,status}`에 모든 시도 기록 (`mqtt_common.py:122, publish_event.py:208-219`). `[Observed]`
4. **MQTT 전송 실패 시 로컬 상태 기록**: 브로커 접속 불가 시에도 `publish_event.py`는 로컬 레지스트리의 상태를 `new_status`로 동기화한 뒤 exit code 2로 종료 (`publish_event.py:223-233`). `[Observed]`
---
## 4. 리포트 파싱 및 승격 계약 (Report & Promotion)
### 4-1. 판정 토큰(Verdict) 추출 로직
- `run_loop.sh:204-210`: `has_verdict "$file" "$verdict"`는 파일의 **마지막 비공백 행(last non-blank line)**을 추출하여 `^\[VERDICT: ${verdict}\][[:space:]]*\r?$` 패턴과 일치하는지 검사합니다.
- `[Gap]`: 마지막 행이 마크다운 코드 블록(```) 내부에 있는 예제 문자열인 경우에도 현재 셸 정규식은 이를 유효한 판정 토큰으로 오인할 수 있습니다. Phase 2의 엄격한 파서(Strict Parser)는 코드 펜스 내부를 제외해야 합니다.
- 리포트 파일 위치 탐색(`find_report`): `find ".mam/jobs/$1" -maxdepth 2 -name "report-final.md" | head -n 1`로 첫 번째 일치 파일을 취하며, 정확한 단일 경로 강제가 아닙니다 (`run_loop.sh:234`). `[Observed]`
### 4-2. 승격(Promotion) 및 정리(Cleanup) 계약
1. **계획서 승격**: `CURRENT_PLAN`을 `.agents/reports/$PLANNER_SESSION/plan-${PLAN_JOB_ID}.md.tmp`에 기록 후 `mv -f`로 원자적 교체 (`run_loop.sh:895-901`). `[Observed]`
2. **리뷰 리포트 승격**: 각 리뷰어의 `report-final.md`를 `.agents/reports/$rev/report-${job_id}.md`로 복사 및 이동 (`run_loop.sh:903-916`). `[Observed]`
3. **배치 비원자성 (`[Gap]`)**: 다수의 리뷰 리포트를 승격할 때 개별 파일 단위로 `cp && mv`를 수행하므로, 중간 크래시 발생 시 일부 리포트만 승격되는 부분 성공 위험이 존재합니다.
---
## 5. 잠금(Lock) 및 동시성 제어 계약
### 5-1. 락 체계 및 획득 순서
1. **Workspace Loop Guard**: `.mam/loop-guard-active` (원자적 하드링크 + `pid`/`lstart` 스틸 재시도).
2. **Session Registry Lock**: `.mam/agent-sessions.db` (SQLite WAL 동시성 및 트랜잭션).
3. **Job Registry Lock**: `.mam/jobs/<id>.lock` 또는 레지스트리 디렉터리 락 (`fcntl.flock(LOCK_EX)`).
### 5-2. `update_job_status`의 비-CAS 특성 (`[Gap]`)
- 현재 `mqtt_common.py:377-403`의 `update_job_status`는 락을 획득한 후 `record.update(fields)`로 단순히 필드를 덮어쓰며, 호출자가 기대한 이전 상태(`expected_status`)나 attempt 버전을 비교하지 않습니다.
- 또한 `registry.py:129-131`의 `pick_pending`은 `update_job_status`를 거치지 않고 직접 파일에 `chosen["status"] = "running"`을 씁니다.
- **`[Proposed]`**: Phase 2a에서는 모든 상태 변경이 단일 엔진의 원자적 CAS(Compare-And-Swap) 인터페이스를 통해서만 수행되도록 통일해야 합니다.
---
## 6. 소스 및 테스트 매핑 요약
| 구성요소 | 핵심 소스 파일 | 핵심 테스트 파일 | 기준선 상태 |
|---|---|---|---|
| 오케스트레이터 CLI | `run_loop.sh` | `tests/test_loop_cli.py` | 9개 테스트 통과 |
| 루프 가드 락 | `loop_lock.sh` | `tests/test_o2_race_free_lock.py` | 22개 테스트 통과 |
| 반론 상한선 | `run_loop.sh` | `tests/test_o1_rebuttal.py` | 10개 테스트 통과 |
| 위임 헬퍼 | `lib.sh` | `tests/test_tier1_unit.py` | 단위 테스트 통과 |
| 작업 레지스트리 | `registry.py`, `mqtt_common.py` | `tests/test_tier2_component.py` | fcntl 기반 락킹 |