# acceptance-loop 레퍼런스

> validate·split·run·inspect 하위 명령, 스위트 구조, holdout, max-iterations, baseline/restore, 판정 산출물.

- Repository: local/mogui-ADE-orchestrator

- Human docs: https://grok-wiki.com/public/docs/local-mogui-ade-orchestrator-97afe791d5ac
- Complete Markdown: https://grok-wiki.com/public/docs/local-mogui-ade-orchestrator-97afe791d5ac/llms-full.txt

## Source Files

- `scripts/acceptance-loop`
- `src/master_runtime/core/acceptance/loop.py`
- `src/master_runtime/core/acceptance/casebook.py`
- `src/master_runtime/core/acceptance/config.py`
- `src/master_runtime/core/acceptance/verdict.py`
- `tests/test_acceptance_loop.py`
- `docs/public/reference.md`

---

---
title: "acceptance-loop 레퍼런스"
description: "validate·split·run·inspect 하위 명령, 스위트 구조, holdout, max-iterations, baseline/restore, 판정 산출물."
---

`scripts/acceptance-loop`는 마스터 측 **결정적 수락 루프** CLI다. JSON 스위트 설정을 로드한 뒤, 후보 변경을 `train`+`holdout` 결합 통과 수(pass count)로만 비교해 수락·거절한다. 모델 판단·휴리스틱·자연어 근거는 판정에 참여하지 않는다. 구현은 `src/master_runtime/core/acceptance/`에 있고, proposer는 구독형 CLI 서브프로세스(`claude` · `codex` · `cursor-agent`)로만 호출되며 SDK·API 키를 읽지 않는다.

## 하위 명령 요약

| 하위 명령 | 목적 | 필수 옵션 | 종료 코드 |
| --- | --- | --- | --- |
| `validate` | 설정·casebook 구조 검증 후 요약 JSON 출력 | `--config` | `0` 성공, `2` 설정 오류 |
| `split` | 마스터 측 스플릿 매니페스트 기록 | `--config` | `0` 성공, `2` 설정 오류 |
| `run` | baseline 평가 → 반복 propose/evaluate/decide | `--config` | `0` gated 전부 통과, `1` 미완료, `2` 오류 |
| `inspect` | 기존 런의 `report.json` 출력 | `--run-dir` | `0` 성공, `2` 리포트 없음 |

```bash
scripts/acceptance-loop validate --config path/to/suite.json
scripts/acceptance-loop split    --config path/to/suite.json [--output-dir DIR]
scripts/acceptance-loop run      --config path/to/suite.json \
  [--max-iterations N] [--baseline-ref REF] [--restore-cmd CMD]
scripts/acceptance-loop inspect  --run-dir path/to/run
```

<Note>
공개 `docs/public/reference.md` 표의 `inspect` 설명(“스위트 구성 보고”)과 달리, 구현은 **이미 작성된** `run_dir/report.json`을 그대로 stdout에 찍는다. 스위트 구조 검증은 `validate`가 담당한다.
</Note>

## 설정 JSON

경로 필드는 **설정 파일 위치 기준**으로 해석된다. 기본 `run_dir`은 `runs/{name}`이다.

```json
{
  "name": "dz-bwh-demo",
  "workspace_root": "workspace",
  "run_dir": "run",
  "max_iterations": 2,
  "proposer": {
    "runtime": "codex",
    "model": "gpt-5-codex",
    "timeout_seconds": 60
  },
  "regression_log": "state/regressions.jsonl",
  "cases": [
    {
      "case_id": "t1",
      "split": "train",
      "stratum": "unit",
      "command": ["true"]
    },
    {
      "case_id": "h1",
      "split": "holdout",
      "stratum": "unit",
      "command": ["true"]
    }
  ]
}
```

### 최상위 필드

<ParamField body="name" type="string" required>
런 이름. 비어 있으면 로드 실패.
</ParamField>

<ParamField body="workspace_root" type="string">
평가·proposer cwd. 기본 `"."` (설정 파일 기준 해석).
</ParamField>

<ParamField body="run_dir" type="string">
산출물 루트. 기본 `runs/{name}`.
</ParamField>

<ParamField body="max_iterations" type="integer">
제안 반복 상한. 기본 `3`. **1 이상** 양의 정수만 허용.
</ParamField>

<ParamField body="proposer" type="object" required>
`runtime` 필수. `model`(string\|null), `timeout_seconds`(기본 `1800` = 30분).
</ParamField>

<ParamField body="regression_log" type="string">
선택. 실패 핀용 append-only JSONL 경로.
</ParamField>

<ParamField body="cases" type="array" required>
검증 케이스 목록. 루트 배열 또는 `{ "cases": [...] }` 형태.
</ParamField>

### proposer.runtime

`core/adapter`의 sync CLI 프로필 이름만 허용한다. 현재: `claude`, `codex`, `cursor-agent`. 그 외 이름은 `AcceptanceConfigError` / exit `2`.

| runtime | argv 형태 (요약) |
| --- | --- |
| `claude` | `claude -p <prompt> [--model …]` |
| `codex` | `codex exec [--model …] <prompt>` |
| `cursor-agent` | `cursor-agent -p --trust --force [--model …] <prompt>` |

## 스위트 · casebook 구조

### VerificationCase

| 필드 | 타입 | 규칙 |
| --- | --- | --- |
| `case_id` | string | 필수, 스위트 내 유일 |
| `split` | string | `train` · `holdout` · `scorecard` (별칭 허용) |
| `stratum` | string | 필수. seed train/holdout이 **같은 stratum 집합**을 커버해야 함 |
| `command` | string[] | 선택. 없으면 command evaluator가 fail-closed (`case has no command`) |
| `origin` | string | `seed`(기본) · `regression` |

### 스플릿 의미

| Split | 가시성 | 수락 게이트 | 용도 |
| --- | --- | --- | --- |
| `train` | **visible** (proposer 노출) | gated | 후보가 고칠 수 있는 실패 목록 |
| `holdout` | **private** | gated | 오버피팅 방지. proposer workspace에 기록되지 않음 |
| `scorecard` | private | **비-gate** | baseline·최종 후보에만 별도 측정 |

별칭: `visible`→`train`, `private`→`holdout`, `acceptance`→`scorecard`.

### 구조 검증 (`CaseBook.validate`)

- 케이스 1개 이상
- `case_id` 중복 금지
- gated 스플릿(`train`, `holdout`) 각각 최소 1케이스
- seed 기준 train/holdout **stratum 집합이 동일**해야 함

`validate`와 `load_acceptance_config` 모두 이 검증을 통과해야 한다.

## validate

```bash
scripts/acceptance-loop validate --config suite.json
```

성공 시 stdout JSON 예:

```json
{
  "holdout": 1,
  "max_iterations": 2,
  "name": "dz-bwh-demo",
  "proposer_runtime": "codex",
  "regression_log": null,
  "run_dir": "/abs/path/run",
  "scorecard": 0,
  "train": 1,
  "workspace_root": "/abs/path/workspace"
}
```

## split

regression log가 설정돼 있으면 **적용 후** 매니페스트를 쓴다.

| 옵션 | 기본 |
| --- | --- |
| `--config` | 필수 |
| `--output-dir` | 생략 시 `config.run_dir` |

기록 파일 (`AcceptanceRunLayout.write_manifest`):

- `manifest.json` — config 직렬화
- `split.json` — 전체 스플릿 매니페스트
- `split.md` — 사람이 읽는 매니페스트

stdout: `split.json`의 절대/상대 경로 한 줄.

## run

### 플래그

<ParamField body="--config" type="path" required>
스위트 JSON.
</ParamField>

<ParamField body="--max-iterations" type="integer">
설정 값을 덮어씀. `< 1`이면 stderr 메시지 후 exit `2`.
</ParamField>

<ParamField body="--baseline-ref" type="string">
baseline `Candidate.ref`. 기본 `""`. 코어는 ref를 해석하지 않음(평가기가 워크스페이스 상태를 읽음).
</ParamField>

<ParamField body="--restore-cmd" type="string">
거절된 후보 후 워크스페이스 복원 셸 명령. `shlex.split` 후 `workspace_root`에서 실행. 타임아웃 **300초**.
</ParamField>

### 루프 동작

```text
baseline = Candidate(label="baseline", ref=--baseline-ref)
evaluate(gated cases) → baseline_score
promote failures → regression_log (optional)

for i in 1..max_iterations:
  if current_score complete → stop
  build visible-only proposer workspace
  candidate = CLI proposer(...)
  if no candidate → record empty iteration, stop
  if surfaces empty → reject NO_CANDIDATE_CHANGE (재평가 없음)
  else evaluate → decide(strict combined pass increase)
  if accepted → current = candidate
  elif restore-cmd → on_reject(candidate)

if scorecard cases exist:
  evaluate baseline + final on scorecard only
  (nothing accepted → final_scorecard is baseline_scorecard, no re-run)

write report.json + report.md
stdout: report Markdown
exit 0 iff final_score.is_complete() else 1
```

### 판정 규칙 (`decide`)

수락은 **gated 스플릿 결합 통과 수의 엄격 증가**만 본다. scorecard는 합산에서 제외.

| `AcceptanceReason` | 의미 |
| --- | --- |
| `PASS_COUNT_INCREASED` | `candidate_combined > current_combined` → 수락 |
| `NO_PASS_COUNT_INCREASE` | 증가 없음(동률·감소 포함) → 거절 |
| `NO_CANDIDATE_CHANGE` | `surfaces` 비어 있음 → 거절, 평가 생략 |

train을 늘리고 holdout을 깎아 합이 같으면 거절된다(테스트: holdout 교환 방지).

누락 케이스 결과는 fail-closed: `detail = "missing result"`, 통과 수에 포함되지 않음. 평가기가 보고한 split/stratum은 무시되고 **casebook 소유 값**으로 덮어쓴다.

### baseline / restore / in-place

- baseline 라벨: 항상 `baseline`
- 후보 라벨: `iter-001`, `iter-002`, … (`CANDIDATE_LABEL_FORMAT`)
- CLI proposer와 `command_evaluator`는 모두 **in-place mutation**을 선언한다
- `max_iterations >= 2` 이고 restore 훅이 없으면 루프가 `ValueError`로 거부한다  
  → CLI에서는 `--restore-cmd` 필요. `max_iterations=1`이면 restore 불필요
- 거절 시에만 `on_reject` 호출. 수락 시에는 복원하지 않음

```bash
scripts/acceptance-loop run --config suite.json \
  --max-iterations 3 \
  --restore-cmd 'git checkout -- . && git clean -fd'
```

### proposer workspace (visible only)

경로: `run_dir/history/visible/iterations/{NNN}/proposer_workspace/`

| 파일 | 내용 |
| --- | --- |
| `task.md` | 제안 작업 지시(가시 실패만 나열) |
| `casebook_visible.json` | visible 매니페스트 (`train`만) |
| `visible_failures.json` | 가시 실패 결과 |
| `history.json` | 이전 반복 결정 요약 |
| `candidate.json` | proposer가 써야 하는 선언 (없으면 후보 없음) |
| `proposal.md` | 선택 근거 텍스트 |
| `proposer_result.json` / `stdout` / `stderr` | CLI 호출 기록 |

`candidate.json` 형태:

```json
{
  "surfaces": ["src/target.py"],
  "summary": "what changed and why",
  "ref": "optional-opaque-handle"
}
```

`surfaces`가 비어 있으면 변경 없음으로 기록되고 거절된다. 파일이 없거나 JSON 불량이면 후보 없음으로 반복이 종료된다.

holdout case_id는 proposer workspace 어디에도 쓰이지 않는다.

## inspect

```bash
scripts/acceptance-loop inspect --run-dir runs/dz-bwh-demo
```

- 대상: `{run_dir}/report.json`
- 없으면: `no report at …` → exit `2`
- 있으면: 파일 내용을 그대로 stdout에 출력 (JSON)

## 런 산출물 레이아웃

```text
run_dir/
  manifest.json
  split.json
  split.md
  report.json
  report.md
  history/
    visible/
      train/<label>/result.json
      iterations/
        001/
          decision.json
          decision.md
          proposer_workspace/...
    private/
      holdout/<label>/result.json
      scorecard/<label>/result.json   # scorecard cases 있을 때
```

- `history/visible/**` — proposer에 복사·노출 가능
- `history/private/**` — 마스터 전용. holdout·scorecard 결과 경로
- 모든 JSON/텍스트 쓰기는 임시 파일 후 `os.replace` 원자 교체

### decision.json 필드

| 필드 | 설명 |
| --- | --- |
| `iteration` | 1-based |
| `starting_label` | 수락 전 현재 라벨 |
| `candidate_label` / `candidate_ref` | 후보 |
| `decision` | `accepted` \| `rejected` |
| `reason` | `AcceptanceReason` 값 |
| `current_combined` / `candidate_combined` / `delta` | 결합 통과 수 |
| `changed_surfaces` | `surfaces` 목록 |
| `promoted_regressions` | 이번 반복에 새로 핀된 case_id |
| `summary` | 제안 요약 |

### report

`report.md` (run stdout과 동일 형식) 요약:

- Baseline / Final 라벨
- Accepted candidates `accepted/iterations`
- 스플릿 표: train · holdout · (있으면) scorecard 의 baseline vs final `passed/total`
- 반복별 수락/거절, reason, combined delta, surfaces, pinned regressions

`report.json`은 `AcceptanceReport.to_dict()` 전체 직렬화(scorecard 포함).

## regression log

선택 경로 JSONL. 한 번 실패한 케이스는 이후 라운드에서 조용히 빠지지 않는다.

- 관측 실패 → `origin=regression`으로 append (`ts`, `iteration`, `observed_split` 포함)
- config에 아직 있으면 원래 split 유지
- config에서 빠진 케이스는 **`holdout`으로 재입대** (`REGRESSION_READMIT_SPLIT`)
- `split` / `run` 시작 시 `RegressionLog.apply(casebook)` 수행

## command evaluator

`run`은 `command_evaluator(workspace_root)`를 사용한다.

- 각 케이스 `command`를 `workspace_root`에서 실행
- exit 0 → pass, 그 외 · 타임아웃 · 누락 바이너리 → fail
- 기본 프로세스 타임아웃: **900초** (15분)
- command 없음 → fail, `detail="case has no command"`
- in-place 선언: multi-iteration 시 `--restore-cmd` 필요

## 종료 코드 정리

| 코드 | 상황 |
| --- | --- |
| `0` | `validate`/`split`/`inspect` 성공; `run`에서 gated 전부 통과 (`is_complete`) |
| `1` | `run` 완료했으나 gated 미통과 |
| `2` | 설정 오류, `--max-iterations < 1`, in-place+restore 누락, `inspect`에 리포트 없음, 알 수 없는 하위 명령 |

## 최소 워크플로

<Steps>
  <Step title="스위트 검증">
    `scripts/acceptance-loop validate --config suite.json` 이 train/holdout 카운트를 출력하고 exit 0인지 확인한다.
  </Step>
  <Step title="스플릿 매니페스트 (선택)">
    `scripts/acceptance-loop split --config suite.json` 후 `split.json` / `split.md`를 검토한다. holdout이 visible 트리에 없는지 확인한다.
  </Step>
  <Step title="루프 실행">
    multi-iteration이면 restore 명령을 넣고 run 한다. stdout Markdown의 Final·Accepted candidates·스플릿 표를 본다.
  </Step>
  <Step title="감사">
    `history/visible/iterations/*/decision.json`과 `report.json`을 확인한다. `scripts/acceptance-loop inspect --run-dir …`로 최종 JSON을 다시 출력할 수 있다.
  </Step>
</Steps>

## 실패 모드

| 증상 | 원인 |
| --- | --- |
| `split holdout must include at least one case` | train만 있는 스위트 |
| `train and holdout must cover the same seed strata` | stratum 집합 불일치 |
| `unsupported proposer runtime` | `claude`/`codex`/`cursor-agent` 외 |
| `… mutates the workspace in place; pass on_reject…` | multi-iter + restore 없음 |
| `--max-iterations must be at least 1` | CLI 플래그 검증 |
| `no report at …/report.json` | inspect 대상 런 미완료 |
| 후보 없음 반복 | CLI 실패 또는 `candidate.json` 미작성 |
| holdout 교환 거절 | train↑ holdout↓ 로 합 통과 수 동일 |

## Related pages

<CardGroup>
  <Card title="증거와 수락" href="/evidence-and-acceptance">
    워커 self-report와 독립 검증, acceptance 판정 규칙의 개념 층.
  </Card>
  <Card title="CLI 레퍼런스" href="/cli-reference">
    `scripts/` 공개 명령 표와 --help 동기화 계약.
  </Card>
  <Card title="dispatch-gate 레퍼런스" href="/dispatch-gate-reference">
    디스패치 허용·ledger. 수락 루프 이전 단계.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    preflight·undecidable·복구 프로브 등 운영 장애.
  </Card>
</CardGroup>
