Files
multi-agent-mux/.agents/reports/creator-claude-01/plan-df85e978.md
T
GodopuandClaude Opus 5 5eb9524fe7 docs(issue-5): phase 2b implementation plan (flag-gated auto-completion)
An executable plan for P2B-01..03, grounded in measurement of what phase 2a
actually left behind rather than in restating the contracts.

The design turns on one question: who writes the completion manifest. If the
watcher computes the hash itself, the manifest only attests that the watcher saw
a stable file, which is the self-certification Codex rejected as R1 last round.
So the side that knows it finished declares it, and the watcher re-derives the
hash rather than trusting the declaration.

That choice makes P2B-03's "preserve the explicit path completely" fall out for
free instead of needing a branch: an agent that declares nothing leaves no
manifest, and a missing manifest already holds today. Verified against the
current policy.

Four measurements shaped the rest. Nothing in the tree writes or reads a
manifest, so the protocol is new in full. Records carry iteration but never
attempt_id, and the engine silently substitutes one for the other -- they agree
today only by coincidence of the wrapper's flow, and the auto-completion gate
hangs off that value, so the plan asks for an explicit decision and reviewer
sign-off before any code is written. The pending-notification contract forbids a
separate outbox, so the intent rides in the record the transaction already
writes. And only three backends have observed evidence of a quiescent state, so
the allowlist has a factual basis instead of an optimistic one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
2026-09-06 21:24:04 +09:00

18 KiB

📑 Issue #5 Phase 2b 구현 계획서 — 이중 감지 자동완료 (Flag-Gated / Opt-In)

  • Job: df85e978 / Planner: creator-claude-01
  • 기준 커밋: 4a5916a (branch refactor, working tree clean) / Python 3.9.6 / herdr 0.8.2 / 전체 테스트 664
  • 요구 출처: implementation_plan.md §2 Phase 2b 행, task.md P2B-01P2B-03, state-transitions.md §3§6
  • 문서 성격: Worker가 그대로 실행할 수 있는 단계별 명세. §1의 모든 인용은 라이브 코드·실행으로 직접 확인했다.

0. 요약 (TL;DR)

  1. 매니페스트를 쓰는 주체가 이 Phase의 전부다. 워처가 스스로 해시를 계산해 매니페스트를 만들면 그것은 "워처가 안정된 파일을 봤다"는 자기 증명일 뿐이며, Round 2에서 Codex가 R1으로 기각한 바로 그 형태다. 끝났다는 사실을 아는 쪽(에이전트)이 확정을 선언해야 한다(§D1).
  2. 그래서 미지원 에이전트는 자동으로 fail-closed가 된다. 확정 선언을 하지 않으면 매니페스트가 없고, 매니페스트가 없으면 policy가 이미 hold한다(Phase 2a에서 구현·테스트 완료). P2B-03의 "미지원 경로 100% 보존"은 새 분기가 아니라 기존 동작의 자연스러운 귀결이다.
  3. attempt_id와 iteration의 의미 충돌을 먼저 정리해야 한다. 엔진은 record.get("attempt_id", record.get("iteration"))로 대체하는데, 레코드에는 attempt_id가 없고 iteration만 있다(실측). 지금은 우연히 값이 겹쳐 동작하지만 두 개념은 다르며, 자동완료 게이트가 이 값에 걸리므로 명시적 결정 없이 진행하면 안 된다(§D2).
  4. 지연 알림은 새 저장소를 만들지 않는다. state-transitions.md §5가 "독립 Outbox DB 신설 금지"를 명시했고, 레코드 안 pending_notification 필드를 같은 CAS 트랜잭션에서 쓴다(§D4).
  5. 자동완료 허용 백엔드는 실측 증거가 있는 것으로 한정한다. Phase 0 능력 행렬 실측: 정지 상태(idle/done) 관측 증거가 있는 것은 claude·grok(idle), codex(done) 뿐이고 agy·hermes·opencode는 없다(§1-5). 플래그가 켜져도 이 셋 외에는 자동완료하지 않는다.
  6. 664개 무회귀 + 신규. Phase 2a와 동일하게 특성화 선행 → 구현 → 문서 순으로 커밋 5개.

1. Planner 사전 실측

1-1. 자동완료에 필요한 것 중 존재하지 않는 것

필요 현황 (실측)
완료 매니페스트 없음. 저장소 어디에도 매니페스트를 쓰거나 읽는 프로덕션 코드가 0건. 리포트 디렉터리에는 report-final.md 하나뿐
리포트 원자적 확정 없음. 에이전트가 report-final.md를 직접 쓴다. tmp+rename 없음
attempt_id 레코드 필드 없음. 레코드에는 iteration만 존재
pending_notification 없음. grep 0건
MAM_AUTO_COMPLETE 없음. grep 0건 (참고: MAM_ENGINE_CAS 5건, MAM_DELEGATION_PY 4건은 존재)

1-2. 이미 준비되어 있는 것 (Phase 2a 산출물)

있음 위치
단일 CAS 전이 경계 + 권한 축 lib_py/delegation/engine.py::transition
expected_attempt fail-closed 레코드가 attempt를 못 대면 거부 (Round 1 R2)
관측 증거 필수 판정 lib_py/loop/policy.py — 4식별자 + 매니페스트 attempt_id 부재 시 hold (Round 2 R1)
매니페스트 미확정 시 hold policy.decide의 manifest_committed/hash_verified 게이트
herdr agent wait 래핑·백오프 lib_py/loop/watcher.py
쓰기 경로 없는 shadow 러너 lib_py/loop/shadow.py
지연 알림의 순서 보장 기반 transition_seq + per-job 감사 락 (Round 2 R4)

즉 Phase 2b는 판정기를 새로 만드는 것이 아니라, 이미 fail-closed로 판정하고 있는 게이트에 "진짜 증거"를 공급하고 그 결과를 커밋에 연결하는 작업이다.

1-3. attempt_id vs iteration — 실행으로 확인한 충돌

record keys with attempt semantics: {'iteration': 1}
transition(completed, expected_attempt=1) -> committed=True   running -> completed
transition(completed, expected_attempt=2) -> committed=False  stale attempt: expected 2, record has 1

엔진은 attempt_id가 없으면 iteration을 쓴다. 래퍼의 loop 흐름에서는 redispatch가 iteration을 증가시키므로 두 값이 우연히 일치하지만, iteration은 루프 회차이고 attempt_id는 디스패치 시도다. 직접(--type direct) 재디스패치나 크래시 후 재시도에서 갈라진다. 자동완료 게이트가 이 값에 걸리므로 §D2에서 명시적으로 결정한다.

1-4. 미지원 경로 보존의 실제 메커니즘

job_subscriber._check_disk_fallback(:60-72)은 레지스트리 status를 읽어 terminal이면 즉시 종료한다. 즉 자동완료가 completed를 쓰면 래퍼는 명시 이벤트와 구별하지 못한다. 이는 의도된 결합이지만, 동시에 "자동완료가 틀리면 래퍼가 조용히 잘못된 완료를 받는다"는 뜻이므로 §D6의 감사 표식이 필수다.

1-5. 자동완료 허용 백엔드 (Phase 0 능력 행렬 실측)

claude    observed slots: ['idle']
agy       observed slots: ['working', 'wait_timeout']
hermes    observed slots: NONE
grok      observed slots: ['idle']
opencode  observed slots: NONE
codex     observed slots: ['done']

자동완료는 정지 상태 관측을 전제로 한다. idle/done 증거가 있는 claude·grok·codex만 후보이며, agy는 working만 관측돼 정지 판정의 실측 근거가 없고, hermes·opencode는 관측 0건이다.

1-6. 에이전트 지시문 주입 지점 (조정 비용)

create_session.sh:483과 래퍼 :202/:319가 publish_event.py --job <id>를 지시문에 넣는다. 확정 선언 명령을 추가하려면 이 두 곳만 바꾸면 된다.


2. 설계 결정

D1. 매니페스트는 끝난 쪽이 선언한다 (P2B-01의 핵심)

에이전트: 작업 종료 시
  publish_event.py --job <id> --finalize-report <path> [--side-effect <kind>=<evidence>] ...
      ① <path>.tmp 를 <path> 로 원자적 rename (이미 최종본이면 rename 생략)
      ② SHA-256 계산
      ③ <report_dir>/report-manifest.json 원자적 기록:
         { schema_version, job_id, attempt_id, report_path, sha256,
           finalized_at, side_effects: [...], declared_by }
  • 워처는 매니페스트를 만들지 않는다. 만들면 자기 증명이 되어 Round 2 R1에서 기각된 형태가 된다. 워처는 읽고 검증만 한다(해시 재계산 후 대조).
  • 미지원 에이전트는 아무것도 하지 않으면 된다 — 매니페스트가 없으면 policy가 이미 hold한다. P2B-03의 "explicit event 경로 100% 보존"이 새 코드 없이 성립한다.
  • 리포트가 재작성되면 해시가 어긋나 매니페스트가 즉시 무효가 된다(§4 계약 그대로).

D2. attempt_id를 레코드의 1급 필드로 승격 (결정 필요 사항)

권고: 승격한다. 근거:

  • 엔진의 expected_attempt는 이미 "레코드가 attempt를 대지 못하면 거부"로 fail-closed다(Round 1 R2). 자동완료가 이 게이트를 통과하려면 레코드가 attempt를 대야 한다.
  • iteration 대체는 우연한 일치에 의존한다. 두 개념이 갈라지는 순간 CAS 가드가 조용히 무력화된다.

구현:

  • register_job이 attempt_id: 1을 기록. engine의 redispatch가 iteration과 함께 attempt_id를 증가.
  • 엔진의 record.get("attempt_id", record.get("iteration")) 대체를 제거하고 attempt_id만 본다(대체는 의미 혼동의 원천).
  • 스키마 영향: 필드 추가이므로 기존 리더는 무시하면 되고 SCHEMA_VERSION은 1을 유지한다. 다만 Phase 1이 "레코드 필드 불변"을 계약으로 명시했으므로 이 계획서가 그 계약의 변경을 명시적으로 요청한다. 리뷰어 승인 없이 진행하지 않는다.
  • 마이그레이션: attempt_id가 없는 기존 레코드는 iteration 값으로 1회 채운다(읽기 시 lazy, 쓰기는 다음 전이에서).

D3. 자동완료 게이트 (기존 판정기에 증거를 공급)

shadow.ShadowRunner를 AutoCompleteRunner로 확장하되 판정 로직은 손대지 않는다. 순서:

  1. watcher.wait_once로 정지 상태 관측 → collector_attribution 봉투에 4식별자를 채워 증거 구성.
  2. report-manifest.json 읽기 → 해시 재계산 후 대조(매니페스트 자기 신고를 신뢰하지 않는다) → report_manifest.attempt_id를 증거에 포함.
  3. policy.decide → candidate_approved가 아니면 종료(로그만).
  4. candidate_approved면 게이트 3중 확인 후 engine.transition(job_id, "completed", expected_attempt=<attempt>, source="auto_complete").
    • 게이트: 플래그 ON + 백엔드 허용목록(§1-5) + 잡별 opt-in.
  5. 커밋 결과가 committed=False면 아무 일도 없었던 것으로 처리하고 감사에 기록(늦은 conflict는 엔진이 이미 거부한다 — 새 코드 불필요).

D4. 지연 알림 (P2B-02) — 레코드 안 outbox

state-transitions.md §5가 독립 Outbox DB 신설을 금지했다. 따라서:

  • engine.transition이 커밋할 때 같은 락 안에서 pending_notification: {event, seq, timestamp, signature}를 레코드에 함께 기록한다(추가 I/O 없음 — 이미 쓰는 레코드에 필드 하나).
  • 락 해제 후 notifier.flush(job_id)가 MQTT 발행 → 성공 시 pending_notification을 지우는 별도 CAS 전이(request="notification_settled", 상태 불변).
  • 재시작 복구: 기동 시 pending_notification이 남은 레코드를 스캔해 재발행. 멱등하다 — 구독자가 seq로 중복을 거른다(기존 replay 방어).
  • 락 규율 유지: 발행은 절대 락 안에서 하지 않는다(Phase 2a G5 테스트가 계속 감시).

D5. 부수효과 증거 (P2B-01 후반)

  • 레코드의 기존 expected_artifacts(이미 존재, --artifact로 설정)를 선언된 부수효과 목록으로 사용한다. 새 필드를 만들지 않는다.
  • expected_artifacts가 비어 있지 않은 job은 매니페스트의 side_effects가 모든 항목을 덮어야 자동완료 후보가 된다. 하나라도 증거가 없으면 hold.
  • 증거 형식은 {kind, reference, verified_at}이며, Phase 2b는 존재와 대응만 검증한다(원격 확인은 비범위 — 정직하게 명시).

D6. 플래그와 감사 표식 (P2B-03)

스위치 기본 범위
MAM_AUTO_COMPLETE 0 (OFF) 전역 킬 스위치. 1이어야 자동완료가 가능
백엔드 허용목록 claude,grok,codex §1-5 실측 근거. MAM_AUTO_COMPLETE_BACKENDS로 좁힐 수만 있게(넓히려면 증거 필요)
잡별 opt-in 레코드 auto_complete: true 셋 다 참일 때만 커밋

감사 표식 필수: 자동완료로 들어간 terminal은 completed_by: "auto_complete"와 판정 근거(매니페스트 해시, 관측 seq)를 감사 로그에 남긴다. §1-4에서 확인했듯 래퍼는 자동/명시를 구별하지 못하므로, 사후에 구별할 수 있는 유일한 수단이다.

D7. 무판정 fail-closed

정책이 예외를 던지거나 알 수 없는 결정을 반환하면 hold로 취급한다. 자동완료 경로 전체를 try/except로 감싸 어떤 실패도 커밋으로 이어지지 않게 한다. "판정 못 함"은 절대 "완료"가 아니다.

D8. 트리·권한 규칙 승계

  • lib_py.loop는 lib_py.delegation 외 형제 임포트 금지(Phase 2a G9).
  • 자동완료의 source는 auto_complete 이며 운영자 권한이 아니다 — terminal을 되돌릴 수 없다.
  • preexec_fn: 알림 재발행기가 스레드를 쓰면 Phase 1 인계 게이트에 걸린다. 2b는 데몬을 스폰하지 않고 기존 구독자 프로세스 안에서 flush한다. 스폰이 필요해지면 그때 preexec_fn을 교체한다.

3. 작업 분해 (WBS) — 커밋 5개

P2B-00 특성화 선행 (커밋 1: test(issue-5): phase 2b pre-conditions and fail-closed characterization)

  • 현행 고정: 매니페스트 없는 job은 hold(이미 참), MAM_AUTO_COMPLETE 부재 시 자동완료 0건, attempt_id 부재 시 expected_attempt가 거부, 미지원 백엔드 목록.
  • 완료 기준: 레거시에서 전부 통과, 664 + 신규 통과.

P2B-01 확정 계약 (커밋 2: feat(issue-5): report finalization manifest and side-effect evidence)

  • 파일: lib_py/loop/finalize.py(매니페스트 작성·검증), publish_event.py에 --finalize-report/--side-effect, create_session.sh:483·래퍼 :202/:319 지시문 1줄 추가.
  • 테스트: tmp→rename 원자성, 해시 재계산 대조(위조 매니페스트 거부), 리포트 재작성 시 무효화, expected_artifacts 미충족 시 hold, 매니페스트 부재 시 hold(불변).

P2B-02 CAS 연동 + 지연 알림 (커밋 3: feat(issue-5): auto-complete gate and durable pending notification)

  • 파일: lib_py/loop/autocomplete.py, lib_py/delegation/notifier.py, engine에 pending_notification 기록 및 notification_settled 요청.
  • 테스트: 게이트 3중 확인, 늦은 conflict 거부(엔진 기존 동작), 크래시 후 재시작 재발행, 중복 발행 멱등, 락 안 발행 0건(Phase 2a G5 재사용), attempt_id 불일치 시 거부.

P2B-03 플래그·허용목록 (커밋 4: feat(issue-5): default-off auto-complete flag with an evidence-backed backend allowlist)

  • 파일: 플래그 3종, completed_by 감사 표식.
  • 테스트: 기본 OFF에서 자동완료 0건이며 명시 경로 결과가 바이트 동일, 허용목록 밖 백엔드는 플래그 ON에서도 미커밋, 잡별 opt-in 미설정 시 미커밋, 자동완료 terminal에 completed_by 존재.

P2B-04 시나리오·문서 (커밋 5: docs(issue-5): phase 2b contracts, scenarios and task checklist)

  • 경쟁/재시작/부분 리포트/부수효과 미충족/무판정 시나리오를 Phase 0 스키마로 추가(기존 validate_watcher_scenario 통과 필수).
  • state-transitions.md §4·§5 구현 매핑, task.md P2B-01~03 증거, 운영 노트(phase2b-autocomplete.md).

4. 게이트

# 조건 측정
G1 기존 664 무회귀 + 신규 통과, skipped 0 pytest tests/ -q
G2 셸 문법·공백 bash -n, 범위 git diff --check
G3 Python 3.9 컴파일 compileall
G4 기본 OFF에서 자동완료 0건 플래그 미설정 시 레지스트리 전후 해시 동일, 명시 경로 결과 바이트 동일
G5 매니페스트 위조 거부 해시 불일치·attempt 불일치·부재 전부 hold
G6 락 안 I/O 0건 Phase 2a G5 감시 테스트 확장(발행 포함)
G7 재시작 복구 크래시 시뮬레이션 후 pending_notification 재발행 및 멱등
G8 롤백 MAM_AUTO_COMPLETE=0 즉시 중단, 이미 확정된 terminal은 보존
G9 허용목록 증거 없는 백엔드는 플래그 ON에서도 미커밋

5. 롤백

  1. 즉시: MAM_AUTO_COMPLETE=0. 신규 자동완료만 중단되고 이미 확정된 terminal 레코드는 보존한다(implementation_plan.md Phase 2b 롤백 조건 그대로).
  2. MAM_ENGINE_CAS=0은 Phase 2a 경로까지 함께 되돌린다(자동완료는 CAS 위에 있으므로 자동으로 무력화).
  3. 커밋 단위 revert는 5→2 순. 커밋 1(특성화)은 남겨도 무해.

6. 리스크

# 리스크 완화
R1 워처가 스스로 매니페스트를 만들어 자기 증명이 됨 D1: 작성 주체를 에이전트로 고정, 워처는 해시 재계산 대조만
R2 attempt_id/iteration 혼동으로 CAS 가드 무력화 D2: 승격 + 대체 제거. 리뷰어 승인 전 착수 금지
R3 잘못된 자동완료를 래퍼가 구별 못 함 D6 completed_by 감사 표식 + 기본 OFF + 허용목록
R4 지연 알림이 새 저장소를 만들어 계약 위반 D4: 레코드 내 필드, §5 명시 준수
R5 알림 재발행기가 스레드를 써 preexec_fn 게이트에 걸림 D8: 2b는 스폰 없음
R6 부수효과 "검증"을 과대 주장 D5: 존재·대응만 검증한다고 문서에 명시, 원격 확인은 비범위

7. 명시적 비범위

  • run_loop.sh 파이썬화 — Phase 3.
  • 부수효과의 원격 확인(실제 git push 도달 여부 등) — 증거 대응까지만.
  • agy/hermes/opencode 자동완료 — 정지 상태 실측 증거 확보 후 별도 판단.
  • preexec_fn 교체 — 스폰이 필요해지는 시점.
  • 스키마 v2 — attempt_id 추가는 v1 유지(추가 필드).

8. Worker 인수인계 체크리스트

  • 착수 전: §D2(attempt_id 승격 = Phase 1 스키마 계약 변경)에 대한 리뷰어 승인을 받을 것
  • P2B-00 커밋 1: 현행 fail-closed 특성화가 레거시에서 통과
  • P2B-01 커밋 2: 매니페스트 작성/검증, 해시 재계산, 부수효과 대응
  • P2B-02 커밋 3: 게이트 연동 + pending_notification + 재시작 복구
  • P2B-03 커밋 4: 플래그 3종 + 허용목록 + completed_by
  • P2B-04 커밋 5: 시나리오·문서
  • 보고서: G1 원문, G4 해시 비교, G7 복구 로그, grep -n 인용
  • 커밋 접두: test/feat/feat/feat/docs(issue-5)

부록 A. 계획 수립 시 실행한 검증 명령

git log --oneline -1                                                      # 4a5916a
.venv/bin/python -m pytest tests/ --collect-only -q | tail -1             # 664
find .mam/jobs -name "*manifest*"                                         # 없음
grep -rn "pending_notification\|MAM_AUTO_COMPLETE" --include='*.py' .agents  # 0건
grep -n 'record.get("attempt_id", record.get("iteration"))' .../engine.py # 대체 확인
python -c "... transition(completed, expected_attempt=1|2) ..."           # iteration 대체 실증
python -c "... herdr/manifest.json observed slots ..."                    # 백엔드 증거 행렬
grep -n "publish_event.py --job" create_session.sh 래퍼                    # 지시문 주입 2곳