Use when a design or decision needs adversarial multi-round scrutiny from several independent model vendors. Runs anonymized A/B/C reviewer rounds with a rotating judge and produces a consensus report.
Scanned 9/9/2026
Install to Claude Code
npx -y skills add baekenough/oh-my-customcode --skill agora --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Agora?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/baekenough-agora)More formats (shields.io, HTML) on the badges page.
---
name: agora
description: Use when a design or decision needs adversarial multi-round scrutiny from several independent model vendors. Runs anonymized A/B/C reviewer rounds with a rotating judge and produces a consensus report.
scope: core
version: 1.0.0
user-invocable: true
argument-hint: "<topic> [--attach <path>] [--max-rounds <N>] [--auto]"
---
# Agora
## 개요
`agora`는 하나의 설계·결정 주제를 3개 독립 벤더 CLI에게 익명 A/B/C 라벨로 검토시키고, 라운드마다 모델이 바뀌는 심판이 판정하는 다중턴 합의 스킬입니다. 라벨-벤더 매핑은 `SEALED/`에 격리되어 심판의 입력 경로에 오르지 않으며, 벤더 공개는 최종 보고서 생성 단계에서만 이루어집니다.
**이 설계는 결정적 익명성을 주장하지 않습니다.** 목표는 "벤더 식별 불가"가 아니라 **"벤더 식별이 심판의 기본 관찰 경로에 놓이지 않음"** 수준입니다. 라벨 뒤에 벤더가 계속 숨겨진다고 가정하지 마십시오 — 남는 누출 경로는 [신뢰 경계](#신뢰-경계)에 열거되어 있습니다.
리뷰어 3벤더(`claude -p`, `omx exec`, `agy -p`)와 심판 로테이션 3슬롯은 **모델 단위로만** 겹치지 않습니다(스펙 REQ-3). 겹치는 축과 겹치지 않는 축을 구분해 적습니다.
**아래 표는 리뷰어를 벤더 슬러그(`claude`/`omx`/`agy`)로 지칭합니다 — `A`/`B`/`C`가 아닙니다.** A/B/C는 라운드마다 시드 `agora-<epoch>-r<N>`으로 다시 섞이는 익명 라벨이므로 어떤 벤더에도 고정되지 않으며, **"리뷰어 A" 같은 고정 벤더는 존재하지 않습니다**. 따라서 게이트가 출력하는 `리뷰어: A BUILD_WITH_CHANGES · B BUILD · C REDESIGN`을 벤더 판정으로 읽지 마십시오 — 같은 세션 안에서도 다음 라운드의 `A`는 다른 벤더입니다. 라벨과 벤더의 대응은 `report.md`에서만 공개됩니다.
| 축 | 겹침 |
|----|------|
| 모델 ID | 없음 — 리뷰어는 `claude-opus-4-8`(`claude` CLI) / `omx` 기본 모델(`omx` CLI) / `gemini-3.1-pro-high`(`agy` CLI), 심판 슬롯은 1: `claude-opus-5`(`claude` CLI) / 2: `claude-opus-4-6-thinking`(`agy` CLI) / 3: `gpt-oss-120b-medium`(`agy` CLI) |
| CLI 바이너리 | **있음** — `claude` 바이너리는 `claude` 리뷰어와 심판 슬롯 1이, `agy` 바이너리는 `agy` 리뷰어와 심판 슬롯 2·3이 공유합니다 |
| 모델 계열 | **있음** — 심판 3슬롯 중 2슬롯(`claude-opus-5`, `claude-opus-4-6-thinking`)이 `claude` 리뷰어의 모델(`claude-opus-4-8`)과 같은 Claude 계열입니다. 이 2슬롯은 CLI로는 각각 `claude`와 `agy`이므로 CLI 축과 계열 축은 서로 다른 모양으로 겹칩니다 |
따라서 "리뷰-판정 간 교차 오염이 없다"고 말할 수 있는 범위는 **모델 ID 단위까지**입니다. 같은 계열 모델이 공유하는 사전 학습 편향은 이 분리로 제거되지 않습니다.
## 라운드 파이프라인
각 라운드는 세 스크립트를 순서대로 호출합니다.
| 단계 | 명령 | 산출 |
|------|------|------|
| 리뷰어 | `bash scripts/reviewers.sh --run --session-dir <dir> --round <N> --prompt-file <f>` | `SEALED/raw/round-N/{claude,omx,agy}.json` |
| 익명화 | `bash scripts/anonymize.sh --build --session-dir <dir> --round <N> --seed agora-<epoch>-r<N> --topic <t> --attachments <json> --agenda <json>` | `SEALED/mapping/round-N.json` + `anon/round-N.json` |
| 심판 | `bash scripts/judge.sh --run --anon-file <dir>/anon/round-N.json --out-file <dir>/verdict/round-N.json --round <N>` | `verdict/round-N.json` |
| 종료 판정 | `bash scripts/agora.sh --decide-stop < <dir>/state.json` | `CONSENSUS\|STALLED\|MAX_ROUNDS\|USER\|CONTINUE` |
라운드 1은 백지 상태로 진행됩니다 — `agenda`, `prior_rounds`, 심판 초안 없이 `topic`과 `attachments`만 리뷰어에게 전달됩니다. 세션 전체에서 진짜 독립 의견을 얻는 유일한 라운드이기 때문입니다. 라운드 2부터는 직전 심판의 `agenda` + 직전 2라운드의 `prior_rounds` + 직전 초안이 함께 전달되어 수렴을 유도합니다.
### CLI 진입점
```
bash agora.sh --decide-stop < <dir>/state.json
bash agora.sh --start "<topic>" [--attach <path>]... [--max-rounds <N>] [--auto]
bash agora.sh --round <N> --session-dir <dir> [--extra-agenda <json-array>]
bash agora.sh --gate --session-dir <dir> --round <N>
bash agora.sh --set-stop <CODE> --session-dir <dir>
bash agora.sh --report --session-dir <dir>
```
| 진입점 | 파일을 쓰는가 | 실행 주체 |
|--------|:---:|-----------|
| `--decide-stop` | 아니오 | `agora-runner` (라운드 실행 직후 정지 코드 산출) |
| `--start` | **예** | `agora-runner` |
| `--round` | **예** | `agora-runner` |
| `--gate` | 아니오 | 오케스트레이터 (사용자 상호작용) |
| `--set-stop` | **예** | `agora-runner` |
| `--report` | **예** | `agora-runner` |
#### 채널 계약
`--start`의 **stdout에는 세션 디렉토리 절대경로 한 줄만** 실립니다. 게이트드 모드에서 라운드 1의 게이트 블록은 **stderr로 나갑니다** — 문서화된 관용구 `dir=$(bash agora.sh --start ...)`가 stdout을 통째로 캡처하므로, 게이트가 stdout에 있으면 `$dir`이 게이트 17줄에 경로가 붙은 문자열이 되어 쓸 수 없게 되기 때문입니다.
반면 **독립 서브커맨드 `--gate`는 블록을 stdout에 출력합니다** — 그 서브커맨드에서는 게이트 렌더링이 부산물이 아니라 산출물 전부이기 때문입니다.
`agora-runner`가 `--start`를 실행하면 게이트 블록은 러너의 stderr로 흘러가 반환 계약에 담기지 못합니다. 오케스트레이터는 `--start` 반환 후 `--gate --session-dir <dir> --round 1`로 게이트를 **다시 렌더**해 사용자에게 표시하십시오.
#### `--set-stop <CODE>`
`.stop` 필드(= `report.md`의 `종료 사유`)를 기록하는 **유일한 수단**입니다. 게이트드 모드에서 라운드 2 이후에는 `.stop`에 다른 writer가 없으므로, 이 명령을 건너뛰면 세션이 아무리 깨끗하게 끝나도 `report.md`가 `종료 사유: UNKNOWN`을 출력합니다.
| 항목 | 값 |
|------|-----|
| 허용 코드 | `CONSENSUS` · `STALLED` · `MAX_ROUNDS` · `USER` |
| 거부 코드 | `CONTINUE` → exit 64. "아직 정지하지 않았다"는 뜻이므로 종료 사유로 기록될 수 없습니다 — 다음 라운드를 도십시오 |
| 그 밖의 미지 코드 | exit 64. stderr에 허용 목록을 출력합니다 |
| 코드 자체가 없거나 `--`로 시작 | exit 64 |
| 범위 | 세션 단위(`--round` 없음). `.stop`은 세션이 **어떻게** 끝났는지를, `.round`가 **어디서** 끝났는지를 담습니다. 재기록은 멱등이라 무해합니다 |
허용 코드 집합은 하드코딩이 아니라 `decide_stop` 함수 본문에서 런타임에 읽어냅니다. 정지 조건이 추가·개명되면 자동으로 따라갑니다.
**절차**: 호출자는 매 라운드 `--decide-stop` 결과가 `CONTINUE`가 **아니면** 그 코드를 그대로 `--set-stop`으로 기록한 뒤 `--report`를 실행합니다. 사용자가 게이트에서 `s`를 고른 경우도 `--set-stop USER`로 직접 기록해야 합니다. (`--start --auto`와 게이트드 `--start`의 라운드 1은 이 기록을 스크립트가 내부에서 수행하므로 예외입니다.)
`--extra-agenda`는 게이트의 `e` 옵션을 실행하는 유일한 수단입니다. 값은 **JSON 배열 문자열**이어야 하며(예: `--extra-agenda '["롤백 명령 시퀀스를 제시할 것"]'`), 심판 `agenda[]` 뒤에 이어붙습니다. 문자열 하나를 그대로 넘기면 `jq` 병합이 실패합니다. 생략 시 기본값은 `[]`입니다.
환경 오버라이드:
| 변수 | 기본값 | 용도 |
|------|--------|------|
| `AGORA_SESSION_EPOCH` | 없음 (자동 생성) | 세션 시드 고정 (재현용) |
| `AGORA_OUTPUT_ROOT` | `.claude/outputs/sessions` | 아티팩트 루트 |
| `AGORA_TIMEOUT_SECS` | `300` | 리뷰어/심판 CLI 타임아웃 |
| `AGORA_CLAUDE_BIN` | `claude` (PATH 탐색) | 리뷰어·심판의 Claude CLI 경로 오버라이드 |
| `AGORA_AGY_BIN` | `agy` (PATH 탐색) | 리뷰어·심판의 agy CLI 경로 오버라이드 |
| `AGORA_OMX_BIN` | **`/opt/homebrew/bin/omx` (절대 경로)** | 리뷰어 omx CLI 경로 오버라이드. 다른 셋과 달리 PATH를 탐색하지 않으므로, omx가 이 경로에 없는 머신에서는 반드시 설정해야 합니다 |
| `AGORA_FORCE_TIMEOUT_FALLBACK` | `0` | **테스트 전용** — `gtimeout` 존재 여부와 무관하게 wait 기반 타임아웃 폴백 경로를 강제합니다. 운영 실행에서는 설정하지 마십시오 |
| `AGORA_VERDICT_SCHEMA` | 스크립트 옆의 `verdict-schema.json` | **테스트 전용** — `judge.sh`가 검증에 쓰는 스키마 경로 대체. 존재하지 않는 경로를 가리켜 exit 68 경로를 재현하는 용도 |
이 변수들은 **자식 CLI에는 전달되지 않습니다** — 아래 [벤더 호출 환경 정화](#벤더-호출-환경-정화)를 참조하십시오.
## 권한 우회 플래그 (실행 전 반드시 확인)
**이 스킬은 리뷰어 벤더 CLI 중 둘을 권한 프롬프트를 건너뛰는 플래그와 함께 실행합니다.** 운영자 승인을 요구하지 않고 자동으로 붙으므로, 스킬을 돌리기 전에 이 사실을 알고 있어야 합니다.
| 호출 | CLI | 붙는 플래그 |
|------|-----|-------------|
| 리뷰어 | `claude` | `--enable-auto-mode` |
| 리뷰어 | `omx` | 없음 |
| 리뷰어 | `agy` | `--dangerously-skip-permissions` |
| 심판 슬롯 1 | `claude` | **없음** |
| 심판 슬롯 2·3 | `agy` | **없음** |
**리뷰어에 붙는 이유**: 이 두 이름은 사용자 셸의 **별칭(alias)**이며 별칭 정의에 해당 플래그가 이미 들어 있습니다. 별칭은 비대화형 `bash script.sh` 실행에서 확장되지 않으므로, 스크립트가 명시적으로 붙이지 않으면 CLI가 대화형 실행과 다르게 동작합니다 — `claude`는 auto mode 없이 돌고, `agy`는 권한 프롬프트에서 멈춰 라운드 타임아웃(→ 결측)에 걸립니다. `omx`는 별칭이 아닌 실제 바이너리라 추가 플래그가 없습니다.
**심판에는 붙지 않습니다 — 비대칭은 실재합니다.** `judge.sh`는 같은 두 CLI를 호출하면서 두 플래그를 모두 생략하며, 심판 CLI에 넘어가는 인자는 `-p --model` (그리고 `agy`의 경우 `--output-format json --json-schema`)와 프롬프트 문자열뿐입니다. 스크립트 주석은 리뷰어 쪽 플래그의 근거만 적고 심판 쪽 생략의 근거는 적지 않으므로, **여기서는 관측된 사실만 기술합니다**: 심판은 리뷰어보다 낮은 도구 권한으로 실행되며, 심판이 권한 프롬프트를 띄우는 환경에서는 그 슬롯이 타임아웃되어 로테이션이 다음 슬롯으로 전진합니다(3슬롯 모두 그러면 exit `4`). 이 비대칭은 심판이 익명 번들 외의 자료에 손대지 못하게 하는 방향과는 정합하지만, 코드가 그 의도를 명시하지는 않았습니다.
## 종료 코드
스크립트 4종이 실제로 반환하는 코드 전부입니다. `agora.sh`는 `reviewers.sh`/`anonymize.sh`/`judge.sh`의 코드를 **그대로 전파**하므로(예외: 68은 자체 진단 한 줄을 덧붙인 뒤 전파), 오케스트레이터가 `--round`에서 보는 코드는 아래 전부가 될 수 있습니다.
| 코드 | 반환 스크립트 | 의미 | 재시도 |
|------|--------------|------|:---:|
| `0` | 전부 | 성공 | — |
| `1` | `anonymize.sh` | **지문 검출** — 검사 대상 텍스트에서 금칙 패턴이 걸림. 익명성 파기를 막기 위한 하드 스톱 | 아니오 |
| `3` | `reviewers.sh` · `anonymize.sh` | **유효 리뷰어 2인 미만**으로 라운드 중단. 두 스크립트가 같은 코드를 쓰는 것은 의도적이며, 어느 단계에서 걸렸는지는 stderr로 구분합니다(아래 [결측 판정](#결측-판정은-2단계입니다)) | 아니오 |
| `4` | `judge.sh` | 심판 로테이션 3슬롯이 **전부** 실패 — CLI 실패·타임아웃·JSON 파싱 실패·스키마 위반을 모두 포함 | 아니오 (로테이션 대체까지가 이미 시도의 전부) |
| `64` | 전부 | 사용법 오류 — 미지의 옵션, 필수 플래그 누락, `--extra-agenda`가 JSON 배열이 아님, `--set-stop`의 코드 누락·미지 코드·`CONTINUE` | 아니오 |
| `65` | `anonymize.sh` | 직전 라운드의 sealed 데이터가 **존재하는데 파싱 불가**(무결성 문제) 또는 미지의 벤더 식별자. `1`(지문)과 일부러 구분합니다 — "번들이 벤더를 누출할 뻔했다"와 "직전 라운드 데이터가 손상됐다"는 운영자 대응이 다릅니다 | 아니오 |
| `66` | `agora.sh` · `judge.sh` | 입력 부재 — 세션 디렉토리를 찾을 수 없음(`agora.sh`) / `--anon-file`이 없음(`judge.sh`) | 아니오 |
| `68` | `judge.sh` | **설정 오류** — `verdict-schema.json`을 읽거나 파싱할 수 없음. 심판의 실패가 아니라 배포 설정의 결함이므로 재시도해도 같은 원인으로 즉시 재실패합니다 | 아니오 |
| `73` | `agora.sh` | **기록 실패** — `state.json` 또는 `report.md`를 쓸 수 없음. 아래 별항 참조 | 아니오 |
내부 전용 코드: `124`(타임아웃)는 `run_with_timeout`이 GNU `timeout` 관례에 맞춰 정규화한 값이며 **호출자에게 그대로 노출되지 않습니다** — 리뷰어에서는 결측으로(→ `3`), 심판에서는 로테이션 전진으로(→ `4`) 흡수됩니다. `reviewers.sh`/`judge.sh`의 `65`(미지 CLI 슬러그)도 로스터가 하드코딩이라 실제로는 도달하지 않습니다.
### exit 73 — 라운드는 돌았는데 기록되지 않음
`73`(EX_CANTCREAT)은 다른 코드와 성격이 완전히 다릅니다. 여기까지 왔다는 것은 **리뷰어 3벤더와 심판이 이미 호출되어 과금까지 끝났다**는 뜻이고, 실패한 것은 그 결과를 `state.json`(또는 `report.md`)에 남기는 마지막 단계뿐입니다.
오케스트레이터의 대응:
- **산출물을 소비하지 마십시오.** `state.json`이 갱신되지 않았으므로 `.round`가 뒤처져 있고, 그 상태에서 `--decide-stop`은 `MAX_ROUNDS`/`STALLED`를 영원히 내지 못하며 `--report`는 뒤처진 라운드의 verdict를 읽습니다.
- **라운드를 재실행하지 마십시오.** 벤더가 다시 과금됩니다.
- 실패 원인(디렉토리 권한, 디스크, 심판이 내놓은 비정수 `new_findings` 등)을 stderr 진단 줄에서 확인해 사용자에게 보고하고 지시를 기다리십시오.
`--set-stop`이 73을 반환한 경우도 같습니다 — 종료 사유가 기록되지 않았으므로 `--report`를 실행하면 `종료 사유: UNKNOWN`이 박힌 보고서가 나옵니다. 보고서를 만들기 전에 멈추십시오.
### 결측 판정은 2단계입니다
리뷰어 결측은 **서로 다른 두 스크립트가 순차로** 판정하며, 둘 다 하한은 "유효 응답 2인"이고 둘 다 exit `3`입니다.
| 단계 | 스크립트 | 무엇을 결측으로 세는가 |
|------|----------|----------------------|
| 1 | `reviewers.sh` | CLI가 응답하지 못한 벤더 — 타임아웃(124), 비영 종료, **구문상 JSON이 아닌 출력**. 각 벤더는 2회 시도(최초 + 재시도 1회) 후 결측 처리되며, 결측 벤더는 파일을 아예 남기지 않습니다. 2개 이상 결측이면 exit 3 |
| 2 | `anonymize.sh` | 파일이 없거나, 있어도 **응답 스키마 계약을 위반**한 벤더(`overall` enum, `findings[]`의 `severity`/`verdict` enum, 빈 문자열 금지 필드 등). 이 검사는 `reviewers.sh`가 통과시킨 뒤에 걸리므로 별도 하한이 필요합니다 — 유효 응답이 2 미만이면 exit 3 |
2단계 하한이 없으면 "1개 유효 + 2개 스키마 위반"이 리뷰어 1인짜리 번들을 만들고, 심판이 단일 의견을 놓고 합의를 판정하게 됩니다. 두 단계의 코드가 같으므로 운영자에게 보이는 의미("리뷰어 부족으로 라운드 중단")는 어느 쪽이 걸렸든 동일하며, 구분이 필요하면 stderr 메시지를 보십시오 — 1단계는 `2 or more reviewers missing`, 2단계는 `valid reviewer response(s); 2 or more are required`입니다.
### 심판 응답 검증
`judge.sh`는 심판 출력이 구문상 JSON인지만 보지 않고, `verdict-schema.json`을 기준으로 세 가지를 검사합니다. 필수 필드 목록·선언 타입·enum 값을 모두 **스키마 파일에서 읽어오므로** 스키마가 바뀌면 검사도 따라갑니다.
| 검사 | 위반 예 |
|------|---------|
| 필수 필드 존재(그리고 non-null) | `draft` 누락 |
| **선언 타입 일치** | 스키마가 배열로 선언한 `agenda`에 `"1. 단일 의제"` 문자열이 옴 |
| enum 값(`consensus`·`verdict`) | `verdict: "MERGE"` |
셋 중 하나라도 걸리면 그 슬롯은 실패로 처리되어 **로테이션이 다음 슬롯으로 전진**하고, 3슬롯이 모두 소진되면 exit `4`입니다. 타입 검사가 특히 중요한 이유는, 타입이 틀린 `agenda`가 검증을 통과해 저장되면 다음 라운드의 `jq '. + $extra'` 병합이 죽고 → 의제가 빈 값으로 붕괴하고 → 리뷰어 프롬프트에 의제 섹션이 **비어 있는 채로** 3벤더가 전부 호출·과금된 뒤에야 문제가 드러나기 때문입니다.
## 사용자 게이트
`mode: gated`(기본값)에서 매 라운드 종료 후 오케스트레이터가 아래 형식을 표시합니다. 벤더는 노출하지 않고 라벨만 노출합니다 — 벤더 공개는 `report.md` 생성 단계에서만 이루어집니다.
```
─── Agora Round 2/5 ───────────────────────────────
심판: (모델 로테이션 #2)
Consensus: MAJORITY Verdict: BUILD_WITH_CHANGES
리뷰어: A BUILD_WITH_CHANGES · B BUILD · C REDESIGN
신규 지적: 2건 최고 심각도: HIGH
라운드 소요: 412초 (참고: 심판 로테이션 3슬롯 순차 재시도 시 최악 약 15분)
해소됨(1)
F1 상태 파일 경합 — 세션 디렉토리 격리로 충분
미해소(1)
F4 [HIGH] 롤백 경로 부재 — A REJECT / C KEEP / B 미언급
다음 라운드 의제
- F4 의 심각도 판정 근거를 각자 제시할 것
- 롤백 경로의 구체적 명령 시퀀스
[c] 계속 [s] 중단하고 보고서 [e] 의제 추가 후 계속
```
**`리뷰어:` 줄의 A/B/C는 벤더가 아닙니다.** 이 라벨은 해당 라운드의 시드로 새로 섞인 것이라 라운드마다 가리키는 벤더가 바뀝니다. `A BUILD_WITH_CHANGES`를 특정 벤더의 입장으로 읽지 마십시오 — 라운드 간 비교도 불가합니다. 벤더 대응은 `report.md`의 「라운드별 참여자(익명 해제)」에서만 확인할 수 있습니다. 결측 벤더가 있으면 이 줄에는 라벨이 2개만 나옵니다.
**토큰 누적은 표시되지 않습니다.** 벤더 CLI들이 토큰 사용량을 일관된 형식으로 보고하지 않아 계측을 신뢰할 수 없으므로 구현하지 않았습니다 — `state.json`의 `history[].tokens`는 항상 `0`으로 기록됩니다. 게이트에 누적 토큰 줄을 두는 설계 스펙 §10과의 **의도적 차이**입니다. 비용 감각은 아래 [`--auto` 경고](#--auto-경고)의 라운드당 추정치로 대체하고, 게이트가 제공하는 실측값은 `라운드 소요` 초 하나뿐임을 전제하십시오.
| 키 | 동작 |
|----|------|
| `c` | 다음 라운드 진행. 심판 의제를 그대로 사용 |
| `s` | 루프 종료(`stop: "USER"`), 현재까지의 결과로 `report.md` 생성 |
| `e` | 사용자 의제를 입력받아 심판 `agenda[]`에 **추가**한 뒤 다음 라운드 진행 |
`e`는 추가만 가능하며 심판 의제를 덮어쓰지 못합니다. 사용자가 심판 의제를 삭제할 수 있으면 심판의 의제 설정 권한이 형해화되고, 사용자가 불편해하는 쟁점이 조용히 사라지는 경로가 생기기 때문입니다.
### 게이트드 모드의 책임 분리
게이트드 모드에서 **루프를 도는 주체는 스크립트가 아니라 호출자**입니다. 스크립트는 라운드 하나만 실행하고 반환하며, 정지 여부를 스스로 판단하지 않습니다.
| 명령 | 하는 일 | 하지 않는 일 |
|------|---------|--------------|
| `--start` (`--auto` 없이) | 세션 생성 + **라운드 1만** 실행 후 반환 | 라운드 2 이후를 돌지 않음 |
| `--round <N>` | 지정 라운드를 실행하고 `state.json`에 기록 | `--decide-stop`을 호출하지 않음. `.stop`도 `max_rounds`도 **검사하지 않음** |
| `--gate` | 게이트 화면 렌더 (stdout) | 사용자 입력을 받지 않음 |
| `--set-stop` | `.stop`에 종료 사유 기록 | 정지 여부를 스스로 판단하지 않음 — 호출자가 준 코드를 그대로 씀 |
| `--report` | `report.md` 생성 | 정지 판단을 하지 않음. `.stop`을 쓰지도 않음 — 미기록이면 `UNKNOWN`을 출력 |
- **라운드 상한은 `--round`를 막지 않습니다.** `max_rounds`를 넘긴 라운드도, 이미 `.stop`이 기록된 세션의 라운드도 거부 없이 실행됩니다. 정지 판단은 전적으로 호출자 몫입니다.
- **게이트드 `--start`가 게이트 없이 끝나는 경로가 있습니다.** 라운드 1에서 종료 조건이 걸리면 `.stop` 기록과 `report.md` 생성까지 수행한 뒤 게이트를 렌더하지 않고 반환합니다. 따라서 `--start` 반환 후에는 게이트 출력 유무가 아니라 `state.json`의 `.stop`을 읽어 분기하십시오.
여기서 **호출자는 두 층**입니다 — 오케스트레이터(메인 대화)와 `agora-runner` 서브에이전트. 파일을 쓰는 명령(`--start`·`--round`·`--set-stop`·`--report`)은 `agora-runner`가, 파일을 쓰지 않는 순수 출력 명령(`--gate`)은 오케스트레이터가 실행합니다(R010). 매 라운드 흐름은 다음과 같습니다.
1. **오케스트레이터 → `agora-runner` 위임**: 러너가 `--round <N> --session-dir <dir>` 를 실행한 뒤 `--decide-stop < <dir>/state.json` 으로 정지 코드를 산출해, **verdict 요약 + `stop_code`** 를 반환합니다.
2. **오케스트레이터가 직접 실행**: `--gate --session-dir <dir> --round <N>` 로 게이트를 표시하고 사용자 응답(`c`/`s`/`e`)을 받습니다. `--gate`는 파일을 쓰지 않으므로 오케스트레이터가 실행해도 R010에 걸리지 않으며, 서브에이전트는 사용자와 상호작용하지 않으므로 이 단계를 위임할 수도 없습니다.
3. **계속이면 다음 라운드를 다시 위임**하고(사용자가 `e`를 골랐으면 `--extra-agenda <json-array>` 동반), **정지면 `--set-stop <CODE>` + `--report --session-dir <dir>` 를 `agora-runner`에 위임**합니다. `<CODE>`는 1단계의 `stop_code`를 그대로 쓰되, 사용자가 `s`를 골라 멈추는 경우에는 `USER`입니다. 이 기록을 건너뛰면 보고서가 `종료 사유: UNKNOWN`으로 나옵니다.
## `--auto` 경고
**`--auto`는 매 라운드 게이트를 생략합니다.** 비용 상한이 명확할 때만 사용하십시오.
| 항목 | 값 |
|------|-----|
| 라운드당 토큰 추정 | 60~120k (리뷰어 3 + 심판 1) |
| 기본 라운드 상한 | 5 (`--max-rounds`로 조정) |
| **최악의 경우 총 토큰** | **약 600k** |
**소요 시간 주의**: 리뷰어 단계와 심판 단계는 병렬성이 다르므로 벽시계 계산이 다릅니다.
| 단계 | 실행 형태 | 최악 벽시계 (`AGORA_TIMEOUT_SECS`=300 기준) |
|------|-----------|-------------------------------------------|
| 리뷰어 | 3벤더 **병렬 팬아웃**(`&` + `wait`), 벤더당 최초 1회 + 재시도 1회 | 300초 × 2시도 = **600초**. 벤더 수는 벽시계에 곱해지지 않습니다 |
| 심판 | 로테이션 3슬롯 **순차** 시도 | 300초 × 3슬롯 = **900초** |
| 라운드 합계 | 두 단계는 순차 | **약 1500초 ≈ 25분** |
기본 상한 5라운드를 `--auto`로 끝까지 돌면 최악 약 2시간이며, 게이트가 없으므로 이 지연이 중단 없이 누적됩니다. (게이트가 표시하는 `참고: 심판 로테이션 3슬롯 순차 재시도 시 최악 약 15분`은 **심판 단계만**의 값이며 라운드 전체가 아닙니다.)
## 신뢰 경계
익명성은 프롬프트 지시가 아니라 **디렉토리 경계**로 유지됩니다.
| 주체 | `SEALED/` 접근 | `anon/` 접근 |
|------|:---:|:---:|
| 오케스트레이터 (메인 대화) | 금지 | 허용 |
| `agora-runner` 에이전트 | 금지 | 허용 |
| 리뷰어 CLI 3종 | 금지 | 금지 (라운드 내에서 서로를 보지 않음) |
| 심판 CLI | 금지 | 허용 (익명 번들만이 유일한 입력) |
| `anonymize.sh` | **허용** — `SEALED/raw/`(원문 읽기)와 `SEALED/mapping/`(직전 라운드 매핑 읽기 + 이번 라운드 매핑 봉인) | 허용 (`anon/round-N.json`을 생산하는 주체) |
| `report.md` 생성 단계 | **허용** — `SEALED/mapping/` 읽기 | 허용 (익명 해제가 이 단계의 목적) |
`SEALED/`를 읽는 주체는 **둘**입니다 — 파이프라인 안쪽의 `anonymize.sh`와 마지막의 보고서 생성 단계(스펙 §4). `anonymize.sh`는 봉인된 원문·매핑을 읽지만 그 산출물인 `anon/` 번들에는 라벨만 남기므로, 봉인을 읽는 것과 봉인을 푸는 것은 구분됩니다. 익명 해제는 `report.md` 생성 단계에서만 일어납니다.
**한계 1 — 격리를 지켜도 남는 누출**(스펙 §6): 위 표가 완벽히 지켜져도 심판은 벤더를 추론할 단서를 갖습니다. 익명성은 라벨이 유지되는 동안에도 아래 세 경로로 새어 나갑니다.
1. **라운드 내 일관성** — 한 라운드 안에서 하나의 라벨은 그 벤더의 전체 응답을 묶습니다. 심판은 "A의 findings 5건이 모두 성능 축에 집중되어 있다"와 같은 스타일 패턴을 관찰할 수 있습니다.
2. **라운드 간 일관성** — 재라벨링(과거 라운드의 라벨을 현재 라운드 매핑으로 변환)을 도입한 이상 `prior_rounds[]`를 통해 라운드 간 일관성도 관찰 가능해집니다. 이는 의도된 트레이드오프입니다 — "A가 입장을 바꿨다"는 오독을 막는 편익이 벤더 추론 위험보다 크다고 판단했습니다.
3. **논증 스타일 지문** — 서식 정규화는 문체 지문을 줄이지만 **논증 스타일 지문은 줄이지 못합니다**. 특정 모델이 반복적으로 특정 축을 파고드는 경향은 템플릿으로 흡수되지 않습니다.
그러므로 개요에 적은 대로 이 설계는 **결정적 익명성을 주장하지 않습니다**. 달성하는 것은 "벤더 식별 불가"가 아니라 "벤더 식별이 심판의 기본 관찰 경로에 놓이지 않음"이며, 심판이 추론을 시도하면 부분적으로 성공할 수 있다고 전제해야 합니다.
**한계 2 — 격리 자체가 강제되지 않음**: 이 격리는 **관례이지 하드 블록이 아닙니다**. 오케스트레이터도 심판 CLI도 `SEALED/`를 읽는 파일 읽기 능력 자체는 갖고 있습니다 — 읽지 못하는 것이 아니라 읽지 않도록 설계된 것입니다. PreToolUse 훅으로 `SEALED/` 경로 Read를 하드 블록하는 방안은 검토했으나, 훅은 프로젝트 전역에 영향을 주므로 초기 도입에서는 제외했습니다(위반율 관측 시 R021 Hard Enforcement Candidates 승격 검토 대상). 실질 방어선은 아래 지문 검사이며, 이는 "누출이 발생했는가"를 사후 탐지할 뿐 "누출을 시도할 수 없게" 만들지는 않습니다.
**한계 3 — 자식 CLI가 cwd를 상속함**: 벤더·심판 CLI는 `AGORA_*` 환경변수를 제거한 채 실행되지만(아래 [벤더 호출 환경 정화](#벤더-호출-환경-정화)), **작업 디렉토리는 상속합니다**. 기본 출력 루트 `AGORA_OUTPUT_ROOT`가 상대경로(`.claude/outputs/sessions`)이므로, 상속받은 cwd에서 `.claude/outputs/sessions/**`를 훑으면 세션 트리 — `SEALED/` 포함 — 에 도달할 수 있습니다. 환경 정화는 "세션 좌표를 **건네주지** 않는다"를 보장할 뿐 "찾을 수 **없게** 한다"를 보장하지 않습니다. 절대경로 `AGORA_OUTPUT_ROOT`를 세션 트리 밖으로 지정하면 이 경로는 좁아지지만, cwd 상속 자체는 남습니다.
### 지문 검사
`anon/round-N.json`을 그대로 검사하지 않습니다. `anonymize.sh`가 번들에서 **벤더 유래 텍스트만 뽑아낸 파생 문서**를 만들어 그것을 검사합니다.
| 필드 | 검사 대상 | 이유 |
|------|:---:|------|
| `reviewers[]` (이번 라운드) | **예** | 리뷰어 저작 — 익명화 대상 당사자 |
| `prior_rounds[].reviewers[]` | **예** | 재라벨링되어 다음 라운드 심판에게 전달되는 리뷰어 저작 텍스트 |
| `prior_rounds[].draft` · `.verdict` | **예** (운영자 어휘 제거 후) | 심판 저작. 심판은 익명화의 *주체*이지만, 그 초안·판정은 `relabel_prior`를 타고 **다음 라운드의 새 심판**에게 넘어갑니다. 심판이 쓴 귀속 추정("A는 Claude 계열로 보인다")이 후임에게 무검사로 전달되는 것이 바로 이 검사가 막으려는 누출입니다 |
| `topic` · `agenda` · `attachments` | **아니오** | **운영자 저작 — 의도적 제외(Ruling 10)**. 토론 주제 자체가 "제미나이를 도입할까"일 수 있으며, 주제가 벤더를 언급했다는 이유로 매 라운드가 중단되면 스킬이 성립하지 않습니다 |
| `round` 등 나머지 메타데이터 | 아니오 | 벤더 유래 텍스트가 아님 |
심판 저작 필드는 검사 전에 **운영자 어휘를 단어 단위로 걸러냅니다** — 심판이 주제·의제를 정당하게 인용할 수 있기 때문입니다. 필터는 토큰화 → 운영자 단어 제거 → 재결합 방식이며, **부분 문자열이 아니라 온전한 단어에만** 작동합니다(운영자 단어 "mini"가 "gemini"를 쪼개 검사를 무력화하는 역방향 회피를 막기 위함). `/`, `-`, `.`, `_`는 단어 문자로 취급되어 경로 형태와 버전 표기가 보존됩니다. 잔여 오탐(의도적, 차단 쪽으로 기움): 심판 텍스트의 운영자 단어에 한국어 조사가 붙으면(주제의 `제미나이` vs 심판의 `제미나이는`) 다른 토큰이라 면제되지 않고 중단시킵니다. 리뷰어 텍스트는 **필터하지 않습니다** — 리뷰어는 익명화 대상이므로 주제를 그대로 되뇌는 것도 누출로 취급합니다.
### 금칙 패턴이 실제로 막는 것
패턴은 대소문자 무시(`grep -Eiq`)로 적용되며 3계층입니다. **"모델명을 막는다"고 뭉뚱그리면 사실과 다릅니다** — 계층마다 걸리는 조건이 다릅니다.
| 계층 | 토큰 | 매칭 방식 |
|------|------|-----------|
| 1. 명백한 벤더·제품 토큰 | `codex` `omx` `gpt` `claude` `gemini` `antigravity` `anthropic` `openai` + 한글 음차(`클로드` `제미나이` `지피티` `앤트로픽` `오픈에이아이`) | **단어 경계 없는 부분 문자열**. 경계를 두면 `claudecode`나 `chatgpt` 같은 실제 자기 식별을 통과시키므로 일부러 뺐습니다 |
| 1'. `agy` | `agy` | **양쪽 단어 경계 유지**. 3글자라 무관한 단어(stagy, cagy, 성씨) 안에 들어갈 수 있고, 모델이 자기를 "agycode"라 부르는 형태도 없기 때문 |
| 2. 영어 일반 단어와 겹치는 모델 계열명 | `opus` `sonnet` `haiku` `flash` | **두 형태에서만** — (a) 버전 인접(`Opus 4.8`, `sonnet-5`, `flash 2.0`) (b) 비ASCII 문자 인접(`Sonnet 관점에서`). 맨 단어를 막으면 정상 리뷰 산문("magnum opus", "flash memory")에서 중단됩니다 |
| 3. 봉인 경로 형태 | `SEALED/` `/mapping/` `raw/round-` | 부분 문자열 |
실제로 이 세션에서 쓰이는 모델 ID는 전부 계층 1에 걸립니다 — `claude-opus-4-8`/`claude-opus-5`/`claude-opus-4-6-thinking`은 `claude`로, `gemini-3.1-pro-high`는 `gemini`로, `gpt-oss-120b-medium`은 `gpt`로, `omx:default`는 `omx`로 걸립니다.
**감수한 미탐(false negative)**: 버전도 한글 인접도 없이 영어 산문 안에 놓인 맨 모델 계열명 — 예: `Sonnet would argue` — 은 `sonnet-length prose` 와 정규식으로 구별할 수 없어 **통과합니다**. **감수한 오탐(false positive)**: `flash 메모리` 같은 한영 혼용 기술 용어는 계층 2-(b)에 걸려 라운드를 중단시킵니다. 오탐은 시끄럽고 복구 가능하지만 미탐은 조용히 익명성을 깨므로, 과차단 쪽을 택한 결과입니다.
지문이 걸리면 `anonymize.sh`가 exit `1`로 중단하며, `SEALED/mapping/`과 `anon/`에는 **아무것도 쓰이지 않습니다** — 매핑과 번들은 검사를 통과할 때까지 임시 디렉토리에 머뭅니다.
### 벤더 호출 환경 정화
`reviewers.sh`와 `judge.sh`는 자식 CLI를 실행할 때 **접두사 `AGORA_`로 시작하는 모든 환경변수를 제거**합니다(`env -u`, 실제 export된 이름을 `compgen -e`로 수집하므로 이름 목록 하드코딩이 아님). 스크립트가 그 값을 넘기지 않는다는 것만으로는 부족했기 때문입니다 — 운영자 셸에 `AGORA_OUTPUT_ROOT`가 export되어 있으면 그 값이 `agora.sh`를 거쳐 모든 벤더 CLI에 그대로 상속되고, 세션 트리(봉인 기록 포함)는 그 아래 한 단계입니다.
`AGORA_` 밖의 변수는 건드리지 않습니다 — `PATH`·`HOME`과 벤더 자신의 인증 토큰·프록시 설정은 그대로 살아 있어야 CLI가 인증할 수 있습니다. 읽는 시점과 넘기는 시점은 분리되어 있어, 스크립트 자신은 계속 `AGORA_TIMEOUT_SECS` 등을 평범한 셸 변수로 사용합니다(테스트 주입 설정이 여전히 동작하는 이유).
**이 정화가 덮지 못하는 것은 cwd입니다 — 위 [한계 3](#신뢰-경계)을 참조하십시오.**
## R010 위임 구조
오케스트레이터는 파일을 직접 쓸 수 없습니다(R010). `agora`는 라운드마다 다수의 아티팩트 파일을 기록하므로, 라운드 실행은 `agora-runner` 에이전트에 위임합니다.
- **위임 단위는 라운드 1개 = 위임 1건**입니다. 다중 라운드를 한 위임에 묶지 않습니다 — Phase 경계가 곧 mid-step 종료 지점이 되는 것을 방지하기 위함입니다(R020).
- **파일을 쓰는 진입점은 `--start`·`--round`·`--set-stop`·`--report` 넷**이며 모두 러너가 실행합니다. `--set-stop`은 이름이 판정처럼 보이지만 `state.json`을 변경하므로 오케스트레이터가 직접 실행하지 않습니다.
- `agora-runner`는 스크립트 실행과 아티팩트 기록을 담당하고, **verdict 요약만** 오케스트레이터에 반환합니다. 리뷰어 원문, 벤더 출처, `SEALED/` 경로는 반환하지 않습니다.
- **사용자 게이트 표시와 게이트 응답 처리는 오케스트레이터 전담**입니다 — 서브에이전트는 사용자와 상호작용하지 않습니다.
- `agora-runner` 에이전트 정의 자체는 이 스킬의 범위 밖입니다. `.claude/agents/agora-runner.md`를 참조하십시오.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!