init: company-haness 설계

This commit is contained in:
DongHyeonka
2026-07-23 17:49:00 +09:00
parent 57d1bab894
commit f668d6a158
962 changed files with 98989 additions and 1 deletions
+311
View File
@@ -0,0 +1,311 @@
#!/usr/bin/env python3
"""benchmark.py — 골든태스크 품질 회귀 벤치마크 (리뷰 3주차).
plain Claude vs 이 하네스를 같은 골든태스크로 실행·채점·비교한다. 개선이 증명되지 않는
role/fan-out/framework의 제거 근거를 만든다. 이 도구는 **측정 인프라**다 — 실제 비교 데이터는
두 arm으로 과제를 실행하고 record 로 점수를 적재해야 쌓인다(정직: 데이터 없으면 '미실행' 표시).
repo-level `benchmark/`(워크스페이스 비의존): golden-tasks.yaml · benchmark-rubric.yaml ·
runs.jsonl(append-only) · BENCHMARK.md(비교 리포트).
Usage:
benchmark.py list # 골든태스크 목록
benchmark.py run --task GT-01 --arm plain|harness [--execute] [--timeout 900]
# fixture+verify 가 있는 과제를 임시 복사본에서 실행·자동채점. 기본 --dry-run(미실행, 예산보호),
# --execute 를 줘야 claude CLI 를 호출하고 verify 로 객관 채점 후 runs.jsonl 에 적재한다.
benchmark.py record --task GT-01 --arm plain|harness \
--scores "first-pass-acceptance=1,tests-pass-rate=0.9,rework-count=1,tokens=8000" [--note ...]
# fixture 없는(문서/결정 등 수동채점) 과제용 — 사람이 채점한 점수를 적재.
benchmark.py compare # runs.jsonl -> BENCHMARK.md (plain vs harness delta)
"""
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
import yaml
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname(os.path.dirname(HERE))
BENCH = os.path.join(ROOT, "benchmark")
TASKS = os.path.join(BENCH, "golden-tasks.yaml")
RUBRIC = os.path.join(BENCH, "benchmark-rubric.yaml")
RUNS = os.path.join(BENCH, "runs.jsonl")
OUT = os.path.join(BENCH, "BENCHMARK.md")
def _load(path, key):
return (yaml.safe_load(open(path, encoding="utf-8")) or {}).get(key, {})
def _tasks():
return _load(TASKS, "golden-tasks")
def _rubric():
return _load(RUBRIC, "benchmark-rubric")
def _runs():
if not os.path.exists(RUNS):
return []
out = []
for line in open(RUNS, encoding="utf-8"):
line = line.strip()
if line:
try:
out.append(json.loads(line))
except json.JSONDecodeError:
pass
return out
def cmd_list():
t = _tasks()
tasks = t.get("tasks", [])
print(f"골든태스크 {len(tasks)}개 (카테고리: {', '.join(t.get('categories', []))})")
for x in tasks:
print(f" {x['id']} [{x['category']}/{x.get('difficulty','-')}] {x['prompt']}")
def cmd_record(opt):
task, arm = opt.get("task"), opt.get("arm")
valid_ids = {x["id"] for x in _tasks().get("tasks", [])}
arms = _rubric().get("arms", ["plain", "harness"])
if task not in valid_ids:
sys.stderr.write(f"unknown task {task!r} — golden-tasks.yaml 참고(list)\n")
sys.exit(2)
if arm not in arms:
sys.stderr.write(f"arm은 {arms} 중 하나여야 한다(got {arm!r})\n")
sys.exit(2)
scores = {}
for kv in (opt.get("scores") or "").split(","):
kv = kv.strip()
if "=" in kv:
k, v = kv.split("=", 1)
try:
scores[k.strip()] = float(v)
except ValueError:
scores[k.strip()] = v.strip()
rec = {"at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"task": task, "arm": arm, "scores": scores, "note": opt.get("note")}
os.makedirs(BENCH, exist_ok=True)
with open(RUNS, "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
print(f"[benchmark] recorded {task}/{arm}: {scores}")
def _mean(vals):
vals = [v for v in vals if isinstance(v, (int, float))]
return sum(vals) / len(vals) if vals else None
# ─────────────────────────────────────────────── REAL runner + automated grader (R3)
# 재리뷰 지적: benchmark.py 는 '임의 점수 recorder'였다(모델 미실행). 이제 fixture+verify 가 있는
# 과제를 실제로 실행한다 — 임시 복사본에서 claude CLI(one-shot -p)를 두 arm(plain/harness)으로
# 돌리고, verify(pytest 등)로 **객관 채점**한다. 정직: 실행은 실제 API 예산을 쓰므로 기본은
# --dry-run(플러밍만 확인, 미실행). --execute 를 줘야 CLI 를 호출한다. 위조 점수 없음.
_CLAUDE_CMD = os.environ.get("ORGOS_BENCH_CLAUDE", "claude")
def _task_by_id(tid):
for t in _tasks().get("tasks", []):
if t.get("id") == tid:
return t
return None
def _git(args, cwd):
return subprocess.run(["git"] + args, cwd=cwd, capture_output=True, text=True)
def _setup_workdir(task, arm):
"""fixture 를 임시 dir 로 복사하고 git 기준선 커밋. harness arm 은 repo .claude 를 얹는다.
(workdir, fixture_abs) 반환. fixture 없으면 (None, None)."""
fx = task.get("fixture")
if not fx:
return None, None
# fixture 경로는 benchmark/ 기준(golden-tasks.yaml 위치). 절대경로면 그대로.
fixture_abs = fx if os.path.isabs(fx) else os.path.join(BENCH, fx)
if not os.path.isdir(fixture_abs):
return None, None
work = tempfile.mkdtemp(prefix=f"bench_{task['id']}_{arm}_")
for name in os.listdir(fixture_abs):
s = os.path.join(fixture_abs, name)
d = os.path.join(work, name)
(shutil.copytree if os.path.isdir(s) else shutil.copy2)(s, d)
_git(["init", "-q"], work)
_git(["add", "-A"], work)
_git(["-c", "user.email=b@b", "-c", "user.name=b", "commit", "-qm", "baseline"], work)
if arm == "harness":
# 하네스 arm: .claude(settings/hooks/agents)를 얹어 게이트가 실제로 작동하게 한다.
shutil.copytree(os.path.join(ROOT, ".claude"), os.path.join(work, ".claude"))
return work, fixture_abs
def _grade(work, task):
"""arm 실행 후 객관 채점. verify 실행 + git diff 로 점수 산출."""
scores = {}
verify = task.get("verify")
if verify:
vr = subprocess.run(verify, cwd=work, shell=True, capture_output=True, text=True, timeout=300)
out = (vr.stdout or "") + (vr.stderr or "")
scores["first-pass-acceptance"] = 1.0 if vr.returncode == 0 else 0.0
m = re.search(r"(\d+)\s+passed(?:,\s*(\d+)\s+failed)?", out)
if m:
p = int(m.group(1)); f = int(m.group(2) or 0)
scores["tests-pass-rate"] = round(p / (p + f), 3) if (p + f) else 0.0
else:
scores["tests-pass-rate"] = 1.0 if vr.returncode == 0 else 0.0
# unnecessary-change-lines: expected-changed-files 밖의 diff 라인 수.
exp = set(task.get("expected-changed-files") or [])
ns = _git(["diff", "--numstat", "HEAD"], work).stdout
extra = 0
for line in ns.splitlines():
parts = line.split("\t")
if len(parts) == 3:
add, dele, path = parts
if path not in exp and not path.startswith(".claude/"):
extra += (int(add) if add.isdigit() else 0) + (int(dele) if dele.isdigit() else 0)
scores["unnecessary-change-lines"] = extra
return scores
def cmd_run(opt):
task = _task_by_id(opt.get("task"))
arm = opt.get("arm")
if not task:
sys.stderr.write(f"unknown task {opt.get('task')!r} (list 참고)\n"); sys.exit(2)
if arm not in _rubric().get("arms", ["plain", "harness"]):
sys.stderr.write(f"arm 은 plain|harness 여야 한다(got {arm!r})\n"); sys.exit(2)
if not task.get("fixture"):
sys.stderr.write(f"{task['id']} 은 실행 fixture 가 없다 — 수동 record 대상(run 불가)\n"); sys.exit(2)
execute = bool(opt.get("execute"))
work, fixture_abs = _setup_workdir(task, arm)
if not work:
sys.stderr.write(f"fixture 설정 실패: {task.get('fixture')}\n"); sys.exit(1)
cli = [_CLAUDE_CMD, "-p", task["prompt"], "--dangerously-skip-permissions"]
env = dict(os.environ)
env["CLAUDE_PROJECT_DIR"] = work
if arm == "harness":
env["ORGOS_WORKSPACE"] = work # 하네스 arm: workspace 를 작업 dir 로
print(f"== benchmark run: {task['id']} / {arm} ==")
print(f" workdir: {work}")
print(f" verify : {task.get('verify')}")
print(f" CLI : {' '.join(cli[:2])} \"<prompt>\" {' '.join(cli[3:])}")
if not execute:
# 기본: 플러밍만 확인(미실행). fixture/verify 가 실제로 돌아가는지 baseline 채점으로 증명.
base = _grade(work, task)
print(f" [dry-run] 미실행(예산 보호). baseline verify → first-pass-acceptance="
f"{base.get('first-pass-acceptance')} (버그 상태라 0 이어야 정상).")
print(" 실제 실행: --execute 를 주면 claude CLI 를 호출하고 자동 채점·record 한다.")
return
if not shutil.which(_CLAUDE_CMD):
sys.stderr.write(f"claude CLI('{_CLAUDE_CMD}') 미가용 — ORGOS_BENCH_CLAUDE 로 지정하세요\n"); sys.exit(1)
print(" [execute] claude CLI 호출 중… (실제 API 예산 소비)")
try:
subprocess.run(cli, cwd=work, env=env, timeout=int(opt.get("timeout", 900) or 900))
except subprocess.TimeoutExpired:
print(" [execute] 타임아웃 — 부분 결과로 채점")
scores = _grade(work, task)
rec = {"at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"task": task["id"], "arm": arm, "scores": scores, "note": "auto(run)",
"workdir": work}
os.makedirs(BENCH, exist_ok=True)
with open(RUNS, "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
print(f" [execute] auto-graded {task['id']}/{arm}: {scores}")
def cmd_compare():
rub = _rubric()
dims = rub.get("dimensions", {})
runs = _runs()
by = {"plain": {}, "harness": {}}
for r in runs:
arm = r.get("arm")
if arm not in by:
continue
for k, v in (r.get("scores") or {}).items():
by[arm].setdefault(k, []).append(v)
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
n_plain = sum(1 for r in runs if r.get("arm") == "plain")
n_harness = sum(1 for r in runs if r.get("arm") == "harness")
L = ["# 🏁 골든태스크 벤치마크 (plain Claude vs 하네스)", "",
f"생성: {ts} · 실행 표본: plain {n_plain} · harness {n_harness} "
f"(골든태스크 {len(_tasks().get('tasks', []))}개)",
"> 측정 인프라. 표본이 없으면 '미실행'으로 **정직히** 표시한다(위장 없음). "
"delta>0 = 하네스 이득(방향 보정됨).", "",
"| dimension | 방향 | weight | plain | harness | delta | 판정 |",
"|---|---|--:|--:|--:|--:|:--:|"]
wins = losses = ties = 0
composite = 0.0
for dim, spec in dims.items():
direction = spec.get("direction", "higher-better")
w = spec.get("weight", 1)
pm = _mean(by["plain"].get(dim, []))
hm = _mean(by["harness"].get(dim, []))
if pm is None or hm is None:
L.append(f"| {dim} | {direction} | {w} | "
f"{'-' if pm is None else round(pm,3)} | "
f"{'-' if hm is None else round(hm,3)} | - | ⚪ 미실행 |")
continue
raw = (hm - pm) if direction == "higher-better" else (pm - hm)
verdict = "✅ 하네스" if raw > 1e-9 else ("❌ plain" if raw < -1e-9 else " 동률")
if raw > 1e-9:
wins += 1; composite += w
elif raw < -1e-9:
losses += 1; composite -= w
else:
ties += 1
L.append(f"| {dim} | {direction} | {w} | {round(pm,3)} | {round(hm,3)} | "
f"{round(raw,3):+} | {verdict} |")
L += ["",
f"**요약**: 하네스 우세 {wins} · plain 우세 {losses} · 동률 {ties} · "
f"가중 composite {composite:+g} (양수=하네스 이득).",
"", "> 판정 규칙(rubric.decision-rule): 하네스가 카테고리에서 delta<=0이면 그 role/fan-out/"
"framework는 비용만 늘리는 것 → 제거/경량화 후보. 표본을 채워 이 표를 실증한다."]
if n_plain == 0 and n_harness == 0:
L += ["", "⚠️ 아직 실행 표본이 없다. `benchmark.py record`로 두 arm의 점수를 적재하면 "
"이 표가 실증 데이터로 채워진다(현재는 프레임만)."]
os.makedirs(BENCH, exist_ok=True)
with open(OUT, "w", encoding="utf-8") as f:
f.write("\n".join(L) + "\n")
print(f"[benchmark] compare -> {os.path.relpath(OUT, ROOT)} "
f"(plain {n_plain} · harness {n_harness} 표본)")
def main():
a = sys.argv[1:]
if not a:
sys.stderr.write(__doc__)
sys.exit(1)
cmd = a[0]
opt = {}
i = 1
while i < len(a):
if a[i].startswith("--"):
k = a[i][2:]
opt[k] = a[i + 1] if i + 1 < len(a) and not a[i + 1].startswith("--") else True
i += 2
else:
i += 1
if cmd == "list":
cmd_list()
elif cmd == "run":
cmd_run(opt)
elif cmd == "record":
cmd_record(opt)
elif cmd == "compare":
cmd_compare()
else:
sys.stderr.write(f"unknown command: {cmd}\n")
sys.exit(1)
if __name__ == "__main__":
main()