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
64 lines
3.7 KiB
Markdown
64 lines
3.7 KiB
Markdown
# 📑 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가 스폰을 도입하면 스레드 안전한 방식으로 교체해야 한다.
|