diff --git a/.agents/skills/lib_py/delegation/mqtt_common.py b/.agents/skills/lib_py/delegation/mqtt_common.py index 6996eff..1bd7c99 100644 --- a/.agents/skills/lib_py/delegation/mqtt_common.py +++ b/.agents/skills/lib_py/delegation/mqtt_common.py @@ -398,7 +398,7 @@ def update_job_status(job_id: str, registry_dir: str = DEFAULT_REGISTRY_DIR, Signature, return value and exceptions are unchanged: phase 1's G-1..G-4 assert through this function, and the shell helpers capture its effects. - What changed is who performs the write -- ``lib_py.loop.engine`` is now the + What changed is who performs the write -- ``lib_py.delegation.engine`` is now the only writer of job status, so the CAS rules and the audit mirror exist once instead of twice. diff --git a/.agents/skills/lib_py/loop/__init__.py b/.agents/skills/lib_py/loop/__init__.py index 42cec98..248d21e 100644 --- a/.agents/skills/lib_py/loop/__init__.py +++ b/.agents/skills/lib_py/loop/__init__.py @@ -7,8 +7,10 @@ Issue #5 phase 2a. Two things live here and they are deliberately separated: -- see that module's docstring for why the package boundary matters under a frozen loop. * `watcher` observes and never decides; `policy` decides and never writes; - `shadow` runs the two together and, in this phase, only logs what it *would* - have committed. + `shadow` runs the two together and only logs what it *would* have committed. + * `autocomplete` is the phase 2b gate: it supplies verified evidence to that + same decision and, only if every gate holds, asks the transition boundary to + commit. It is off by default and has no production caller yet. Package rule, enforced by test: nothing here imports a `lib_py` module other than `lib_py.delegation`. Under a frozen loop the parent package may resolve to diff --git a/docs/contracts/issue-5/phase2b-autocomplete.md b/docs/contracts/issue-5/phase2b-autocomplete.md index e2446fb..d4d80f0 100644 --- a/docs/contracts/issue-5/phase2b-autocomplete.md +++ b/docs/contracts/issue-5/phase2b-autocomplete.md @@ -15,12 +15,25 @@ 확정을 선언하지 않은 에이전트는 매니페스트를 남기지 않는다. 매니페스트가 없으면 정책이 이미 `hold`한다(Phase 2a에서 구현·테스트 완료). **"미지원 경로 100% 보존"은 새 분기가 아니라 기존 동작의 귀결이다.** +## 2-1. ⚠️ 아직 배선되지 않았다 (정직한 현황) + +**`autocomplete.evaluate`와 `notifier.recover`를 호출하는 프로덕션 경로는 아직 없다.** `daemon.py`/`job_subscriber.py`/`cli.py`/`service.py` 어디도 이들을 임포트하지 않으며, `register`에 `auto_complete` opt-in 옵션도 없다. 즉 **현재 라이브 셸 루프에서 `MAM_AUTO_COMPLETE=1`은 무동작(no-op)이다.** + +Phase 2b가 제공하는 것은 **검증된 라이브러리와 게이트**이며, 이를 감시 루프에 연결하는 배선은 Phase 3의 몫이다. 초기 문서가 "기동 시 재발행이 구독자에서 수행된다"고 적었던 것은 사실이 아니었으며 여기서 정정한다(교차 리뷰 `3fa76e08` O-1, `a0de0e5e` 잔여 1·2). + +배선 시 필요한 것: +1. 감시 루프에서 `evaluate(...)` 호출 — **`report_dir`는 필수 인자다.** +2. 커밋 후 `notifier.flush(...)` 호출 — `evaluate`는 intent를 남길 뿐 발행하지 않는다. +3. 기동 시 `notifier.recover(...)` 호출. +4. `register`에 잡별 opt-in 경로 추가. + ## 3. 3중 게이트 (전부 참일 때만 커밋) | 게이트 | 기본값 | 근거 | |---|---|---| | `MAM_AUTO_COMPLETE=1` | **`0` (OFF)** | 전역 킬 스위치 | -| 백엔드 허용목록 | `claude,grok,codex` | 정지 상태(`idle`/`done`) **관측 증거가 있는 백엔드만**. `agy`는 `working`만 관측, `hermes`·`opencode`는 관측 0건 | +| `MAM_ENGINE_CAS=1` | `1` | `0`이면 자동완료도 함께 중단된다. 추론 완료는 CAS 경계 위에 서 있으므로, 경계가 롤백된 상태의 커밋은 그 경계를 우회하는 것이다 | +| 백엔드 허용목록 | `claude,grok,codex` (+ `-code`/`-build`/`-cli` 정식 키) | 정지 상태(`idle`/`done`) **관측 증거가 있는 백엔드만**. `agy`는 `working`만 관측, `hermes`·`opencode`는 관측 0건. **정확 일치**로 비교한다 — 접두 일치라면 미래의 `claude-experimental`이 수집된 적 없는 증거를 물려받는다 | | 잡별 opt-in | 레코드 `auto_complete: true` | 잡 단위 동의 | `MAM_AUTO_COMPLETE_BACKENDS`로 목록을 **좁힐 수만** 있다. 관측된 적 없는 이름은 무시된다 — 넓히려면 설정이 아니라 증거가 필요하다. @@ -49,7 +62,19 @@ - **독립 Outbox 저장소를 만들지 않는다**(`state-transitions.md` §5 명시). 이미 쓰는 레코드에 필드 하나를 더 쓴다. - **발행 실패는 의도적으로 intent를 남긴다.** 보내지 않은 알림이 보낸 것처럼 보이는 편이, 재시도되는 편보다 나쁘다. -- `notification_settled`는 **상태를 바꾸지 않는 bookkeeping 요청**이라 terminal 상태에서도 허용된다. 알림은 terminal에 *관한* 것이므로, terminal이 거부하는 요청으로는 영원히 정리할 수 없다. +- `notification_settled`는 **상태를 바꾸지 않는 bookkeeping 요청**이라 terminal 상태에서도 허용되며, **`pending_notification` 외의 필드는 쓸 수 없다**(경계에서 거부 — 현재 호출자가 그것만 넘긴다는 사실에 의존하지 않는다). +- 정리는 **실제로 보낸 intent와 일치할 때만** 이루어진다. 발행은 락 밖에서 일어나므로 그 사이에 새 intent가 정당하게 들어올 수 있고, 무조건 지우면 아무도 보내지 않은 알림이 사라진다. +- 발행 payload에 intent의 `seq`/`event`/`timestamp`를 실어 보낸다. **전송 계층 seq는 같은 intent의 재시도를 식별하지 못하므로**, 소비자는 `notification_intent`로 중복을 제거한다. 알림은 terminal에 *관한* 것이므로, terminal이 거부하는 요청으로는 영원히 정리할 수 없다. + +## 5-1. 필수 검증 (교차 리뷰 반영, 전부 미충족 시 `hold`) + +| 검증 | 없으면 | +|---|---| +| `report_dir` 인자 | `hold` — **선택이 아니다.** 생략하면 디스크 대조가 통째로 건너뛰어져 수집기의 자기 신고가 다시 판단 주체가 된다 | +| 기록된 dispatch | `hold` — 귀속시킬 대상이 없다 | +| 증거의 **대상 일치** | `hold` — 시나리오의 `job_id`·attempt·session·pane이 기록된 dispatch와 일치해야 한다. 정책은 시나리오 **내부**의 dispatch_context와만 비교하므로 구조상 항상 자기 일관적이다 | +| 매니페스트 재계산 대조 | `hold` | +| Reviewer 역할의 판정 토큰 | `hold` — Phase 0 안전 계약. 판정을 아직 쓰지 않은 리뷰어는 검토를 끝내지 않은 것이다 | ## 6. 무판정 fail-closed diff --git a/docs/contracts/issue-5/task.md b/docs/contracts/issue-5/task.md index 96d714c..c4cfef9 100644 --- a/docs/contracts/issue-5/task.md +++ b/docs/contracts/issue-5/task.md @@ -99,11 +99,14 @@ - [x] **P2B-00** 자동완료 도입 전 fail-closed 선행 조건 특성화 (X-01~X-05). - *증거*: 커밋 `9a827b3`. 매니페스트 없는 job이 이미 `hold`임을 고정 — 미지원 경로 보존이 새 분기 없이 성립하는 근거. - [x] **P2B-01** 리포트 확정 계약(매니페스트·SHA-256) 및 부수효과 증거 프로토콜. - - *증거*: 커밋 `3ea5757`. `lib_py/loop/finalize.py` + `publish_event --finalize-report/--side-effect`. **선언은 에이전트가, 해시 재계산은 읽는 쪽이** — 워처가 만든 매니페스트는 자기 증명이므로 채택하지 않았다. 리포트 재작성 시 즉시 무효, 선언한 부수효과는 전부 증거 필요. + - *증거*: 커밋 `3ea5757`. `lib_py/delegation/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-05** 교차 리뷰 3인(`3fa76e08` Claude / `a0de0e5e` Grok / `e5f26823` Codex) 차단 지적 7건 전건 수용·수정. + - *증거*: 커밋 `cb7ed1a`. 필수 매니페스트 검증(`report_dir` 생략 = 거부), 증거의 대상 일치(job/attempt/session/pane), Reviewer 판정 토큰 게이트(Phase 0 안전 계약), bookkeeping 필드 제한, 조건부 settle + intent 신원 전달, 부수효과 evidence 스키마 검증, `MAM_ENGINE_CAS=0` 준수, 백엔드 정확 일치, 절대 경로. 신규 회귀 21종 전부 수정 전 코드에서 실패함을 역검증. + - *배선 현황*: `evaluate`/`recover`의 **프로덕션 호출자는 아직 없다**(Phase 3 배선). 운영 노트 §2-1에 정정 기록. - [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.