#2에서 Hook으로 에이전트의 코드를 실시간 검증하고, 에러가 나면 self-heal로 자동 수정하게 만들었다. 근데 같은 에러가 세션마다 반복된다. import 정렬을 안 하고, console.log를 남기고, 타입을 빠뜨린다. 매번 잡아주는 것도 한계가 있다.
에이전트가 실수에서 학습하게 만들었다. 에러를 기록하고, 반복되면 규칙을 자동 추가하고, 메트릭으로 효과를 측정한다.
즉시 학습 — learnings.json
post-write hook이 에러를 감지하면 .harness/learnings.json에 즉시 기록한다. 세션이 끝날 때까지 기다리지 않는다.
{
"learnings": [
{
"id": "learn-1718500000-12345",
"date": "2026-06-16",
"mistake": "organizeImports",
"rule": "빌드/린트 에러 발생 — 수정 후 검증한다 (apps/web/src/ui/input.tsx)"
}
]
}
session-init hook이 세션 시작 시 이 파일을 읽어서 에이전트 컨텍스트에 주입한다. “지난번에 이런 실수를 했으니 이번에는 조심하라”는 경고가 된다.
중복 체크 — 처음에 잘못 만들었다
처음에는 rule 전체 문자열로 중복을 체크했다. 근데 같은 organizeImports 에러인데 파일 경로가 다르면 다른 규칙으로 취급됐다.
// 이 두 개가 "다른" 규칙으로 들어감
{ "mistake": "organizeImports", "rule": "...에러... (input.tsx)" }
{ "mistake": "organizeImports", "rule": "...에러... (button.tsx)" }
organizeImports가 7개나 쌓였는데 AutoHarness가 안 돌아가는 이유였다. 중복 체크를 mistake(에러코드) 기준으로 바꿔서 해결했다.
AutoHarness — 반복 에러 → 규칙 자동 추가
같은 에러코드가 3번 이상 반복되면 harness.config.json의 codingStandards에 규칙을 자동 추가한다.
🔧 [AutoHarness] organizeImports 4회 반복 → 자동 추가됨
발생 시점이 두 군데다:
- session-init (세션 시작): learnings.json에서 빈도 집계 → config에 추가
- post-write (파일 수정 후): 에러 발생 시 즉시 체크 → 3회 이상이면 config에 추가
매핑 테이블의 함정
처음에는 에러코드를 사람이 읽을 수 있는 규칙명으로 매핑했다.
case "$CODE" in
TS2322) RULE_ID="strict-return-type"; RULE_DESC="함수 반환 타입을 반드시 명시한다" ;;
TS7006) RULE_ID="no-implicit-any"; RULE_DESC="파라미터에 타입을 반드시 명시한다" ;;
organizeImports) RULE_ID="sort-imports"; RULE_DESC="import 문을 정렬한다" ;;
lint/a11y/*) RULE_ID="a11y"; RULE_DESC="접근성 규칙을 준수한다" ;;
no-console) RULE_ID="no-console-log"; RULE_DESC="console.log를 남기지 않는다" ;;
# ... 끝이 없다
esac
biome 에러코드, eslint 에러코드, TypeScript 에러코드, 블록체인 보안 코드… 새로운 에러가 나올 때마다 매핑을 추가해야 한다. 유지보수가 불가능하다.
에러코드를 그대로 규칙 ID로 사용하는 방식으로 바꿨다.
_code_to_config_rule() {
local code="$1"
local sev="warning"
case "$code" in SWC-*) sev="error" ;; esac
printf '{"id":"%s","description":"%s","severity":"%s"}' "$code" "$code" "$sev"
}
어떤 linter, 어떤 에러코드가 와도 자동으로 수용한다. 보안 에러(SWC-*)만 severity를 error로 올리고, 나머지는 warning. 매핑 테이블 0줄.
에러코드 수집 — 삽질의 기록
에러코드를 메트릭에 기록해야 하는데, linter마다 출력 포맷이 달라서 수집이 쉽지 않았다.
시도 1: 정규식 한 방
처음에는 $CONTEXT(에러 메시지 전체)에서 정규식으로 에러코드를 추출했다.
ERROR_CODES=$(printf '%s' "$CONTEXT" | grep -oE 'TS[0-9]+|SWC-[0-9]+|unwrap|assert!')
문제: biome 에러코드(organizeImports, lint/a11y/noSvgWithoutTitle)가 안 잡힌다. 패턴이 다르니까.
시도 2: 정규식 확장
biome/eslint 패턴을 추가했다.
grep -oE 'TS[0-9]+|SWC-[0-9]+|unwrap|assert!|organizeImports|lint/[a-zA-Z/]+|no-[a-z-]+|react/[a-z-]+'
문제: 정규식이 끝없이 길어진다. 새 linter가 추가되면 또 패턴을 넣어야 한다.
시도 3: linter별 직접 파싱
각 검사 단계에서 linter 출력 포맷에 맞게 에러코드를 바로 수집하는 방식으로 바꿨다.
ECODES=""
_add_codes() { ECODES="$ECODES $*"; }
# lint 단계에서
case "$LINTER" in
biome) _add_codes $(echo "$LINT_RESULT" | awk '/━━/ && /\.(ts|tsx)/ {print $2}') ;;
eslint) _add_codes $(echo "$LINT_RESULT" | awk '/error/ {print $NF}') ;;
esac
# TypeScript 단계에서
_add_codes $(echo "$TS_RESULT" | grep -oE 'TS[0-9]+')
# 블록체인 보안 단계에서
_add_codes $(printf '%s' "$SEC" | grep -oE 'SWC-[0-9]+')
정규식 한 방이 아니라, 각 단계에서 해당 도구의 출력 포맷에 맞게 추출한다.
시도 3.5: biome 구분선이 에러코드로 들어간다
biome 출력에서 awk '/━━/ {print $2}'로 에러코드를 뽑는데, 요약 줄도 ━━를 포함한다.
apps/web/src/ui/input.tsx organizeImports ━━━━━━━━━━━━ ← 이건 OK
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ← 이건 NO
두 번째 줄에서 $2가 ━━━━━━━━━━━━━━━━━가 된다. metrics.jsonl에 구분선이 에러코드로 기록되는 참사.
파일 확장자 필터를 추가해서 해결했다.
awk '/━━/ && /\.(ts|tsx|js|jsx|css|json)/ {print $2}'
메트릭 — 하네스가 얼마나 일하는지
hook이 실행될 때마다 .harness/metrics.jsonl에 이벤트를 기록한다.
{"ts":"2026-06-16T10:48:27+09:00","hook":"post-write","event":"clean","file":"src/ui/button.tsx","codes":[]}
{"ts":"2026-06-16T10:48:38+09:00","hook":"post-write","event":"error","file":"src/ui/input.tsx","codes":["organizeImports"]}
{"ts":"2026-06-16T10:49:00+09:00","hook":"scope-guard","event":"block","file":"package.json","codes":[]}
세 가지 핵심 메트릭
| 메트릭 | 정의 | 의미 |
|---|---|---|
| 차단율 | scope-guard/scaffold-guard가 차단한 횟수 | 에이전트가 범위 밖을 얼마나 건드리려 하는지 |
| first-pass | 파일의 첫 post-write가 에러 없이 통과한 비율 | 에이전트가 처음부터 얼마나 잘 쓰는지 |
| self-heal | 에러 발생 후 같은 파일이 clean으로 전환된 비율 | 에이전트가 에러를 얼마나 잘 고치는지 |
세션 시작 시 자동 요약
session-init hook이 최근 7일 메트릭을 요약해서 에이전트에 주입한다.
📊 차단: 12회 | first-pass: 65% | 에러감지: 28회 (최근 7일)
에이전트가 이걸 보고 “최근에 에러가 많았으니 더 신중하게 작성해야겠다”고 판단할 수 있다. 실제로 메트릭 주입 후 first-pass 비율이 올라가는지는 더 많은 데이터가 필요하다.
/metrics 스킬로 상세 확인
📊 Harness Metrics (최근 7일)
─────────────────────────
scope-guard 차단: 12회
scaffold-guard 차단: 3회
post-write 에러 감지: 28회
self-heal 성공: 22/28 (79%)
first-pass 성공: 18/28 (64%)
🔥 가장 많은 에러:
organizeImports: 9회
TS2322: 5회
agent hook을 결국 빼다
Stop hook에 agent hook(adversarial code review + learnings loop)을 넣었었다. 에이전트가 응답을 끝낼 때 코드 리뷰를 자동으로 돌리고, errors.log를 learnings.json으로 변환하는 역할이었다.
두 가지 문제가 있었다.
- 에러 표시가 안 사라진다 — agent hook이
ok: false를 반환하면 Claude Code가 “Stop hook error”를 표시한다.ok: true로 바꿔도, 에이전트가 실제로 JSON{"ok": true}를 정확히 반환하지 못하는 경우가 있었다. 프롬프트에 “항상 ok: true를 반환하라”고 써도 안 될 때가 있었다. - 분석만 하고 끝나는 경우에도 2분+ 소요 —
/start로 구현 계획만 제시하고 멈추려 해도 adversarial review가 돌아간다.
결국 agent hook을 전부 빼고:
- 코드 리뷰 →
/code-review스킬로 필요할 때 수동 호출 - learnings loop → post-write에서 이미 즉시 기록하고 있어서 중복
교훈: 자동화가 항상 좋은 건 아니다. 에이전트 응답이 끝날 때마다 2분씩 리뷰를 돌리는 건 생산성을 떨어뜨린다. 필요할 때 명시적으로 호출하는 게 낫다.
다음
학습과 메트릭 시스템까지 완성했다. #4에서는 이 위에 올라가는 워크플로우 — Jira 이슈에서 시작해서 Figma 디자인 분석, 구현, 품질 게이트, MR 생성까지 자동화하는 SDLC 파이프라인을 다룬다.