docs(issue-5): phase 2b scenarios, operations note and task checklist
Three scenarios join the contract set, so the fifteen vectors now cover an unproven side effect, an undecidable attribution, and an agent whose state goes unreadable in the window between finalization and notification. All fifteen match their expected decision. The side-effect case needed a decision about layering. Authoritative enforcement belongs in manifest verification, which has the job record and therefore knows what was declared; the policy honours an explicit negative from the collector so the case is expressible as a vector. Absence is not read as proof there, because the manifest check is the layer that fails closed on missing evidence. The operations note records why the agent declares and the reader re-derives, the three gates and why the backend allowlist can only be narrowed, the notification transaction, and the rollback. It also records a deviation from my own plan. §D2 asked for reviewer sign-off before promoting an attempt id into the record, since that changes a contract phase 1 froze. Measurement during implementation showed the change was not needed: the attempt guard already lives in the evidence layer, where attempt identity actually is, so auto-completion passes the engine a status precondition instead. The schema is untouched, the frozen contract holds, and a characterization test fails the moment a record starts carrying an attempt id, which is when to revisit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
1 parent
803ba43884
commit
8adbe2245c
9 files changed
+329
-6
No files matched your search
@@ -154,6 +154,15 @@ def decide(scenario) -> Decision:
|
|||||||
if not filesystem.get("hash_verified"):
|
if not filesystem.get("hash_verified"):
|
||||||
return Decision("hold", "report manifest hash unverified", attribution_ok=True)
|
return Decision("hold", "report manifest hash unverified", attribution_ok=True)
|
||||||
|
|
||||||
|
# Side-effect evidence is enforced authoritatively in finalize.verify_manifest,
|
||||||
|
# which has the job record and therefore knows what was declared. The policy
|
||||||
|
# honours an explicit negative from the collector so the case is expressible
|
||||||
|
# as a scenario vector; absence here is not read as proof, because the
|
||||||
|
# manifest check is the layer that fails closed on missing evidence.
|
||||||
|
if filesystem.get("side_effects_verified") is False:
|
||||||
|
return Decision("hold", "declared side effect has no evidence in the commit manifest",
|
||||||
|
attribution_ok=True)
|
||||||
|
|
||||||
return Decision("candidate_approved",
|
return Decision("candidate_approved",
|
||||||
"attribution, quiescence and a verified manifest all hold",
|
"attribution, quiescence and a verified manifest all hold",
|
||||||
attribution_ok=True)
|
attribution_ok=True)
|
||||||
@@ -24,7 +24,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 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 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**<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 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 2b**<br>*(완료, 기본 비활성)* | **이중 감지 자동완료 활성화 (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 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`) 재활성화 및 라이브 헬퍼 버전 검사 복구 |
|
| **Phase 4** | **불변 런타임 검증 및 Freeze 제거** | • 기동 시 전체 모듈 상주(Eager Import) 및 자식 템플릿 불변성 검증<br>• 작업 도중 스킬 파일 교체 및 늦은 경로 스트레스 테스트 통과<br>• `/tmp/mam-loop-freeze.XXXXXX` 우회책 완전 제거 | • 런타임 자기수정 크래시(B-13) 재발 방지 검증<br>• Live Helper 버전 호환 게이트 검증 통과<br>• 운영 문서 및 배포 스크립트 갱신 | Freeze 복사 로직(`run_loop.sh:112-127`) 재활성화 및 라이브 헬퍼 버전 검사 복구 |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# 📑 Issue #5 Phase 2b — 자동완료 운영 노트 (Flag-Gated / Opt-In)
|
||||||
|
|
||||||
|
- **문서 버전**: Rev.1
|
||||||
|
- **상태**: 구현 완료, **기본 비활성(`MAM_AUTO_COMPLETE=0`)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 한 문장 요약
|
||||||
|
|
||||||
|
**끝났다는 사실을 아는 쪽(에이전트)이 확정을 선언하고, 읽는 쪽은 그 선언을 믿지 않고 해시를 다시 계산한다.**
|
||||||
|
|
||||||
|
워처가 스스로 해시를 계산해 매니페스트를 만들면 그것은 "워처가 안정된 파일을 봤다"는 자기 증명일 뿐이다. 이 이슈의 앞선 라운드에서 리뷰어가 기각한 증거 형태이며, 같은 실수를 반복하지 않기 위한 설계 결정이다.
|
||||||
|
|
||||||
|
## 2. 그래서 미지원 에이전트는 자동으로 안전하다
|
||||||
|
|
||||||
|
확정을 선언하지 않은 에이전트는 매니페스트를 남기지 않는다. 매니페스트가 없으면 정책이 이미 `hold`한다(Phase 2a에서 구현·테스트 완료). **"미지원 경로 100% 보존"은 새 분기가 아니라 기존 동작의 귀결이다.**
|
||||||
|
|
||||||
|
## 3. 3중 게이트 (전부 참일 때만 커밋)
|
||||||
|
|
||||||
|
| 게이트 | 기본값 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| `MAM_AUTO_COMPLETE=1` | **`0` (OFF)** | 전역 킬 스위치 |
|
||||||
|
| 백엔드 허용목록 | `claude,grok,codex` | 정지 상태(`idle`/`done`) **관측 증거가 있는 백엔드만**. `agy`는 `working`만 관측, `hermes`·`opencode`는 관측 0건 |
|
||||||
|
| 잡별 opt-in | 레코드 `auto_complete: true` | 잡 단위 동의 |
|
||||||
|
|
||||||
|
`MAM_AUTO_COMPLETE_BACKENDS`로 목록을 **좁힐 수만** 있다. 관측된 적 없는 이름은 무시된다 — 넓히려면 설정이 아니라 증거가 필요하다.
|
||||||
|
|
||||||
|
## 4. 확정 계약 (P2B-01)
|
||||||
|
|
||||||
|
```
|
||||||
|
에이전트: publish_event.py --job <id> --event completed \
|
||||||
|
--finalize-report <path> [--side-effect <kind>=<ref>] ...
|
||||||
|
① <path>.tmp → <path> 원자적 rename (반쯤 쓰인 리포트를 최종본으로 볼 수 없게)
|
||||||
|
② SHA-256 계산
|
||||||
|
③ report-manifest.json 원자적 기록 (job_id, attempt_id, sha256, side_effects, declared_by)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **리포트를 다시 쓰면 선언은 즉시 무효**가 된다. 해시를 다시 계산해 대조하기 때문이다. 새 본문에는 새 확정이 필요하다.
|
||||||
|
- **선언한 부수효과는 전부 증거가 있어야 한다.** 일부만 있는 것은 없는 것으로 취급한다 — 작업이 끝났다고 알 수 없기 때문이다.
|
||||||
|
- 확정은 발행 **앞에** 일어난다. 리포트를 아직 쓰는 중인 `completed` 이벤트는 계약이 금지하는 부분 리포트 채택을 부른다.
|
||||||
|
|
||||||
|
## 5. 지연 알림 (P2B-02)
|
||||||
|
|
||||||
|
```
|
||||||
|
락 안: 상태 커밋 + pending_notification 기록 ← 하나의 원자적 쓰기
|
||||||
|
락 밖: 발행 → 성공 시 notification_settled 전이로 정리
|
||||||
|
기동 시: pending_notification이 남은 레코드를 재발행 (멱등: 구독자가 seq로 중복 제거)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **독립 Outbox 저장소를 만들지 않는다**(`state-transitions.md` §5 명시). 이미 쓰는 레코드에 필드 하나를 더 쓴다.
|
||||||
|
- **발행 실패는 의도적으로 intent를 남긴다.** 보내지 않은 알림이 보낸 것처럼 보이는 편이, 재시도되는 편보다 나쁘다.
|
||||||
|
- `notification_settled`는 **상태를 바꾸지 않는 bookkeeping 요청**이라 terminal 상태에서도 허용된다. 알림은 terminal에 *관한* 것이므로, terminal이 거부하는 요청으로는 영원히 정리할 수 없다.
|
||||||
|
|
||||||
|
## 6. 무판정 fail-closed
|
||||||
|
|
||||||
|
매니페스트를 읽지 못함, 정책이 예외를 던짐, 알 수 없는 결정 — 전부 `hold`다. **"판정하지 못함"은 결코 "완료"가 아니다.** 늦게 도착한 명시 terminal이 있으면 전이 경계가 추론 완료를 거부하며, 그것은 경계가 제 일을 한 것이지 게이트의 오류가 아니다.
|
||||||
|
|
||||||
|
## 7. 감사 표식
|
||||||
|
|
||||||
|
자동완료로 들어간 terminal에는 `completed_by: "auto_complete"`와 판정 사유가 남는다. 구독자와 래퍼는 자동/명시 완료를 구별하지 못하므로(`job_subscriber._check_disk_fallback`은 레지스트리 status만 본다), **사후에 구별할 수 있는 유일한 수단**이다.
|
||||||
|
|
||||||
|
## 8. 롤백
|
||||||
|
|
||||||
|
1. `MAM_AUTO_COMPLETE=0` — 신규 자동완료만 중단. **이미 확정된 terminal 레코드는 보존**한다.
|
||||||
|
2. `MAM_ENGINE_CAS=0` — Phase 2a 경로까지 되돌린다(자동완료는 그 위에 있으므로 함께 무력화).
|
||||||
|
3. `.mam/shadow/`는 레지스트리와 분리되어 있어 삭제해도 job 상태에 영향이 없다.
|
||||||
|
|
||||||
|
## 9. 계획 대비 이탈 1건 (레코드 스키마 미변경)
|
||||||
|
|
||||||
|
계획서 §D2는 `attempt_id`를 job 레코드의 1급 필드로 승격할 것을 권고하고, Phase 1이 동결한 "레코드 필드 불변" 계약의 변경이므로 **리뷰어 승인을 선행 조건**으로 걸었다.
|
||||||
|
|
||||||
|
구현 중 실측한 결과 **그 변경이 필요하지 않았다**:
|
||||||
|
- 엔진의 `expected_attempt`는 레코드의 `attempt_id`(없음)를 `iteration`(루프 회차)으로 대체 비교한다 — 두 개념이 달라 의미가 성립하지 않는다.
|
||||||
|
- 반면 **시도 식별은 이미 증거 계층에서 검증된다**: `policy`가 관측 식별자와 shadow 저장소의 dispatch 레코드를 대조하며, 시도가 어긋나면 `hold`한다(실측 확인).
|
||||||
|
|
||||||
|
따라서 자동완료는 엔진에 `expected_attempt`를 넘기지 않고 `expected_status="running"`(엔진의 고유 영역인 상태)만 전제조건으로 준다. **시도 보증은 시도 정체성이 실제로 사는 곳에 둔다.** 레코드 스키마는 바뀌지 않았고, Phase 1 계약도 유지된다.
|
||||||
|
|
||||||
|
`attempt_id` 승격은 열린 질문으로 남긴다. 승격하면 엔진에 2차 방어선이 생기지만 동결 계약을 바꿔야 하며, 현재는 필요하지 않다. `test_x03_the_record_carries_iteration_not_an_attempt_id`가 이 전제를 감시한다 — 레코드가 `attempt_id`를 갖게 되는 순간 실패하며, 그때가 재검토 시점이다.
|
||||||
|
|
||||||
|
## 10. Phase 3 인계
|
||||||
|
|
||||||
|
1. `preexec_fn`: 2b도 데몬을 스폰하지 않아 미노출 유지. 알림 flush는 기존 구독자 프로세스 안에서 수행한다.
|
||||||
|
2. 부수효과의 **원격** 확인(실제 push 도달 여부 등)은 비범위 — 증거의 존재·대응까지만 검증한다.
|
||||||
|
3. `agy`/`hermes`/`opencode` 자동완료는 정지 상태 실측 증거 확보 후 별도 판단.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- **Document Version**: Rev.7 (Phase 2a 실행 완료 — P2A-01~P2A-04 증거 기록)
|
- **Document Version**: Rev.7 (Phase 2a 실행 완료 — P2A-01~P2A-04 증거 기록)
|
||||||
- **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
|
- **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 2a Complete (shadow-only)** / Phase 2b–4 Staged
|
- **Status**: Phase 0 Complete / **Phase 1 Complete (v4.3.0)** / **Phase 2a Complete (shadow-only)** / **Phase 2b Complete (default-off)** / Phase 3–4 Staged
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -94,10 +94,19 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 2b: 이중 감지 자동완료 활성화 (후속)
|
## Phase 2b: 이중 감지 자동완료 활성화 (완료, 기본 비활성)
|
||||||
- [ ] **P2B-01** report finalization/manifest/hash 및 외부 부수효과 evidence protocol 구현.
|
|
||||||
- [ ] **P2B-02** terminal+pending notification commit/재전송·재시작·늦은 conflict 처리 검증.
|
- [x] **P2B-00** 자동완료 도입 전 fail-closed 선행 조건 특성화 (X-01~X-05).
|
||||||
- [ ] **P2B-03** 검증된 backend/job에만 default-off flag로 활성화; unsupported explicit event 경로 보존.
|
- *증거*: 커밋 `9a827b3`. 매니페스트 없는 job이 이미 `hold`임을 고정 — 미지원 경로 보존이 새 분기 없이 성립하는 근거.
|
||||||
|
- [x] **P2B-01** 리포트 확정 계약(매니페스트·SHA-256) 및 부수효과 증거 프로토콜.
|
||||||
|
- *증거*: 커밋 `3ea5757`. `lib_py/loop/finalize.py` + `publish_event --finalize-report/--side-effect`. **선언은 에이전트가, 해시 재계산은 읽는 쪽이** — 워처가 만든 매니페스트는 자기 증명이므로 채택하지 않았다. 리포트 재작성 시 즉시 무효, 선언한 부수효과는 전부 증거 필요.
|
||||||
|
- [x] **P2B-02** terminal + pending notification, 재전송·재시작·늦은 conflict.
|
||||||
|
- *증거*: 커밋 `803ba43`. `pending_notification`을 상태와 **같은 원자적 쓰기**로 커밋, 락 밖 발행, `notification_settled`(상태 무변경 bookkeeping 요청 — terminal에서도 허용)로 정리, 기동 시 재발행. 발행 실패 시 intent 보존.
|
||||||
|
- [x] **P2B-03** 검증된 backend/job 한정 default-off 플래그.
|
||||||
|
- *증거*: 커밋 `803ba43`. 3중 게이트(전역 OFF 기본 / 관측 증거 기반 허용목록 `claude,grok,codex` / 잡별 opt-in). 허용목록은 좁히기만 가능. 자동완료 terminal에 `completed_by` 표식.
|
||||||
|
- [x] **P2B-04** 부수효과 예외·무판정 fail-closed·경쟁/재시작 시나리오.
|
||||||
|
- *증거*: 신규 시나리오 3종(`side_effect_unproven`, `no_verdict_fail_closed`, `notification_crash_window`)을 Phase 0 스키마로 추가, **총 15종 전부** `expected_policy.decision` 일치.
|
||||||
|
- *계획 대비 이탈*: 계획서 §D2의 `attempt_id` 레코드 승격(및 그 선행 조건인 리뷰어 승인)은 **불필요함이 실측으로 확인되어 수행하지 않았다.** 시도 보증은 이미 증거 계층(`policy` ↔ shadow dispatch 대조)에 있으며, 레코드 스키마와 Phase 1 동결 계약은 그대로다. 상세는 `phase2b-autocomplete.md` §9.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+18
@@ -73,6 +73,24 @@
|
|||||||
"provenance": "simulated",
|
"provenance": "simulated",
|
||||||
"expected_decision": "reject_stale",
|
"expected_decision": "reject_stale",
|
||||||
"description": "The pane was restarted mid-flight, so the idle signal comes from a different pane than the one dispatched to."
|
"description": "The pane was restarted mid-flight, so the idle signal comes from a different pane than the one dispatched to."
|
||||||
|
},
|
||||||
|
"side_effect_unproven": {
|
||||||
|
"file": "side_effect_unproven.json",
|
||||||
|
"provenance": "simulated",
|
||||||
|
"expected_decision": "hold",
|
||||||
|
"description": "The job declared an external side effect but the manifest carries no evidence for it. Partial evidence is no evidence."
|
||||||
|
},
|
||||||
|
"no_verdict_fail_closed": {
|
||||||
|
"file": "no_verdict_fail_closed.json",
|
||||||
|
"provenance": "simulated",
|
||||||
|
"expected_decision": "hold",
|
||||||
|
"description": "The collector could not establish the agent session, so attribution cannot be completed. An undecidable case must hold."
|
||||||
|
},
|
||||||
|
"notification_crash_window": {
|
||||||
|
"file": "notification_crash_window.json",
|
||||||
|
"provenance": "simulated",
|
||||||
|
"expected_decision": "hold",
|
||||||
|
"description": "The agent's state became unreadable between finalization and notification. Unknown is not quiescent."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"scenario_id": "no_verdict_fail_closed",
|
||||||
|
"backend": "backend_agnostic",
|
||||||
|
"provenance": "simulated",
|
||||||
|
"description": "The collector could not establish the agent session, so attribution cannot be completed. An undecidable case must hold.",
|
||||||
|
"dispatch_context": {
|
||||||
|
"job_id": "test-job-no-verdict-01",
|
||||||
|
"attempt_id": 1,
|
||||||
|
"agent_session_uuid": "7d5700d0-bab1-4f65-9665-6b887396a9a6",
|
||||||
|
"pane_process_generation": "w2X:p1:1234:2026-09-06T00:00:00Z",
|
||||||
|
"dispatch_ack": true,
|
||||||
|
"execution_anchor_observed": true
|
||||||
|
},
|
||||||
|
"timeline": [
|
||||||
|
{
|
||||||
|
"collector_clock_id": "CLOCK_MONOTONIC",
|
||||||
|
"relative_ms": 0,
|
||||||
|
"source_kind": "event",
|
||||||
|
"source_provenance": "simulated",
|
||||||
|
"payload": {
|
||||||
|
"event": "started",
|
||||||
|
"job_id": "test-job-no-verdict-01"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collector_clock_id": "CLOCK_MONOTONIC",
|
||||||
|
"relative_ms": 5000,
|
||||||
|
"source_kind": "filesystem",
|
||||||
|
"source_provenance": "simulated",
|
||||||
|
"payload": {
|
||||||
|
"report_manifest": {
|
||||||
|
"report_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"attempt_id": 1,
|
||||||
|
"finalized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collector_clock_id": "CLOCK_MONOTONIC",
|
||||||
|
"relative_ms": 6000,
|
||||||
|
"source_kind": "herdr",
|
||||||
|
"source_provenance": "simulated",
|
||||||
|
"payload": {
|
||||||
|
"agent_status": "idle",
|
||||||
|
"interactive_ready": true
|
||||||
|
},
|
||||||
|
"collector_attribution": {
|
||||||
|
"job_id": "test-job-no-verdict-01",
|
||||||
|
"attempt_id": 1,
|
||||||
|
"pane_process_generation": "w2X:p1:1234:2026-09-06T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filesystem": {
|
||||||
|
"report_path": ".mam/jobs/test-job-finalized-01/report-final.md",
|
||||||
|
"exists": true,
|
||||||
|
"manifest_committed": true,
|
||||||
|
"hash_verified": true
|
||||||
|
},
|
||||||
|
"expected_policy": {
|
||||||
|
"decision": "hold",
|
||||||
|
"reason": "no observed agent_session_uuid to attribute this completion to",
|
||||||
|
"policy_version": "2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"scenario_id": "notification_crash_window",
|
||||||
|
"backend": "backend_agnostic",
|
||||||
|
"provenance": "simulated",
|
||||||
|
"description": "The agent's state became unreadable between finalization and notification. Unknown is not quiescent.",
|
||||||
|
"dispatch_context": {
|
||||||
|
"job_id": "test-job-notify-crash-01",
|
||||||
|
"attempt_id": 1,
|
||||||
|
"agent_session_uuid": "7d5700d0-bab1-4f65-9665-6b887396a9a6",
|
||||||
|
"pane_process_generation": "w2X:p1:1234:2026-09-06T00:00:00Z",
|
||||||
|
"dispatch_ack": true,
|
||||||
|
"execution_anchor_observed": true
|
||||||
|
},
|
||||||
|
"timeline": [
|
||||||
|
{
|
||||||
|
"collector_clock_id": "CLOCK_MONOTONIC",
|
||||||
|
"relative_ms": 0,
|
||||||
|
"source_kind": "event",
|
||||||
|
"source_provenance": "simulated",
|
||||||
|
"payload": {
|
||||||
|
"event": "started",
|
||||||
|
"job_id": "test-job-notify-crash-01"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collector_clock_id": "CLOCK_MONOTONIC",
|
||||||
|
"relative_ms": 5000,
|
||||||
|
"source_kind": "filesystem",
|
||||||
|
"source_provenance": "simulated",
|
||||||
|
"payload": {
|
||||||
|
"report_manifest": {
|
||||||
|
"report_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"attempt_id": 1,
|
||||||
|
"finalized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collector_clock_id": "CLOCK_MONOTONIC",
|
||||||
|
"relative_ms": 6000,
|
||||||
|
"source_kind": "herdr",
|
||||||
|
"source_provenance": "simulated",
|
||||||
|
"payload": {
|
||||||
|
"agent_status": "unknown",
|
||||||
|
"interactive_ready": false
|
||||||
|
},
|
||||||
|
"collector_attribution": {
|
||||||
|
"job_id": "test-job-notify-crash-01",
|
||||||
|
"attempt_id": 1,
|
||||||
|
"agent_session_uuid": "7d5700d0-bab1-4f65-9665-6b887396a9a6",
|
||||||
|
"pane_process_generation": "w2X:p1:1234:2026-09-06T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filesystem": {
|
||||||
|
"report_path": ".mam/jobs/test-job-finalized-01/report-final.md",
|
||||||
|
"exists": true,
|
||||||
|
"manifest_committed": true,
|
||||||
|
"hash_verified": true
|
||||||
|
},
|
||||||
|
"expected_policy": {
|
||||||
|
"decision": "hold",
|
||||||
|
"reason": "agent status is unknown; fail closed",
|
||||||
|
"policy_version": "2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"scenario_id": "side_effect_unproven",
|
||||||
|
"backend": "backend_agnostic",
|
||||||
|
"provenance": "simulated",
|
||||||
|
"description": "The job declared an external side effect but the manifest carries no evidence for it. Partial evidence is no evidence.",
|
||||||
|
"dispatch_context": {
|
||||||
|
"job_id": "test-job-sideeffect-missing-01",
|
||||||
|
"attempt_id": 1,
|
||||||
|
"agent_session_uuid": "7d5700d0-bab1-4f65-9665-6b887396a9a6",
|
||||||
|
"pane_process_generation": "w2X:p1:1234:2026-09-06T00:00:00Z",
|
||||||
|
"dispatch_ack": true,
|
||||||
|
"execution_anchor_observed": true
|
||||||
|
},
|
||||||
|
"timeline": [
|
||||||
|
{
|
||||||
|
"collector_clock_id": "CLOCK_MONOTONIC",
|
||||||
|
"relative_ms": 0,
|
||||||
|
"source_kind": "event",
|
||||||
|
"source_provenance": "simulated",
|
||||||
|
"payload": {
|
||||||
|
"event": "started",
|
||||||
|
"job_id": "test-job-sideeffect-missing-01"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collector_clock_id": "CLOCK_MONOTONIC",
|
||||||
|
"relative_ms": 5000,
|
||||||
|
"source_kind": "filesystem",
|
||||||
|
"source_provenance": "simulated",
|
||||||
|
"payload": {
|
||||||
|
"report_manifest": {
|
||||||
|
"report_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"attempt_id": 1,
|
||||||
|
"finalized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collector_clock_id": "CLOCK_MONOTONIC",
|
||||||
|
"relative_ms": 6000,
|
||||||
|
"source_kind": "herdr",
|
||||||
|
"source_provenance": "simulated",
|
||||||
|
"payload": {
|
||||||
|
"agent_status": "idle",
|
||||||
|
"interactive_ready": true
|
||||||
|
},
|
||||||
|
"collector_attribution": {
|
||||||
|
"job_id": "test-job-sideeffect-missing-01",
|
||||||
|
"attempt_id": 1,
|
||||||
|
"agent_session_uuid": "7d5700d0-bab1-4f65-9665-6b887396a9a6",
|
||||||
|
"pane_process_generation": "w2X:p1:1234:2026-09-06T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filesystem": {
|
||||||
|
"report_path": ".mam/jobs/test-job-finalized-01/report-final.md",
|
||||||
|
"exists": true,
|
||||||
|
"manifest_committed": true,
|
||||||
|
"hash_verified": true,
|
||||||
|
"side_effects_verified": false
|
||||||
|
},
|
||||||
|
"expected_policy": {
|
||||||
|
"decision": "hold",
|
||||||
|
"reason": "declared side effect has no evidence in the commit manifest",
|
||||||
|
"policy_version": "2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -560,6 +560,8 @@ def _scenarios():
|
|||||||
# ... and the five P2A-04 requires, held to the same contract
|
# ... and the five P2A-04 requires, held to the same contract
|
||||||
"race_double_dispatch", "attempt_reuse", "partial_report",
|
"race_double_dispatch", "attempt_reuse", "partial_report",
|
||||||
"unknown_status", "restart_midflight",
|
"unknown_status", "restart_midflight",
|
||||||
|
# ... and the three phase 2b adds, held to the same contract
|
||||||
|
"side_effect_unproven", "no_verdict_fail_closed", "notification_crash_window",
|
||||||
])
|
])
|
||||||
def test_g7_policy_matches_the_phase0_expected_decision(scenario_id):
|
def test_g7_policy_matches_the_phase0_expected_decision(scenario_id):
|
||||||
"""G7: the seven scenarios that survived three-reviewer scrutiny are the
|
"""G7: the seven scenarios that survived three-reviewer scrutiny are the
|
||||||
|
|||||||
Reference in new issue
Block a user