# Acceptance loop 실행

> acceptance suite 구조, train과 holdout 분리, proposer runtime, 반복 실행, scorecard, regression log를 다룹니다.

- Repository: local/master-ops-with-local-mogui-ADE-orchestrator

- Human docs: https://grok-wiki.com/public/docs/local-master-ops-with-local-mogui-ade-orches-0ac7093355f3
- Complete Markdown: https://grok-wiki.com/public/docs/local-master-ops-with-local-mogui-ade-orches-0ac7093355f3/llms-full.txt

## Source Files

- `local-mogui-ade-orchestrator:src/master_runtime/core/acceptance/config.py`
- `local-mogui-ade-orchestrator:src/master_runtime/core/acceptance/casebook.py`
- `local-mogui-ade-orchestrator:src/master_runtime/core/acceptance/loop.py`
- `local-mogui-ade-orchestrator:src/master_runtime/core/acceptance/report.py`
- `local-mogui-ade-orchestrator:scripts/acceptance-loop`
- `local-mogui-ade-orchestrator:tests/test_acceptance_loop.py`

---

---
title: "Acceptance loop 실행"
description: "acceptance suite 구조, train과 holdout 분리, proposer runtime, 반복 실행, scorecard, regression log를 다룹니다."
---

`local-mogui-ade-orchestrator:scripts/acceptance-loop`는 JSON acceptance config를 읽어 `train`과 `holdout` casebook을 검증하고, proposer CLI가 만든 후보를 반복 평가한 뒤 `run_dir`에 split manifest, per-split result, iteration decision, 최종 report를 기록한다. 구현은 `local-mogui-ade-orchestrator:src/master_runtime/core/acceptance/`에 있으며, `local-master-ops`에는 동일한 acceptance loop 실행 파일이 없다.

## 실행 surface

| 작업 | 명령 | 결과 |
| --- | --- | --- |
| config 검증 | `scripts/acceptance-loop validate --config acceptance.json` | config와 casebook을 로드하고 split 개수, proposer runtime, run 경로를 JSON으로 출력한다. |
| split manifest 생성 | `scripts/acceptance-loop split --config acceptance.json [--output-dir runs/demo]` | `split.json`, `split.md`, `manifest.json`을 쓴다. |
| loop 실행 | `scripts/acceptance-loop run --config acceptance.json [--max-iterations N] [--baseline-ref REF] [--restore-cmd CMD]` | 후보 proposer를 호출하고 acceptance report를 출력한다. 모든 gated case가 통과하면 exit `0`, 미완료면 exit `1`이다. |
| report 확인 | `scripts/acceptance-loop inspect --run-dir runs/demo` | `report.json`을 그대로 출력한다. 파일이 없으면 exit `2`이다. |

<Warning>
`inspect`는 완료된 run directory의 `report.json`을 읽는 cold audit 경로다. acceptance suite를 다시 계산하거나 holdout 내용을 proposer workspace로 복사하지 않는다.
</Warning>

## Acceptance config

Config는 JSON object여야 하며, 상대 경로는 config 파일 위치를 기준으로 해석된다.

```json title="acceptance.json"
{
  "name": "dz-bwh-demo",
  "workspace_root": "workspace",
  "run_dir": "runs/dz-bwh-demo",
  "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": ["python3", "-m", "pytest", "tests/test_unit.py"]
    },
    {
      "case_id": "h1",
      "split": "holdout",
      "stratum": "unit",
      "command": ["python3", "-m", "pytest", "tests/test_holdout.py"]
    }
  ]
}
```

<ParamField body="name" type="string" required>
비어 있지 않은 suite 이름이다.
</ParamField>

<ParamField body="workspace_root" type="path">
평가 command와 proposer CLI가 실행되는 작업 디렉터리다. 기본값은 `"."`이다.
</ParamField>

<ParamField body="run_dir" type="path">
manifest, history, report가 기록되는 디렉터리다. 기본값은 `runs/{name}`이다.
</ParamField>

<ParamField body="max_iterations" type="integer">
양의 정수여야 한다. 기본값은 `3`이다.
</ParamField>

<ParamField body="proposer.runtime" type="string" required>
동기 CLI profile 이름이다. 현재 profile은 `claude`, `codex`, `cursor-agent`이다.
</ParamField>

<ParamField body="proposer.model" type="string | null">
CLI profile에 전달할 model 이름이다. 없으면 profile 기본값을 사용한다.
</ParamField>

<ParamField body="proposer.timeout_seconds" type="integer">
proposer CLI timeout이다. 기본값은 `1800`초이다.
</ParamField>

<ParamField body="regression_log" type="path">
선택적 JSONL 파일이다. 관측된 실패 case를 append-only로 고정하고 이후 run에 재투입한다.
</ParamField>

## Casebook 구조

`cases` 항목은 `VerificationCase`로 로드된다. 각 case는 `case_id`, `split`, `stratum`, `command`, `origin`을 가진다.

| 필드 | 규칙 |
| --- | --- |
| `case_id` | 필수 문자열이다. 중복되면 config validation이 실패한다. |
| `split` | 필수 값이다. `train`, `holdout`, `scorecard`를 사용한다. alias로 `visible`은 `train`, `private`은 `holdout`, `acceptance`는 `scorecard`로 정규화된다. |
| `stratum` | 필수 문자열이다. seed `train`과 seed `holdout`은 동일한 stratum 집합을 가져야 한다. |
| `command` | 문자열 배열이다. `command_evaluator`에서 비어 있으면 해당 case는 fail-closed 처리된다. |
| `origin` | `seed` 또는 `regression`이다. 생략하면 `seed`이다. |

`train`과 `holdout`은 gated split이다. acceptance 결정은 두 split의 combined pass count만 비교한다. `scorecard` split은 gated pass count에 포함되지 않으며, 최종 보고용으로 baseline과 final candidate에 대해서만 실행된다.

## Train과 holdout 분리

`train`만 proposer-visible split이다. `holdout`과 `scorecard`는 private artifact 경로에 기록되며 proposer workspace에 들어가지 않는다.

```text
run_dir/
├── manifest.json
├── split.json
├── split.md
├── report.json
├── report.md
└── history/
    ├── visible/
    │   ├── train/<candidate-label>/result.json
    │   └── iterations/001/
    │       ├── decision.json
    │       ├── decision.md
    │       └── proposer_workspace/
    │           ├── task.md
    │           ├── casebook_visible.json
    │           ├── visible_failures.json
    │           ├── history.json
    │           ├── proposer_result.json
    │           ├── proposer_stdout.log
    │           └── proposer_stderr.log
    └── private/
        ├── holdout/<candidate-label>/result.json
        └── scorecard/<candidate-label>-scorecard/result.json
```

<Info>
Visibility는 `is_visible_split()` predicate 하나로 결정된다. layout routing, visible manifest, visible failure list가 같은 predicate를 사용하므로 `holdout` case id가 proposer workspace에 노출되는 경로를 분리한다.
</Info>

## Proposer runtime

CLI proposer는 `ProposerRequest`를 만들고 `invoke_cli_proposer()`를 통해 subscription CLI subprocess를 실행한다. core loop는 vendor SDK나 API key를 직접 다루지 않는다. Runtime별 argv 조립은 `local-mogui-ade-orchestrator:src/master_runtime/core/adapter/profile.py`의 `SyncCliProfile` 구현이 담당한다.

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

Proposer prompt는 `task.md`에 기록된 visible failure, visible casebook, 현재 candidate label, visible train score를 기반으로 한다. Proposer는 `candidate.json`을 써야 하며, 없거나 읽을 수 없으면 해당 iteration은 “no candidate produced”로 끝난다.

```json title="candidate.json"
{
  "surfaces": ["src/target.py"],
  "summary": "widened the guard",
  "ref": "refs/candidates/iter-001"
}
```

`surfaces`가 비어 있으면 후보는 변경 없음으로 기록되고 `NO_CANDIDATE_CHANGE`로 거절된다. `summary`가 없으면 `proposal.md` 내용이 fallback summary로 사용된다.

## 반복 실행과 restore guard

Loop는 baseline을 먼저 gated cases에 대해 평가한다. 이미 모든 gated case가 통과하면 proposer iteration을 실행하지 않는다. 그렇지 않으면 `1..max_iterations` 범위에서 visible-only workspace를 만들고 proposer를 호출한다.

<Steps>
<Step title="Baseline 평가">
`train`과 `holdout` case를 평가하고 baseline score를 기록한다. 실패 case가 있으면 optional regression log에 promote한다.
</Step>

<Step title="Visible workspace 생성">
`casebook_visible.json`, `visible_failures.json`, `history.json`, `task.md`를 `history/visible/iterations/<nnn>/proposer_workspace/`에 쓴다.
</Step>

<Step title="Proposer 호출">
선택한 `proposer.runtime` CLI를 `workspace_root`에서 실행한다. CLI 결과는 `proposer_result.json`, `proposer_stdout.log`, `proposer_stderr.log`로 남는다.
</Step>

<Step title="Candidate 평가">
`candidate.json`이 있고 `surfaces`가 비어 있지 않으면 gated cases를 다시 평가한다. 평가 결과는 split visibility에 따라 visible/private 경로에 분리 저장된다.
</Step>

<Step title="Acceptance 결정">
후보의 combined pass count가 현재 후보보다 엄격히 증가할 때만 accept한다. accept되면 current candidate가 갱신된다.
</Step>

<Step title="거절 후 복구">
proposer 또는 evaluator가 workspace를 in-place로 바꾸는 경우, `max_iterations > 1`에서 rejection 이후 반복하려면 `--restore-cmd`가 필요하다.
</Step>
</Steps>

`cli_proposer()`와 `command_evaluator()`는 in-place mutation으로 표시된다. 따라서 여러 iteration을 실행하면서 rejected tree를 평가하지 않으려면 restore hook을 명시해야 한다.

```bash
scripts/acceptance-loop run \
  --config acceptance.json \
  --max-iterations 3 \
  --restore-cmd "git restore ."
```

## Scorecard와 결정 규칙

`Scorecard`는 case-level `CaseResult`와 split aggregate `SplitScore`를 분리한다. Evaluator가 case 결과를 누락하면 `missing result` 실패로 채운다. Evaluator가 split이나 stratum을 다르게 보고해도 casebook의 split과 stratum으로 덮어쓴다.

| reason | 조건 | decision |
| --- | --- | --- |
| `PASS_COUNT_INCREASED` | candidate combined pass count가 current보다 크다. | accepted |
| `NO_PASS_COUNT_INCREASE` | candidate가 바뀌었지만 combined pass count가 증가하지 않았다. | rejected |
| `NO_CANDIDATE_CHANGE` | candidate가 변경 surface를 선언하지 않았다. | rejected |

`scorecard` split이 있으면 baseline과 final candidate에 대해 별도로 실행된다. 아무 후보도 accept되지 않았으면 final tree가 baseline과 같으므로 final scorecard는 baseline scorecard를 재사용한다.

## Regression log

`regression_log`가 설정되면 매 평가에서 실패한 gated case가 JSONL에 append된다. 이미 기록된 `case_id`는 다시 promote하지 않는다. 이후 run에서 config가 해당 case를 여전히 포함하면 원래 config split을 유지한다. Config에서 빠진 regression case는 `holdout` split으로 재등록되어 proposer-visible set에 들어가지 않는다.

```json title="regressions.jsonl entry"
{"case_id":"t2","command":["true"],"iteration":1,"observed_split":"train","origin":"regression","split":"train","stratum":"io","ts":1000.0}
```

이 동작은 과거 실패 case가 suite 축소로 조용히 사라지는 것을 막는다. 단, unreadable JSONL line은 무시되므로 regression log 자체의 파일 무결성은 별도 운영 절차에서 관리해야 한다.

## 출력과 검증 신호

| 파일 | 의미 |
| --- | --- |
| `manifest.json` | 로드된 `AcceptanceConfig` 직렬화 결과다. |
| `split.json` | master-side 전체 split manifest다. private split도 포함한다. |
| `split.md` | split별 visibility label이 붙은 사람이 읽는 manifest다. |
| `history/visible/train/*/result.json` | proposer-visible train 결과다. |
| `history/private/holdout/*/result.json` | proposer에게 숨기는 holdout 결과다. |
| `history/visible/iterations/*/decision.json` | iteration별 audit record다. |
| `report.json` | 최종 machine-readable report다. |
| `report.md` | baseline/final split 점수와 iteration 요약이다. |

`validate`는 config 구조 오류, unsupported runtime, invalid JSON, casebook validation 실패를 stderr와 exit `2`로 보고한다. `run`은 config load 오류도 exit `2`로 반환하고, loop가 정상 종료되더라도 final score가 complete가 아니면 exit `1`을 반환한다.

## Provider-neutral 운영 경계

Acceptance loop의 core는 proposer를 `Proposer` callable로, evaluator를 `Evaluator` callable로 받는다. 기본 CLI proposer도 `SyncCliProfile` adapter를 통해 argv를 만들 뿐 특정 hosted provider SDK에 연결하지 않는다. 따라서 새 runtime을 추가할 때는 core acceptance decision을 바꾸지 않고 profile 또는 callable 주입 경계를 확장한다.

BYOC/BYOK 환경에서는 config의 `proposer.runtime`, `proposer.model`, local CLI authentication, `workspace_root`만 운영자가 소유한다. Acceptance 결정은 local command 결과와 casebook pass count로 계산되며, natural-language rationale이나 provider별 score는 gate에 참여하지 않는다.

## Related pages

<CardGroup>
<Card title="CLI 참조" href="/cli-reference">
`acceptance-loop` subcommand와 exit code를 다른 공개 script surface와 함께 확인한다.
</Card>
<Card title="증거 라벨" href="/evidence-labels">
Acceptance report, split result, regression log를 운영 증거로 해석할 때의 라벨 기준을 맞춘다.
</Card>
<Card title="설정 참조" href="/configuration-reference">
런타임 설정, 환경 변수 override, fail-closed 설정 해석 규칙을 함께 확인한다.
</Card>
<Card title="작업자 위임" href="/dispatch-workers">
Worker deliverable을 acceptance 전 재검증 흐름과 연결한다.
</Card>
</CardGroup>
