Files
multi-agent-mux/.agents/reports/creator-claude-01/plan-2770df36.md
T

27 KiB

본 리포트는 Job 2770df36(role: Planner)의 산출물인 Phase 2a 구현 계획서다. durable 사본: .agents/reports/creator-claude-01/plan-2770df36.md (커밋됨).

범위 명시: 본 job의 지시는 "구현 계획서를 작성"이며, 목표 문장의 (5) 테스트 작성·(6) 리뷰어 만장일치 PASS는 이 계획을 실행할 후속 Worker/Reviewer job의 완료 조건이다. 본 계획서는 그 job들이 그대로 실행할 수 있도록 커밋 5개·게이트 9개·인수인계 체크리스트까지 명세했다.


📑 Issue #5 Phase 2a 구현 계획서 — lib_py/loop/ 코어 & Shadow Watcher

  • Job: 2770df36 / Planner: creator-claude-01
  • 기준 커밋: 015ab70 (branch refactor, working tree clean) / Python 3.9.6 / paho-mqtt 2.1.0 / herdr 0.8.2 / 전체 테스트 수집 576
  • 요구 출처: implementation_plan.md §2 Phase 2a 행, task.md P2A-01P2A-04, state-transitions.md §2§5
  • 문서 성격: Worker가 그대로 실행할 수 있는 단계별 명세. §1의 모든 인용은 계획 수립 중 라이브 코드·라이브 CLI로 직접 확인했다.

0. 요약 (TL;DR)

  1. Phase 2a는 레지스트리에 단 1바이트도 쓰지 않는다. Shadow watcher는 "Would-Complete" 판정을 로그로만 남긴다. 이 불변식을 문장이 아니라 구조와 테스트로 강제한다(§D5, G4).
  2. CAS 경계는 신규 파일이 아니라 기존 writer 3곳을 한 함수 뒤로 모으는 작업이다. 현재 상태 writer는 3개이고 그중 하나(pick_pending)는 이미 문서화된 chokepoint를 우회한다(§1-2 실측).
  3. 4중 귀속에 필요한 필드가 job 레코드에 없다(attempt_id/agent_session_uuid/pane_process_generation 전부 부재, §1-4). Phase 1이 레코드 스키마 불변을 약속했고 Phase 2a는 레지스트리 쓰기가 금지되므로, 귀속 증거는 shadow 전용 저장소에 둔다. 레코드 스키마 결정은 Phase 2b로 미룬다.
  4. herdr agent wait --timeout의 단위는 밀리초다(라이브 --help 확인, Phase 0 fixture도 integer_ms로 정확히 기록). 초 단위로 오해하면 1000배 오차가 난다.
  5. 락 안에서 감사 로그 I/O가 일어나고 있다(update_job_status가 락 보유 중 update_logged_status/append_event 호출, §1-3). Phase 2a는 이를 "락 안에서 결정 + 알림 큐잉 → 락 해제 → 밖에서 flush"로 바꾼다.
  6. 판정 로직의 테스트 벡터는 새로 만들지 않는다. Phase 0에서 3인 리뷰로 검증된 watcher 시나리오 7종과 그 expected_policy가 그대로 엔진의 결정 테이블 테스트가 된다(§D7).

1. Planner 사전 실측

1-1. 대상 파일과 규모

항목 실측
기준 커밋 015ab70
전체 테스트 576 수집 (Phase 1 종료 시점)
lib_py/ 기존 패키지 agents/, delegation/, atomic_yaml.py, layout.py, paths.py, state.py, verify_session.py, workspace_uuid.py
MAM_ENGINE_CAS 아직 존재하지 않음 (grep 결과 implementation_plan.md 언급 1건뿐) — Phase 2a가 신설

1-2. job 레코드 상태 writer 전수 조사 (P2A-01의 실제 범위)

_atomic_write_record 직접 호출자 = 5곳, 그중 상태를 바꾸는 것은 3곳이다.

# 위치 하는 일 CAS 여부
W1 mqtt_common.py:410 (update_job_status) record.update(fields) 무조건 병합 ❌ 없음
W2 registry.py:131 (pick_pending) 락 안에서 status == "pending" 필터 후 running으로 기록 ⚠️ 사실상 CAS이나 W1을 우회한 별도 구현
W3 mqtt_common.py:433 (next_seq) last_seq 증가 (상태 무관) 해당 없음
— registry.py:107 (register_job) 레코드 생성 해당 없음
— registry.py:151 (update_status) → W1
— registry.py:365 (update CLI) → W1
— publish_event.py:179 (record_outcome) → W1

핵심 (정확히): update_job_status의 docstring은 자신을 "the single chokepoint for status writes"라고 선언하지만, registry.py:135의 주석이 스스로 인정하듯 pick_pending은 이를 우회한다.

단, pick_pending이 틀린 것은 아니다. 락 안에서 status == "pending"을 필터한 뒤 기록하므로 pending → running에 대해서는 이미 올바른 compare-and-swap이다(registry.py:123). 문제는 정확성이 아니라 중복이다: CAS 판정과 감사 로그 mirror가 두 벌로 존재하며, Phase 1의 O-1(1회성 전송 이중 구현)에서 확인했듯 같은 로직의 사본은 한쪽에만 결함이 생기는 경로다.

따라서 P2A-01은 "없는 CAS를 새로 만드는" 작업이 아니라 "한쪽에만 있는 CAS를 모든 전이로 일반화하고 두 사본을 하나로 합치는" 작업이다. 실제로 CAS가 전혀 없는 것은 W1(record.update(fields) 무조건 병합)이다.

셸/래퍼 측은 job 레코드를 직접 쓰지 않는다: 래퍼는 registry.py status --set(→W1)을, reconcile.sh:192는 registry.load_job(읽기 전용)만 쓴다. run_loop.sh:254는 job JSON을 읽기만 한다(3초 폴링). 즉 모든 쓰기가 파이썬 안에 있다 — CAS 경계를 파이썬 한 곳에 두면 전수 커버가 가능하다.

1-3. 현재 락 트랜잭션의 I/O 실측

update_job_status(:405-418)는 registry_lock 보유 중에 다음을 수행한다:

  • load_job (읽기 I/O)
  • _atomic_write_record (쓰기 + fsync + os.replace)
  • update_logged_status (감사 로그 디렉터리 쓰기)
  • append_event (감사 로그 append)

네트워크/서브프로세스는 락 안에 없다(publish_event.main의 순서: next_seq(락) → 발행(네트워크, 락 없음) → record_outcome(락)). 이 성질은 Phase 2a에서 반드시 보존해야 하며, 추가로 감사 로그 I/O를 락 밖으로 빼는 것이 P2A-01의 "락 내 I/O 배제" 요구다.

1-4. 4중 귀속에 필요한 필드 부재 (설계를 좌우하는 사실)

라이브 job 레코드의 실제 키:

agent, agent_session, auth_token, broker, created_at, expected_artifacts,
idle_timeout_sec, iteration, job_id, job_type, last_seq, max_iterations,
prompt, reviewer, reviewer_session, role, schema_version, status,
timeout_sec, topic_prefix, updated_at

state-transitions.md §2가 요구하는 4중 귀속 중 attempt_id·agent_session_uuid·pane_process_generation이 전부 없다. iteration(루프 회차)은 있으나 attempt와 의미가 다르다.

동시에 Phase 1은 "스키마 불변(SCHEMA_VERSION = 1, 레지스트리 레코드 필드 불변)"을 계약으로 못박았고, P2A-03은 레지스트리 쓰기 자체를 금지한다. → Phase 2a에서 귀속 증거를 레코드에 넣는 것은 불가능하며, 넣어서도 안 된다. §D6이 이를 해결한다.

1-5. herdr agent wait 라이브 계약 (watcher.py의 입력)

Usage: herdr agent wait <TARGET> [OPTIONS]
  --until <STATUS>   idle|working|blocked|done|unknown  (반복 가능)
  --timeout <MS>     Fail after this many milliseconds
Without --until, matches idle, done, or blocked.
Without --timeout, waits indefinitely.
  • 단위는 밀리초다. Phase 0 fixture(wait-contract.json)도 "type": "integer_ms"로 정확히 기록되어 있다.
  • 성공 → exit 0, stdout에 {"id":"cli:agent:wait","result":{"agent":{...}}}.
  • 타임아웃/대상 부재 → exit 1, stderr에 에러 JSON(error.code = timeout / agent_not_found). stdout은 비어 있다.
  • --until 생략 시 기본 매칭은 idle,done,blocked — working은 매칭되지 않는다. watcher는 항상 --until을 명시한다.
  • 단일 wait는 1개의 최종 스냅샷만 주는 동기 장벽이다(herdr-runtime.md §2-1). 연속 관측은 반복 호출로 구성해야 한다.

1-6. Phase 0에서 이미 검증된 판정 벡터

tests/fixtures/issue_5/watcher/의 7종과 expected_policy.decision은 3인 교차 리뷰를 통과한 계약이다. Phase 2a 엔진의 결정 함수는 이 7종을 그대로 입력받아 같은 결정을 내야 한다.

시나리오 기대 결정
initial_idle hold (실행 앵커 없음)
tool_running hold (도구 실행 중)
report_pause_15s hold (매니페스트 없음 — 디바운스만으로 완료 금지)
finalized_candidate candidate_approved
blocked_permission hold_blocked
transport_disconnected hold_reconnect
restarted_generation reject_stale

dispatch_context 필드: job_id, attempt_id, agent_session_uuid, pane_process_generation, dispatch_ack, execution_anchor_observed.

1-7. Phase 1에서 넘어온 인계 사항 (본 Phase가 게이트)

  1. preexec_fn (3인 리뷰어 공통 지적): daemon.spawn은 preexec_fn으로 SIGHUP을 무시하는데 이는 멀티스레드 프로세스에서 안전하지 않다. Shadow watcher가 스레드를 쓰면서 DelegationService.ensure_subscriber_daemon을 호출하면 이 게이트에 걸린다(§D9).
  2. O-4 (Claude): freeze 트리의 _delegate_py_bin은 .venv를 찾지 못해 paho 없는 시스템 python으로 떨어진다. Phase 2a가 freeze lib.sh 헬퍼를 호출하지 않는 한 무해하다(§D9에서 재확인).
  3. 연결 지문 하드코딩 필드 목록: BrokerConfig 필드 추가 시 조용히 누락된다. Phase 2a에서 dataclasses.fields() 유도로 바꾸거나 커버리지 단정 테스트를 추가한다(§3 P2A-04에 포함).

2. 설계 결정 (Design Decisions)

D1. 패키지 배치

.agents/skills/lib_py/loop/
  __init__.py        # 공개 API 재수출
  evidence.py        # 증거 튜플 정규화 (watcher → engine 사이의 유일한 자료형)
  attribution.py     # 4중 귀속 + dispatch anchor 판정 (순수 함수)
  policy.py          # 결정 함수: 증거 -> hold|hold_blocked|hold_reconnect|reject_stale|candidate_approved
  engine.py          # 단일 전이 주체 + CAS 트랜잭션 경계
  watcher.py         # herdr agent wait 래핑, 재연결 백오프, 스냅샷 수집
  shadow.py          # Shadow 러너: watcher -> policy -> "would-complete" 로깅 (쓰기 없음)
  store.py           # shadow 전용 귀속/증거 저장소 (레지스트리와 분리)
  • policy.py는 순수 함수여야 한다(I/O·시계·전역 상태 금지). Phase 0 fixture 7종을 그대로 먹여 테스트하기 위한 필수 조건이다.
  • D1(b) 재적용: lib_py.loop는 lib_py.delegation 외의 형제 lib_py 모듈을 임포트하지 않는다. 이유는 Phase 1과 동일하다 — freeze 하에서 부모 lib_py와 바인딩된 패키지가 다른 트리일 수 있다. AST grep 테스트로 고정한다(§D8).
  • Python 3.9 호환 필수(match, PEP 604 런타임 유니온 금지). 게이트에서 compileall.

D2. 단일 CAS 전이 경계 (P2A-01)

engine.transition()이 유일한 상태 writer가 된다.

# lib_py/loop/engine.py
class TransitionResult(object):
    __slots__ = ("committed", "from_status", "to_status", "reason", "record", "notifications")

def transition(job_id, request, *, registry_dir, expected_attempt=None,
               expected_status=None, fields=None, source="unknown") -> TransitionResult:
    """유일한 상태 전이 주체.

    락 안에서 하는 일: load -> CAS 판정 -> (허용 시) 기록 -> 알림 큐잉.
    락 밖에서 하는 일: 알림 flush(감사 로그), MQTT 발행.
    """

CAS 규칙은 state-transitions.md §3 표를 그대로 구현한다:

현재 요청 조건 결과
pending claim/started 세션·attempt 일치 running
running progress/permission_required — running (메타만 갱신)
running explicit terminal 유효 세션 서명 요청 terminal
terminal 동일 terminal 재수신 — No-op 멱등 ACK
terminal 상충 신호 — 거부 + conflict 감사 기록
임의 stale attempt/generation 불일치 거부 + stale 경고
running deadline_timeout 타임아웃 확인 error

기존 writer 3곳의 라우팅:

  • W1 update_job_status → 내부적으로 engine.transition(request="merge", ...)를 호출하도록 교체. 시그니처·반환값·예외는 불변(Phase 1의 G-1~G-4가 이 함수 계약에 걸려 있다).
  • W2 pick_pending → engine.transition(request="claim", expected_status="pending")로 교체. 이로써 우회가 사라진다.
  • W3 next_seq → 상태를 바꾸지 않으므로 그대로 두되, 같은 락을 쓰므로 트랜잭션 시간 예산에만 포함한다.

롤백 스위치 MAM_ENGINE_CAS(기본 1): 0이면 W1/W2가 현재의 무조건 병합 본체로 분기한다. Phase 1의 MAM_DELEGATION_PY와 동일한 패턴이며, legacy 본체는 _*_legacy로 보존한다(제거는 Phase 4).

D3. 락 규율 — 락 안에서는 결정만, I/O는 밖에서

with registry_lock(registry_dir):        # 짧게
    record = load_job(...)
    decision = _cas_decide(record, request, expected_attempt, expected_status)
    if decision.commit:
        _atomic_write_record(...)        # 레코드 1회 쓰기만 허용
    pending = decision.notifications     # 감사 로그는 "쓰지 않고" 큐에 담는다
# 락 해제 후
_flush_notifications(pending)            # 감사 로그 append / status mirror
  • 금지: 락 보유 중 subprocess, 소켓, MQTT, herdr 호출, time.sleep.
  • 허용: load_job 1회 + _atomic_write_record 1회.
  • 감사 로그 mirror(update_logged_status/append_event)를 락 밖으로 옮긴다. 순서 보장 손실 우려: 현재는 락 안이라 감사 이력이 직렬화된다. 이를 유지하기 위해 알림에 **CAS가 부여한 단조 transition_seq**를 실어 보내고, flush가 늦게 도착해도 순서를 복원할 수 있게 한다.
  • 이 규율은 문장이 아니라 테스트로 강제한다: 락 획득 중 subprocess.run/socket.socket/time.sleep을 monkeypatch로 감시해 호출 0건을 단정한다(G5).
  • 저장소에 이미 선례가 있다: pick_pending은 레코드 쓰기를 락 안에서 끝내고 감사 로그 mirror(update_logged_status/append_event)를 락 해제 후에 수행한다(registry.py:131-145). D3은 새 패턴을 발명하는 것이 아니라 이 기존 패턴을 update_job_status에도 적용해 일반화하는 것이다.

D4. watcher.py — herdr agent wait 래핑

def wait_once(target, until=("idle", "done", "blocked"), timeout_ms=15000, run=subprocess.run) -> WaitOutcome
  • --timeout은 밀리초로 전달한다(§1-5). 초 단위 인자를 받지 않는다 — 단위 혼동을 타입 이름(timeout_ms)으로 차단한다.
  • --until을 항상 명시한다(생략 시 working이 빠지는 기본값에 의존하지 않는다).
  • exit 0 → stdout JSON 파싱 → result.agent 스냅샷.
  • exit 1 → stderr JSON 파싱 → error.code로 분기: timeout(정상적 무이벤트), agent_not_found(대상 소멸).
  • 그 외 exit code / JSON 파싱 실패 / 실행 실패 → transport_error.
  • 재연결 백오프: transport_error 연속 발생 시 1s → 2s → 4s → … → 최대 16s. 백오프 대기는 락 밖에서만 한다. 복구 시 캐시된 스냅샷을 폐기하고 재스냅샷한다(transport_disconnected → hold_reconnect 정책과 일치).
  • watcher는 판정하지 않는다. 증거 튜플만 만들어 큐에 넣는다(Phase 0 합의: watcher는 관측, engine은 결정).

D5. Shadow 모드 — 쓰기 불가를 구조로 보장 (P2A-03)

# shadow.py
class ShadowRunner:
    def __init__(self, ..., decide=policy.decide, sink=logging_sink):
        ...   # engine 을 아예 주입받지 않는다
  • ShadowRunner는 engine.transition에 대한 참조를 갖지 않는다. "쓰지 않기로 약속"이 아니라 쓸 수단이 없다. 이것이 P2A-03을 문장이 아니라 구조로 만드는 방법이다.
  • 판정 결과는 would_complete 레코드로 shadow 전용 로그에만 남는다: .mam/shadow/<job_id>.ndjson (레지스트리와 다른 디렉터리).
  • 검증(G4): shadow 러너를 시나리오 전량으로 돌린 전후에 레지스트리 디렉터리 전체의 파일 목록·mtime·SHA-256이 바이트 동일함을 단정한다. "status를 안 썼다"보다 강한 단정이다.

D6. 귀속 증거는 shadow 전용 저장소에 (§1-4 결론)

store.py가 .mam/shadow/attribution/<job_id>.json을 소유한다. 담기는 것:

{ "job_id": "...", "attempt_id": 1, "agent_session_uuid": "...",
  "pane_process_generation": "w2X:p1:1234:2026-09-06T00:00:00Z",
  "dispatch_ack": true, "dispatch_ack_at": "...", "execution_anchor_observed": false }
  • job 레코드는 건드리지 않는다 → Phase 1의 스키마 불변 계약과 P2A-03을 동시에 만족한다.
  • attempt_id는 Phase 2a에서 shadow가 자체 부여한다(디스패치 관측 시 1부터). Phase 2b에서 레코드로 승격할지는 그때 결정한다 — 이 계획서는 레코드 스키마를 바꾸지 않는다.
  • pane_process_generation은 <pane_id>:<pid>:<start_iso> 형식이며, Phase 0의 정체성 검증(_validate_observed_herdr_item)이 이미 이 형식을 강제한다.

D7. 판정 테스트 벡터 = Phase 0 fixture (P2A-04의 절반)

policy.decide(scenario) -> decision 이 Phase 0의 7종 시나리오에 대해 expected_policy.decision과 정확히 일치해야 한다. 신규 fixture를 만들기 전에 기존 계약을 먼저 만족시킨다. 이렇게 하면:

  • 판정 로직이 3인 리뷰를 통과한 계약에 묶인다.
  • Phase 0의 validate_watcher_scenario가 fixture 무결성을 계속 지킨다.

추가 시나리오(P2A-04 요구)는 같은 스키마로 작성하고 validate_watcher_scenario를 통과해야 한다: race_double_dispatch, attempt_reuse, partial_report, unknown_status, restart_midflight.

D8. 트리 일관성 (Phase 1 R1의 교훈 재적용)

  • lib_py.loop는 lib_py.delegation만 형제로 임포트한다. 그 외 lib_py.* 금지 → AST 테스트.
  • 루프 프로세스는 shim을 인프로세스로 임포트하지 않으므로 lib_py.loop와 lib_py.delegation은 부모 lib_py를 통해 같은 트리로 해석된다. 이를 테스트로 고정한다(__file__ 접두 비교).
  • Shadow 러너를 서브프로세스로 띄울 때는 Phase 1의 _delegation_py와 동일한 명시 부트스트랩을 쓴다. python -m은 cwd가 PYTHONPATH를 이기므로 금지한다(R1 재발 방지).

D9. Phase 1 인계 게이트 해소

  • preexec_fn: Shadow watcher가 스레드를 쓸 경우 daemon.spawn의 preexec_fn이 위험해진다. Phase 2a에서는 shadow 러너가 데몬을 스폰하지 않는다(관측만). ensure_subscriber_daemon 호출 금지를 grep 테스트로 고정하고, 실제 제거/대체는 Phase 2b 스폰 도입 시 처리한다.
  • O-4: shadow는 freeze lib.sh 헬퍼를 호출하지 않으므로 인터프리터 해석 문제에 노출되지 않는다. 호출 0건을 grep으로 고정한다.
  • 연결 지문 필드 목록: dataclasses.fields(BrokerConfig) 전량이 지문에 포함됨을 단정하는 테스트를 추가한다(필드 추가 시 조용히 누락되는 것을 차단).

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

P2A-00 특성화 선행 (커밋 1: test(issue-5): phase 2a writer inventory and lock discipline characterization)

  • 파일: tests/test_issue5_phase2a_loop.py(신규)
  • 현행 동작을 먼저 고정한다(Phase 1에서 효과가 입증된 순서):
    • X-01 update_job_status가 무조건 병합임을 단정(현행 사실).
    • X-02 pick_pending의 현행 계약 고정: pending만 claim하고 동시 claim 시 한 세션만 이긴다(라우팅 후에도 이 성질이 유지되어야 한다).
    • X-03 락 보유 중 감사 로그 I/O가 발생함을 단정(현행 사실 — D3 이후 이 테스트는 반대로 뒤집힌다).
    • X-04 next_seq/register_job/record_outcome 경로의 현행 부작용 스냅샷.
  • 완료 기준: 레거시 코드에서 전부 통과, 576 + 신규 통과.

P2A-01 엔진·CAS 경계 (커밋 2: feat(issue-5): single CAS transition boundary for every job-status writer)

  • 파일: lib_py/loop/{__init__,engine,evidence,attribution}.py; mqtt_common.update_job_status·registry.pick_pending 라우팅; MAM_ENGINE_CAS 분기 + _*_legacy 보존.
  • 테스트: state-transitions.md §3 표 9행 전부에 대한 전이 테스트, 멱등 재수신, 상충 거부 + conflict 감사, stale attempt 거부, MAM_ENGINE_CAS=0 양 경로 parametrize, 락 내 I/O 0건 단정(subprocess/socket/sleep monkeypatch).
  • 완료 기준: Phase 1의 G-1~G-4가 그대로 통과(계약 불변 증명), 전체 스위트 무회귀.

P2A-02 watcher (커밋 3: feat(issue-5): herdr agent wait watcher with reconnect backoff)

  • 파일: lib_py/loop/watcher.py, store.py
  • 테스트: 가짜 run으로 exit 0/stdout, exit 1/stderr timeout, exit 1/stderr agent_not_found, 파싱 실패, 실행 실패 5경로; 백오프 수열(1,2,4,8,16 상한)과 재연결 시 캐시 폐기; --timeout이 밀리초로 전달됨을 argv 단정; --until 항상 명시됨을 argv 단정. herdr 실제 호출 0건(전부 주입된 가짜).
  • 완료 기준: 위 통과 + 실제 herdr로 1회 스모크(수동, 보고서 첨부).

P2A-03 shadow 러너 (커밋 4: feat(issue-5): shadow watcher that logs would-complete without writing)

  • 파일: lib_py/loop/{policy,shadow}.py
  • 테스트: Phase 0 시나리오 7종 → policy.decide가 expected_policy.decision과 일치; 신규 시나리오 5종 추가(validate_watcher_scenario 통과 필수); 레지스트리 디렉터리 SHA-256 전후 동일 단정; ShadowRunner가 engine/update_job_status/_atomic_write_record를 참조하지 않음을 AST로 단정.
  • 완료 기준: shadow 전량 실행 후 레지스트리 바이트 불변.

P2A-04 통합·문서 (커밋 5: docs(issue-5): phase 2a contracts, scenarios and task checklist)

  • race/attempt 재사용/부분 리포트/도구 실행/blocked/unknown/재시작 시나리오 실행 결과 기록.
  • state-transitions.md에 구현 매핑 각주, task.md P2A-01~04 체크 + 증거, implementation_plan.md Phase 2a 상태 갱신, MAM_ENGINE_CAS 운영 문서.
  • Phase 1 인계 3건 처리 상태 명시(§D9).

4. 게이트

# 조건 측정
G1 기존 576 전부 통과 + 신규 전부 통과, skipped 0 pytest tests/ -q (약 13분)
G2 셸 문법·공백 bash -n, git diff --check (범위 기준)
G3 Python 3.9 컴파일 compileall .agents/skills/lib_py/loop
G4 Shadow가 레지스트리에 0바이트 기록 실행 전후 레지스트리 디렉터리 파일 목록·SHA-256 동일
G5 락 내 I/O 배제 락 보유 중 subprocess/socket/sleep 호출 0건 (monkeypatch 감시)
G6 모든 상태 writer가 CAS 경유 _atomic_write_record 직접 호출자가 engine·register_job·next_seq뿐임을 AST로 단정
G7 판정이 Phase 0 계약과 일치 시나리오 7종 + 신규 5종 expected_policy 일치
G8 롤백 MAM_ENGINE_CAS=0에서 특성화 통과 + 라이브 스모크
G9 트리 일관성 lib_py.loop/lib_py.delegation 동일 트리, 형제 임포트 0건

Worker 보고서에는 G1 pytest 요약 원문, G4의 해시 비교 출력, G5의 감시 로그, G7 표를 그대로 붙인다. file:line 인용은 grep -n 출력으로 뒷받침한다.


5. 롤백

  1. 즉시: MAM_ENGINE_CAS=0 → W1/W2가 보존된 legacy 무조건 병합으로 분기. Shadow는 애초에 쓰기가 없으므로 프로세스 종료만으로 흔적이 사라진다.
  2. 프로세스: Shadow 러너 종료 + 인메모리 큐 폐기. .mam/shadow/는 삭제해도 무해(레지스트리와 분리되어 있음).
  3. 커밋 단위: 5→4→3→2 순 git revert. 커밋 1(특성화)은 레거시에서도 통과하므로 남겨도 된다.

6. 리스크와 완화

# 리스크 완화
R1 CAS 도입이 Phase 1의 G-1~G-4 계약을 깬다 update_job_status 시그니처·반환·예외 불변 유지, 두 경로 parametrize
R2 감사 로그를 락 밖으로 빼면서 순서가 뒤바뀐다 CAS가 부여하는 단조 transition_seq를 알림에 실어 순서 복원
R3 pick_pending 라우팅이 이미 올바른 동시 claim 경쟁을 깨뜨린다 라우팅 전에 X-02로 현행 성질을 고정하고, expected_status="pending" CAS가 동일 성질을 유지함을 동시 claim 테스트로 검증
R4 --timeout 단위 혼동(초 vs ms) 인자명 timeout_ms, argv 단정 테스트
R5 shadow가 실수로 쓰기 경로를 얻는다 구조적 차단(engine 미주입) + AST 단정 + 해시 불변 단정
R6 freeze/live 트리 혼재 D8 (형제 임포트 금지 + 명시 부트스트랩)
R7 preexec_fn 스레드 비안전 Phase 2a는 데몬 스폰 없음(grep 고정), 실제 대체는 Phase 2b
R8 실브로커/실herdr 의존 테스트 전부 주입된 가짜, 실호출 0건. 라이브 스모크는 수동·정보성

7. 명시적 비범위

  • 자동완료 실제 커밋 — Phase 2b. Phase 2a는 would_complete 로깅까지만.
  • job 레코드 스키마 변경(attempt_id 등 승격) — Phase 2b.
  • run_loop.sh의 파이썬화 — Phase 3.
  • 래퍼·reconcile.sh의 상태 머신 변경 — 전 Phase 공통 불변.
  • preexec_fn 제거 — Phase 2b.
  • pgrep 다중 PID 정리, Windows — Phase 4 후보.

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

  • P2A-00 커밋 1: 현행 동작 특성화(W1 무조건 병합, W2 우회, 락 내 I/O 발생)가 레거시 코드에서 통과
  • P2A-01 커밋 2: engine.transition 단일 경계, W1/W2 라우팅, MAM_ENGINE_CAS 양 경로, 락 내 I/O 0건
  • P2A-02 커밋 3: watcher 5경로 + 백오프 + ms 단위 argv 단정
  • P2A-03 커밋 4: Phase 0 시나리오 7종 일치 + 신규 5종, 레지스트리 해시 불변
  • P2A-04 커밋 5: 문서·체크리스트·인계 3건 처리 상태
  • 보고서: G1 원문, G4 해시 출력, G5 감시 로그, G7 표, grep -n 인용
  • 커밋 접두: test(issue-5), feat(issue-5), feat(issue-5), feat(issue-5), docs(issue-5)

부록 A. 계획 수립 시 실행한 검증 명령 (재현용)

git log --oneline -1                                                     # 015ab70
.venv/bin/python -m pytest tests/ --collect-only -q | tail -1            # 576
grep -rn "_atomic_write_record" --include='*.py' .agents | grep -v "def " # writer 5곳
sed -n '125,155p' .agents/skills/lib_py/delegation/registry.py            # pick_pending 우회
sed -n '395,420p' .agents/skills/lib_py/delegation/mqtt_common.py         # 락 내 감사 I/O
HERDR_SESSION_NAME=multi-agent-mux herdr agent wait --help                # --timeout <MS>
cat tests/fixtures/issue_5/herdr/wait-contract.json                       # integer_ms
grep -rn "MAM_ENGINE_CAS" --include='*.py' --include='*.sh' .             # 미존재
.venv/bin/python -c "import json,glob; print(sorted(json.load(open(sorted(glob.glob('.mam/jobs/*.json'))[-1]))))"  # 레코드 키 (attempt_id 부재)