0087: opt-in OMC team 병렬 실행 runner 백엔드 (--runner omc) — 데이터-경계 ack fail-closed + no-auto-merge gate 라우팅
0087: opt-in OMC team 병렬 실행 runner 백엔드 (--runner omc) — 데이터-경계 ack fail-closed + no-auto-merge gate 라우팅
- Status: accepted
- Date: 2026-05-29
- Deciders: User, Claude Code
- Related: ADR 0085 (무한 모드 / unlimited caps), ADR 0086 (lane Tool/Sandbox 정책), ADR 0080 (registry v2 / dual-agent lanes), ADR 0061 (외부 API 데이터-경계 3조건), ADR 0005 (private 데이터 경계), ADR 0001 (baseline byte-identical 보존), ADR 0007
- Issue: #1679
Context
in-repo active-codex-runner의 --max-parallel은 가짜 동시성이다: subprocess.Popen을
배치로 띄운 뒤 순차로 wait하며, 실제로는 선택된 session count ≤ max-parallel만 검증한다.
OMC의 omc team은 진짜 동시 tmux worker를 per-worker git-worktree 격리로 제공한다.
조사 중 CRITICAL SAFETY FINDING: omc team은 per-worker sandbox / permission / network
플래그를 전혀 노출하지 않는다. 그 worker(claude/codex CLI)는 자체 DEFAULT 권한으로
돌아 — in-repo runner의 명시적 --sandbox read-only(codex) / tool allowlist(claude)보다
덜 통제된다. 즉 OMC worker는 worktree의 비공개 데이터를 읽고 네트워크로 egress할 수 있으며,
--auto-merge는 worker commit을 leader 브랜치로 머지한다. 이는 ADR 0086의 danger-full-access와
같은 ADR 0005 데이터-경계 리스크지만 더 강하다(sandbox 옵션 자체가 없음). 유지보수자는 이를
명시적 OPT-IN으로, 데이터-경계 acknowledgment 뒤에 gating해 ship하기로 결정했다.
Decision
- (a)
--runner {codex,omc}opt-in, 기본 codex byte-identical.active-codex-runner와active-auto-loopargparse 양쪽에--runner(choicescodex/omc, defaultcodex)를 추가하고write_active_codex_runner(신규 keywordrunner: str = "codex") →write_active_auto_loop→ runner 호출로 thread한다. MakefileACTIVE_RUNNER ?= codex를active-codex-runner/active-auto-loop/시작target에--runner "$(ACTIVE_RUNNER)"로 넘긴다. 기본codex는 오늘과 byte-identical(ADR 0001 보존) — omc 분기는runner == "omc"일 때만 진입한다. - (b) omc worker는 uncontrolled → ack 없이 fail-closed.
runner=omc는 명시적 acknowledgment envACTIVE_OMC_RUNNER_ACK=1(모듈 상수OMC_RUNNER_ACK_ENV)을 요구한다. ack가 없으면 FAIL-CLOSED: blockedActiveCodexRunnerResult를 반환하고 (메시지 상수OMC_RUNNER_REQUIRES_ACK_MESSAGE) omc를 절대 spawn하지 않는다. omc team worker는 per-worker sandbox가 없어(network + private-data access) ADR 0005 경계를 완화하므로, 그 완화는 명시 opt-in으로만 허용한다(ADR 0061 데이터-경계 조건). - (c) adapter가 거버넌스를 재부과 + NO auto-merge → gate 라우팅. 어댑터
_run_omc_team_runner는 injectable callable(omc_runner=None, 기본 실 subprocess wrapper —popen_factoryinjection 패턴 미러)을 통해서만 omc를 부르므로 테스트는 실 omc를 절대 spawn하지 않는다. 명령은omc team N:claude,M:codex --no-decompose "<task>"를 envOMC_TEAM_WORKTREE_MODE=branch(per-worker git-worktree 격리)로 띄우되--auto-merge는 절대 넘기지 않는다.- ENV-var secrets 제거 (round-2 fix #1, round-4 fix #1 정정 — defense-in-depth,
완전한 자격증명 경계 아님):
_build_omc_env가_OMC_ENV_ALLOWLIST(PATH / HOME / USER / SHELL / TERM / LANG / LC_* / TMUX* / OMC_HOME / XDG_CONFIG_HOME / CI / GIT_{AUTHOR,COMMITTER}{NAME,EMAIL} / NODE_PATH)만 필터링해 전달한다. round-4 fix #1:OMC_CONFIG,XDG_CACHE_HOME,XDG_RUNTIME_DIR을 허용리스트에서 제거(omc에 불필요, 표면 축소). ANTHROPIC_API_KEY / OPENAI_API_KEY / GH_TOKEN / AWS* / DATABASE_URL 등 ENV-var 시크릿은 차단된다. 단, 이것은 완전한 자격증명 경계가 아니다. omc worker는 user의 자체 인증된 CLI(claude/codex)를 실행하며, 그 CLI는 파일시스템 경로(~/.codex, ~/.claude, ~/.config/gh, ~/.aws)에서 직접 자격증명을 로드한다. HOME은 CLI 인증에 필수이므로 유지한다. env 허용리스트는 defense-in-depth이며,ACTIVE_OMC_RUNNER_ACK=1ack가 home-scoped 자격증명 + 네트워크 egress 접근을 명시 수용하는 핵심 게이트다. - 단일 worker 강제 +
--read-agent우선 (round-2 fix #2, round-3 fix #2 강화):_resolve_omc_worker_mix는 총 worker 수를 항상 1로 강제한다(total_workers = 1). round-3 fix #2:read_agent: str = "auto"파라미터 추가. 명시적"claude"→ 무조건(1, 0),"codex"→ 무조건(0, 1),"auto"만 agent_mix 정책 majority로 fallback.read_agent는_run_omc_team_runner→write_active_codex_runner에서 thread된다. >1 worker를 launch하면 leader 외 diff가 묵묵히 버려지고 ADR 0005 노출이 배수로 늘어난다. multi-worker diff 캡처/머지는 follow-up. task_id전파 + standalone 파생 (round-2 fix #3, round-4 fix #2):_run_omc_team_runner에task_id: str | None파라미터를 추가해write_active_codex_runner의task_id를 흘린다. round-4 fix #2:--task없이 standalone으로 호출할 때(task_id is None) 선택된 registry session, 그 다음 전체 registry session 순으로T-YYYY-NNNNtask_id를 파생한다. 파생 불가 시 omc spawn 전 fail-closed (높은 비용의 run이write_active_apply거부 artifact를 만들지 않도록).write_active_apply는 유효한 task_id가 없으면 항상 거부한다.- heartbeat 무효화는 실행된 run에만 (round-2 fix #4):
_finalize_omc_runner_result에invalidate_heartbeats: bool파라미터를 추가한다. no-ack / dry-run(execute=False) / pre-spawn 차단(privacy 실패 등) 경로는 모두invalidate_heartbeats=False— omc team이 실제로 launch된 경우(try/finally 블록 진입 후)에만True를 전달한다. 이전 구현은 _finalize_omc_runner_result를 항상 호출해 no-ack/dry-run 경로에서도 registry를 오염시켰다. <task>강화 privacy scrub (round-1 fix #2, ADR 0005 data-boundary): task text에_redact_private_text+_privacy_findings_for_text사전 감사를 적용한다.- deadline-based poll loop + per-command 타임아웃 (round-1 fix #1, round-3 fix #1 강화,
round-5 fix #1 API 수정): deadline 기반 polling으로 terminal 상태 확인; timeout 경과 →
blocked + teardown. round-3 fix #1:
_default_omc_runner에timeout: float | None = None파라미터 추가; 모든 subprocess 호출에timeout=_remaining_budget(). shutdown은timeout=30.0. round-5 fix #1 [CRITICAL]:omc team status(텍스트 출력)와omc team api get-diff는 실 omc CLI에 존재하지 않는다. poll loop는omc team api get-summary --input '{"team_name":"<name>"}' --json으로 교체됐다. 응답data.summary.tasks의in_progress == 0 and failed == 0 and total > 0 and completed == total→ terminal-success;in_progress == 0 and failed > 0→ terminal-fail. diff 캡처는summary.workers[0].worktree_path를 읽어git_runner(["git", "-C", <worktree>, "diff", "HEAD"])로 대체됐다. 테스트의_fake_omc_runner는 strict 계약 검사 —get-diff시도 시 AssertionError. injectablegit_runner=None(기본_git_worktree_runner) 파라미터로 테스트가 git를 제어한다. - committed worker diff 캡처 — merge-base diff (round-6 fix #1 [CRITICAL]): OMC worker는
per-worker branch에 commit한다.
git diff HEAD는 uncommitted 변경만 캡처하므로 committed work가 있으면 empty diff를 반환 — “empty/completed”로 잘못 리포트되고 scope/privacy/gate 라우팅을 완전히 건너뜀. 수정: diff 캡처 전에git -C <worktree> merge-base HEAD origin/main으로 분기 지점 SHA를 먼저 계산하고,git -C <worktree> diff <base_sha>로 교체 — committed + staged + unstaged 전체를 캡처. merge-base 계산 실패(remote ref 없음, shallow clone 등) → 워닝 발행 +git diff HEAD로 graceful fallback(uncommitted-only, 운영에서 merge-base 실패는 발생하지 않아야 함). - gate heartbeat 무효화 (round-1 fix #3):
_invalidate_omc_blocking_gate_heartbeats로 blocking-role status를pending-omc-review로 초기화 (실행된 run에만 적용 — round-2 fix #4). - scope 검사 warn-open → fail-closed (round-5 fix #2):
_finalize_omc_runner_result에서 active write lease에claimed_files가 없거나 lease가 없는 상태로verdict == "proposed"이면 무조건blocked로 강등된다(이전: 경고만). omc worker는 uncontrolled이므로 명시적 scope 선언 없이 proposed diff를 허용하는 것은 위험하다.claimed_files가 있어도 diff가 범위 밖 파일을 포함하면 기존대로 blocked. - scope 검사를 current task_id lease로 한정 (round-6 fix #2 [CRITICAL]): 이전
_find_active_write_lease(lease_id=None)는 첫 번째 active write lease를 반환했다.active-start가 기존 lease를 보존한 채 새 lease를 추가하면 다른 task의 stale lease가 먼저 반환될 수 있다 — 잘못된claimed_files로 omc diff가 검증되거나(보안 위반), 현재 task의 유효한 diff가 잘못 blocked됨. 수정: lease 목록을 현재task_id로 필터링해 정확히 1개의 matching lease를 요구. 0개(현재 task lease 없음) → blocked fail-closed; 2개 이상(ambiguous) → blocked fail-closed. - lease scope 완전 강화 — no-task-id lease 허용 제거 (round-7 fix #1 [HIGH]): round-6의
구현이
task_id필드가 없는 구형(legacy) lease도 eligible로 취급하는 fallback을 남겨두었다. 이는 fail-open edge다 — task_id 없는 lease는 어느 task에도 속할 수 있는 stale/유출 lease. 수정:task_id가 설정된 경우, lease의task_id필드가 명시적으로 현재task_id와 정확히 일치하는 lease만 eligible. task_id 없는 lease 일체 거부.task_id is None/""인 standalone call은 기존대로 any active write lease 허용._write_expanded_active_runner_fixture가 lease dict에"task_id": task_id를 명시적으로 기록하도록 업데이트됨. - shutdown 실패 가시화 + –force fallback (round-7 fix #2 [HIGH]): 이전 finally 블록은
omc team shutdownrc를 검사하지 않아 비정상 종료된 worker가 silent하게 방치됐다. 수정: shutdown 결과의 rc를 확인해 nonzero → warning 기록 +omc team shutdown <team> --forcefallback 시도(timeout=10s).TimeoutExpired시도 마찬가지로 warning + force fallback. force fallback 자체도 실패하면 warning 추가(raise 없이 teardown 최선 유지)._fake_omc_runner가shutdown_rc/shutdown_force_rc파라미터를 지원해 테스트가 두 경로를 모두 검증한다. - assignment 파일 누락/빈 경우 spawn 전 block (round-7 fix #3 [MEDIUM]): 이전
_build_omc_task_text는 assignment 파일이 없거나 비어있으면 silently 빈 body나 generic fallback text로 계속 진행했다. 정의되지 않은 scope의 omc worker를 spawn하는 것은 안전 위반이다. 수정:_build_omc_task_text의 반환형을str→tuple[str, list[str]](text,blockers)로 변경. 선택된 session 중 assignment 파일이 없거나(FileNotFoundError) 내용이 비어있으면 해당 session별 blocker를blockers리스트에 추가. 호출자에서task_text_blockers가 비어있지 않으면 omc spawn 전 fail-closed 반환.invalidate_heartbeats=False(omc team 미실행). - –force shutdown 결과 검사 (round-8 fix #1 [HIGH]): round-7 fix #2는 shutdown nonzero →
--forcefallback을 추가했으나--force결과(returncode/stderr)를 검사하지 않았다.--force도 실패하면 네트워크/자격증명 접근 가능한 omc worker가 여전히 실행 중일 수 있다. 수정:--force결과를 캡처해 nonzero rc 또는 stderr → warning 기록(“may still be running — manual cleanup required”).TimeoutExpired경로의--forcefallback도 동일하게 결과 검사. raise 없이 teardown 최선 유지. - run-specific artifact 항상 덮어쓰기 (round-8 fix #2 [HIGH]):
_finalize_omc_runner_result의else브랜치(no-ack / dry-run / pre-spawn block / empty diff —write_artifact=False또는diff_text="")가 표준 소비 경로(patch_runs/implementer/patch_artifact.json)는 overwrite하지만 run-specificartifact_path(omc_runs/omc-team/patch_artifact.json)는 overwrite하지 않았다. state가sessions[0].assignment에artifact_path를 기록하므로, 해당 경로에 prior proposed artifact가 있으면 현재 blocked/empty run의 출력인 것처럼 소비될 수 있다. 수정:else브랜치에서artifact_path에 먼저 blocked artifact를 항상 쓴다(parent mkdir 포함). 이제artifact_path와standard_path양쪽 모두 unconditionally 현재 run 결과를 반영한다. - merge-base 실패 시 fail-closed — git diff HEAD fallback 제거 (round-8 fix #3 [HIGH]):
round-6 fix #1은 merge-base 실패 시
git diff HEAD로 graceful fallback을 구현했다. 그러나 omc worker는 per-worker branch에 commit하므로git diff HEAD는 uncommitted 변경만 잡는다. shallow clone / remote ref 부재 환경에서 worker의 committed 변경이 전혀 캡처되지 않아 privacy 감사 / scope 검사 / gate 라우팅을 완전히 우회한 채empty/completed로 완료된다. 이는 graceful degradation이 아니라 안전 허점이다. 수정: merge-base 실패 → fail-closed: blocker 추가 후 반환.git diff HEADfallback 경로 삭제. 운영자는origin/main이 worker worktree에서 접근 가능한지 먼저 확인해야 한다._fake_git_runner기본merge_base_sha="deadbeef00000000"(비어있지 않은 sentinel SHA)으로 변경해 기존 테스트가 merge-base-success 경로를 운용하게 됨. - gate-heartbeat 무효화 실패 → blocked (round-9 fix #1 [CRITICAL]): 이전
_invalidate_omc_blocking_gate_heartbeats는OSError/JSONDecodeError/ValueError를pass로 삼켜 빈 리스트를 반환했다. 호출자는 이를 “무효화할 것이 없음”과 구별하지 못해, registry 쓰기 실패 시에도 stale “passed” 리뷰어/감사자 heartbeat가 그대로 남아 Conservative Gate가 READY로 보일 수 있었다 — omc uncontrolled path에서의 fail-open. 수정: 반환형을list[str]→tuple[list[str], str | None]((invalidated_roles, error_message))으로 변경.error_message가 None이 아닐 때 (invalidate_heartbeats=True경로에서 쓰기 실패) —decision = "blocked"+ blocker 추가 (“omc gate-heartbeat invalidation failed”). 정상 무효화 성공 시 기존과 동일하게 warning 기록. - dry-run은 artifact 무변경 (round-9 fix #2 [HIGH]): round-8 fix #2의
else브랜치가execute=False(dry-run/plan-only) 경로에서도artifact_path와standard_path를 덮어썼다. dry-run은 읽기 전용 planning 액션이므로 이전 executed run이 생산한 proposed artifact를 지워서는 안 된다 — 이후active-apply가 live proposed diff를 잃는다. 수정: round-8else브랜치를elif execute:로 변경해execute=True경우에만 artifact overwrite를 실행. dry-run은 disk의 artifact를 일체 건드리지 않는다. - untracked worker 파일 diff 캡처 (round-10 fix #1 [HIGH]): round-6/8 diff 캡처는
git -C <worktree> diff <base_sha>를 직접 실행했다. omc worker가 추가한 신규(untracked) 파일은 이 명령에 표시되지 않아 privacy/scope 검사를 완전히 우회한 채 diff가 비어 보이는 false-empty completion이 발생했다. 수정: diff 직전에git -C <worktree> add -A로 모든 변경(committed + staged + untracked)을 staging하고,git -C <worktree> diff --cached <base_sha>로 변경.add -A실패 시 blocker + blocked. 이제 신규 파일도 privacy redaction / scope / gate 경로를 통과해야만 proposed artifact로 이어진다. - 단일 task 일관성 검증 (round-10 fix #2 [HIGH]): 선택된 sessions가 두 개 이상의 서로
다른 task_id를 가지면, 여러 task의 assignment text를 단일 omc worker에 전달하게 된다 —
데이터 경계 위반이자 의도치 않은 cross-task context 누출. 마찬가지로
--task T-A와 함께 T-B sessions이 선택되면 잘못된 task 컨텍스트가 worker에 전달된다. 수정:_build_omc_task_text호출 직전에 선택된 sessions의 task_id 집합(selected_task_ids)을 구성한다. (1)len(selected_task_ids) > 1→ blocked fail-closed (“ambiguous: spans N distinct task IDs”). (2)not task_id+len == 1→ 유일 task_id로 자동 파생(기존 동작 유지). (3)task_id제공 +selected_task_ids에 불일치 → blocked fail-closed (“mismatch: –task X does not match selected sessions”). omc는 어떤 경우에도 spawn되지 않는다. - worker diff 캡처 후 privacy 재감사 +
claimed_filesscope 검사 fail-closed 재부과. 결과를ActiveCodexRunnerResult+ 동일한patch_artifact.json모양으로 매핑해 active-apply / Conservative Gate / human-gated ship이 변경 없이 동작한다 — diff는 main으로 머지되지 않고 기존 gate 경로로 라우팅된다. team은 finally에서 항상omc team shutdown; 실패 시 raise 없이 blocked 결과 반환. - stale 표준 패치 아티팩트 덮어쓰기 (round-3 fix #3):
_finalize_omc_runner_result에서write_artifact=False(no-ack / pre-spawn 차단 / dry-run) 경우에도 표준 소비 경로patch_runs/implementer/patch_artifact.json이 존재하면 blocked 아티팩트로 덮어쓴다. 이전 successful run의 stale proposed patch가 이후 blocked run을 우회해write_active_apply에 소비되는 것을 막는다.write_artifact=True경로에서도decision == "blocked"이면 동일하게 blocked 아티팩트를 표준 경로에 쓴다. - auto-loop
task_id전파 (round-3 fix #4):write_active_auto_loop의write_active_codex_runner(...)호출에task_id=task.task_id를 추가한다. 이전에는task_id가 전달되지 않아 omc 경로에서 생성된patch_artifact.json에nulltask_id가 기록되어write_active_apply가 artifact를 거부했다.
- ENV-var secrets 제거 (round-2 fix #1, round-4 fix #1 정정 — defense-in-depth,
완전한 자격증명 경계 아님):
- (d)
agent_loop.py는LOAD_BEARING_PATHS비승격 유지 (ADR 0080/0085/0086 결정 유지) — 병렬 runner 백엔드는 retrieval/verifier/answer/eval 런타임을 건드리지 않으며, ship 실행은 여전히 기존 human-gated 경로가 담당한다.
Consequences
- 운영자가 명시적으로
ACTIVE_OMC_RUNNER_ACK=1+--runner omc를 켰을 때만 진짜 동시 tmux worker로 병렬 실행되고, 그 외에는 byte-identical codex 경로가 그대로 돈다. - omc worker는 uncontrolled(no per-worker sandbox)이지만, 캡처된 diff가 privacy 재감사 + scope 검사 + no-auto-merge를 통과해야만 active-apply/gate로 흐르므로 Conservative Gate와 human-gated ship이 유지된다 — main으로의 자동 머지는 없다.
- ENV-var secrets 제거 (round-2 fix #1, round-4 fix #1 정정):
_OMC_ENV_ALLOWLIST가 ANTHROPIC_API_KEY / OPENAI_API_KEY / GH_TOKEN / AWS_* / DATABASE_URL 등 ENV-var 시크릿을 차단한다. 이는 defense-in-depth이지 완전한 자격증명 경계가 아니다. omc worker는 user의 자체 인증된 CLI이므로 HOME 아래 파일시스템 경로(~/.codex, ~/.claude, ~/.config/gh, ~/.aws)로 자격증명에 독립 접근한다. HOME은 CLI 동작에 필수이므로 유지한다.ACTIVE_OMC_RUNNER_ACK=1ack는 이 home-scoped 자격증명 접근 + 네트워크 egress를 명시 수용한다.OMC_CONFIG,XDG_CACHE_HOME,XDG_RUNTIME_DIR은 round-4에서 허용리스트에서 제거했다(불필요, 노출 축소). - 단일 worker 강제 (round-2 fix #2):
_resolve_omc_worker_mix가 항상 1 worker만 반환한다. agent_mix가 claude=5,codex=5 이어도omc team 1:claude ...만 launch된다. multi-worker diff 캡처가 구현될 때까지 노출 배수 문제가 발생하지 않는다. --read-agent명시 우선 (round-3 fix #2):read_agent="claude"/"codex"전달 시 agent_mix 정책을 무시하고 해당 lane으로 고정된다."auto"(기본)만 정책 majority로 fallback.task_id전파 + standalone 파생 (round-2 fix #3, round-3 fix #4, round-4 fix #2): patch_artifact.json에 유효한 T-YYYY-NNNN task_id가 채워져write_active_apply가 artifact를 거부하지 않는다.write_active_auto_loop의 호출에task_id=task.task_id(round-3 fix #4). round-4 fix #2: standalone(--task미전달) 시 registry session에서 first-validT-YYYY-NNNN을 파생한다. 파생 불가 시 omc spawn 전 fail-closed(높은 비용 run 낭비 방지).- heartbeat 무효화 범위 제한 (round-2 fix #4): no-ack / dry-run / pre-spawn 차단 경로는 레지스트리를 건드리지 않는다. 실제 launch된 run에서만 blocking-role status가 초기화된다.
- 강화된 ADR 0005 data-boundary 보장 (round-1 fix #2): task text는
_redact_private_text강화 redaction 후_privacy_findings_for_text사전 감사를 통과해야 omc가 spawn된다. - false-complete 방지 + per-command 타임아웃 (round-1 fix #1, round-3 fix #1, round-5 fix #1):
deadline-based poll loop는
get-summary응답의 terminal-success 조건(in_progress == 0 and failed == 0 and total > 0 and completed == total) 확인 후에만 diff 캡처를 진행한다.timeout_seconds > 0이면 모든 subprocess 호출에 남은 예산이timeout=으로 전달된다. round-5 fix #1: poll API를 실 omc CLI 계약과 일치시킴(get-summary --input JSON --json); diff는git -C <worktree> diff HEAD로 캡처(API에get-diff없음). - committed worker diff 캡처 (round-6 fix #1, round-8 fix #3 강화): diff 캡처 커맨드가
git -C <worktree> merge-base HEAD origin/main→git -C <worktree> diff <base_sha>로 바뀌어 worker가 commit한 변경을 누락 없이 캡처한다. round-8 fix #3: merge-base 실패 시 graceful fallback(round-6 방식) 대신 fail-closed —git diff HEAD경로 삭제. 실패 시 blocker 기록- blocked 반환.
_fake_git_runner기본값merge_base_sha="deadbeef00000000"— 기존 테스트는 merge-base-success 경로를 운용한다.
- blocked 반환.
- scope 검사 fail-closed (round-5 fix #2): write lease에
claimed_files가 없으면 proposed diff가 있어도blocked로 강등된다. omc worker는 uncontrolled이므로 명시적 scope 없이 proposed artifact 허용은 위험하다._write_expanded_active_runner_fixture가 proper write lease(claimed_files 포함)를 초기화하므로 테스트가 fixture 레벨에서 계약을 검증한다. - scope 검사를 current task_id lease로 한정 (round-6 fix #2): lease 목록에서
task_id로 필터링한 정확히 1개의 active write lease를 요구한다. 0개(현재 task lease 없음) 또는 2개 이상 (중복) → blocked fail-closed. 다른 task의claimed_files로 검증하는 것을 원천 차단한다. - no-task-id lease 완전 거부 (round-7 fix #1):
task_id설정 시 lease에 동일한task_id필드가 없으면 eligible에서 제외된다. unscoped/legacy lease가 scope 검사를 통과하는 fail-open 경로가 완전히 닫혔다. 테스트 fixture_write_expanded_active_runner_fixture가 lease에 명시적task_id를 기록한다. - shutdown 실패 가시화 (round-7 fix #2): shutdown nonzero rc → warning 기록 +
--forcefallback.TimeoutExpired→ warning +--forcefallback. 두 경로 모두 raise 없음(teardown 최선 유지). 운영자는 warning으로 orphaned worker를 인지할 수 있다. - missing/empty assignment 시 spawn 차단 (round-7 fix #3):
_build_omc_task_text가tuple[str, list[str]]을 반환한다. assignment 파일이 없거나 비어있으면 caller가 omc spawn 전 fail-closed 반환한다. 정의되지 않은 scope의 worker가 절대 spawn되지 않는다. - –force shutdown 결과 검사 (round-8 fix #1):
--force결과(rc/stderr)를 확인해 실패 시 “may still be running — manual cleanup required” warning을 추가한다.TimeoutExpired경로도 동일. 이제 두 단계 shutdown(normal + force) 모두 결과를 기록한다. - run-specific artifact 항상 덮어쓰기 (round-8 fix #2):
_finalize_omc_runner_result의else브랜치(write_artifact=False또는diff_text="")에서artifact_path에 blocked artifact를 항상 쓴다. 이전 proposed artifact가state.sessions[0].assignment경로를 통해 소비될 수 없다.standard_path도 unconditionally overwrite(이전엔standard_path.exists()조건부였음). - merge-base 실패 시 fail-closed (round-8 fix #3): round-6
git diff HEADfallback 제거. merge-base 실패 → blocker + blocked 반환. shallow clone/remote ref 부재 환경에서 worker committed 변경이 privacy/scope 우회하는 false-empty completion이 불가능해졌다._fake_git_runner기본merge_base_sha="deadbeef00000000"로 변경 — 기존 테스트가 merge-base-success path를 운용. round-6test_active_codex_runner_omc_fallback_to_head_diff_when_merge_base_fails가test_active_codex_runner_omc_blocked_when_merge_base_fails로 이름 변경 + blocked 검증으로 갱신. - gate-heartbeat 무효화 실패 → blocked (round-9 fix #1):
_invalidate_omc_blocking_gate_heartbeats반환형이list[str]→tuple[list[str], str | None]으로 변경됐다.error_message != None시_finalize_omc_runner_result가decision = "blocked"+ blocker를 추가한다. registry 쓰기 실패로 stale reviewer/auditor heartbeat가 생존해 Conservative Gate가 오판(READY)하는 fail-open 경로가 닫혔다. - dry-run은 artifact 무변경 (round-9 fix #2): round-8 fix #2의
else브랜치를elif execute:로 변경.execute=Falsedry-run은artifact_path와standard_path양쪽 모두 건드리지 않는다. 이전 proposed artifact를 지워 active-apply를 실패시키는 round-8 regression이 수정됐다. - untracked 파일 캡처 포함 (round-10 fix #1): diff 캡처가
git add -A→ `git diff –cached` 시퀀스로 변경됐다. omc worker가 신규 파일을 생성하면 이전 `git diff ` 방식은 untracked 파일을 놓쳐 false-empty로 완료됐다. 이제 committed + staged + untracked 변경 모두 캡처해 privacy/scope/gate 검사를 거친다. `git add -A` 실패 → blocker + blocked. - 단일 task 일관성 gate (round-10 fix #2): omc spawn 직전에 선택된 sessions의 task_id 집합을
검증한다. 두 개 이상의 서로 다른 task_id → blocked fail-closed (“ambiguous”).
--task X와 selected sessions의 task_id가 불일치 → blocked fail-closed (“mismatch”). cross-task assignment text가 단일 worker에 전달되는 것이 차단됐다. - Conservative Gate stale-pass 방지 (round-1 fix #3): 실제 launch된 run에서만
_invalidate_omc_blocking_gate_heartbeats가 호출되어 blocking-role status가pending-omc-review로 초기화된다. 실제 blocking-role session이 실행되어야만 gate가 READY 상태가 된다. - stale 표준 패치 아티팩트 덮어쓰기 (round-3 fix #3, round-9 fix #2 scope): executed blocked / no-ack 경로에서 표준 소비 경로의 기존 패치 아티팩트를 blocked 아티팩트로 덮어쓴다. 이전 successful run의 proposed diff가 이후 blocked executed run을 우회하는 것이 차단된다. dry-run은 제외 (round-9 fix #2): dry-run은 artifact를 건드리지 않는다.
- injectable
omc_runner로 테스트가 실 omc 없이 명령/환경/거버넌스 경로를 전부 검증한다. - (scope-down) 단일-worker diff 캡처 경로만 완전 구현. >1 worker worktree의 diff 캡처/머지는
의도적 follow-up —
_resolve_omc_worker_mix가 단일 worker를 강제하므로 현재는 도달 불가.
Alternatives considered
- in-repo runner를 진짜 동시성으로 재작성. 기각:
omc team이 per-worker git-worktree 격리 + tmux 동시성을 이미 제공한다(재발명 회피). 핵심 리스크는 동시성이 아니라 OMC worker의 uncontrolled 권한이며, 그건 ack gate + 캡처-diff 거버넌스 재부과로 닫는다. - omc를 ack 없이 기본 허용. 기각: omc worker는 sandbox 옵션 자체가 없어 비공개 데이터 egress가 가능하다 → ADR 0005 경계를 무조건 완화. 명시 opt-in(ack)이 ADR 0061 데이터-경계 조건이다.
--auto-merge로 worker commit을 leader 브랜치에 자동 머지. 기각: 거버넌스(privacy / scope / Conservative Gate / human-gated ship)를 우회해 main에 닿는다. diff는 반드시 기존 active-apply / gate 경로로만 라우팅한다.- codex 기본 동작 변경(omc를 기본 runner로). 기각: ADR 0001 byte-identical 보존 위반. omc는 순수 opt-in 분기로만 둔다.
Deferred / follow-up
- multi-worker diff 캡처/머지 — PR-D(#1804)에서 구현됨(ADR 0095).
본 ADR 의 single-worker pin(
_resolve_omc_worker_mix의total_workers=1+assert ... == 1)은 ADR 0095 PR-D 가 부분 supersede 한다 — worker-count 결정만 번복되고, 본 ADR 의 거버넌스/ack 기계(ack fail-closed, no-auto-merge, privacy 재감사, scope 재부과, gate 라우팅)는 그대로 유지된다. 구현:_resolve_omc_worker_mix가read_agent="auto"에서 agent_mix weight 를 worker 수로 매핑하고min(OMC_MAX_WORKERS, max_parallel)로 clamp(explicitclaude/codexoverride 는 single lane 유지). per-worker diff 는 worker 별 merge-base→add -A→diff --cached로 캡처해 각omc_runs/omc-team/worker-{idx}/patch_artifact.jsonnamespace 에 privacy+scope 재감사 후 기록. fail-closed 집계: 어느 worker 라도 캡처/재감사 실패 시 전체 run blocked(부분 성공 없음). 정본 정책: N==1 + 전 검사 통과는 본 ADR 의 표준 경로 proposed 기록과 byte-identical; N>1 + 전 검사 통과는 표준 active-apply 경로를 “needs human selection” blocked artifact 로 라우팅 (per-worker proposed 는 보존, 자동 승격은 PR-D non-goal). NO auto-merge 불변. _OMC_ENV_ALLOWLIST확장. omc/tmux 런타임이 추가 환경 변수를 요구하면 허용리스트에 추가 (단, 자격증명·토큰 류는 추가 금지).
Verification
python3 -m pytest -q tests/test_agent_loop.py -k 'omc or active_runner_parser'
python3 scripts/_governance.py --check-adr-readme-parity docs/adr/0087-opt-in-omc-team-parallel-runner.md
git diff --check