Skip to main content

truncated-diagnostic-shows-only-the-passing-half

자동 소비자(에이전트·봇·CI)에게 실패 원인을 돌려줄 때 결과 객체 전체를 직렬화하고 앞에서 N자를 자르면, 통과 블록이 먼저 직렬화될 경우 "실패했다"면서 통과 목록만 보여준다. 소비자는 우회하지 않고 같은 실패를 무한 반복한다. 트리거 - 실패 피드백/에러 리포트/재시도 프롬프트를 만들 때, `json.dumps(result)[:N]` 같은 blind prefix, 동일 실패가 3회 이상 반복, 라인 번호가 안 움직이는 반복 실패, "어려운 작업인가 결함인가" 판단이 안 될 때.

설치로 이동

소스 정보

저장소
blas1n/claude-skills
최근 소스 활동
2026년 8월 14일 06:21
감지된 SKILL.md 언어
한국어
스타
2
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
truncated-diagnostic-shows-only-the-passing-half
description
자동 소비자(에이전트·봇·CI)에게 실패 원인을 돌려줄 때 결과 객체 전체를 직렬화하고 앞에서 N자를 자르면, 통과 블록이 먼저 직렬화될 경우 "실패했다"면서 통과 목록만 보여준다. 소비자는 우회하지 않고 같은 실패를 무한 반복한다. 트리거 - 실패 피드백/에러 리포트/재시도 프롬프트를 만들 때, `json.dumps(result)[:N]` 같은 blind prefix, 동일 실패가 3회 이상 반복, 라인 번호가 안 움직이는 반복 실패, "어려운 작업인가 결함인가" 판단이 안 될 때.
# 잘린 진단은 통과한 절반만 보여준다 ## Problem - **증상**: 에이전트/봇이 같은 실패를 **바이트 단위로 동일하게** 반복한다. 겉보기엔 "모델이 못 고치는 어려운 작업"으로 읽힌다. - **근본 원인**: 실패 피드백이 **결과 객체 전체의 blind prefix** 다. 앞자리를 차지한 블록이 통과한 검사면, 메시지는 누락이 아니라 **정면으로 거짓말**을 한다. - **흔한 오해**: "정보는 result 안에 다 있다" → 있는 것과 **소비자가 받는 것**은 다르다. ### 실측 (BSVibe #751, run `010bbdd8`) 피드백이 이랬다: ```python "Verification FAILED. Details:\n" f"{json.dumps(verdict.result)[:1500]}\n" "Fix the problem and try again." ``` | | | |---|---| | `json.dumps(verdict.result)` 전체 | **13,056자** | | 앞 1500자의 내용 | 에이전트 **자기** 명령 4개 — 전부 `"passed": true` (verbose `pytest -v` 로그) | | 진짜 실패(`no-untyped-def`) 위치 | **인덱스 3516** — 잘림 | 에이전트가 매 라운드 받은 것은 문자 그대로: > **"Verification FAILED. Details: [전부 통과했음] Fix the problem and try again."** 결과: **41분, 16회, 같은 mypy 오류 3건, 같은 라인 번호.** 고칠 수가 없었다. ### 왜 하필 통과 블록이 앞에 왔나 — 이게 급소다 코드는 두 소스의 권위를 **명시적으로 구분**하고 있었다: - `derived_gate` — 레포가 선언한 게이트. **authoritative**, 판정을 내림 - `command_results` — 에이전트 자기 신고. **advisory**, 아무것도 결정 안 함 그런데 **직렬화 순서는 advisory 가 먼저**였다. 즉 리포트는 *판정을 내린 필드를 자르고 아무것도 결정하지 않는 필드를 보여준다*. 순서는 아무도 의도하지 않았고 아무도 안 봤다. ### 증폭기: `ensure_ascii` `json.dumps` 기본값 `ensure_ascii=True` 는 한글 1자를 `\uXXXX` **6자**로 부풀린다. 한국어 judge 사유 200자 = 예산 1200자. 비-ASCII 로그가 있으면 예산은 체감보다 6배 빨리 소진된다. ## Solution **리포트를 자르지 마라. 실패만으로 조립하라.** 1. 실패한 항목만 뽑아 **권위 순서**로 배치 (authoritative → … → advisory 는 맨 뒤) 2. 통과 검사는 **한 줄 요약**으로만 — 어떤 양의 통과 출력도 실패를 밀어낼 수 없다 3. 텍스트를 **텍스트로** 렌더 (JSON 직렬화 금지 → `\uXXXX` 부풀림 제거) 4. 절단은 **양끝** 보존 — mypy 는 앞, pytest 는 뒤에 답을 둔다. head-only 는 도구에 따라 답을 잃는다 5. 모든 절단이 **몇 자 버렸는지 고지** 6. **실패가 하나도 식별 안 되면 그렇게 말하라** — 통과 목록을 대신 보여주는 것이 그 거짓말이다 ```python def _clip(text: str, limit: int) -> str: """양끝 보존 + 버린 양 고지.""" if len(text) <= limit: return text template = "\n… {n} characters omitted …\n" room = limit - len(template.format(n=len(text))) if room <= 0: return text[:limit] head, tail = room * 2 // 3, room - room * 2 // 3 return text[:head] + template.format(n=len(text) - head - tail) + text[-tail:] def render_failure(result) -> str: sections = [block(c) for c in failed(result["derived_gate"]["commands"])] # authoritative 먼저 sections += [block(c) for c in failed(result["command_results"])] # advisory 나중 if not sections: return "실패했지만 어떤 검사도 실패를 보고하지 않았다 — 하네스 문제로 보인다." note = f"({passed_count}개 통과, 출력 생략.)" return _clip("\n\n".join(sections) + "\n\n" + note, BUDGET) ``` ### 테스트는 렌더러 유닛으로 부족하다 렌더러만 단언하면 **루프가 그걸 실제로 쓰는지**는 여전히 미검증이다(같은 세션에서 이 구멍으로 결함 2개가 프로덕션에 나갔다). 실제 루프를 돌려 **LLM 스텁이 받은 메시지**를 읽어라: ```python async def test_the_agent_receives_the_failure_on_its_next_turn(): # 실패를 통과 출력 3000자 뒤에 일부러 배치 — 옛 blind prefix 라면 잘린다 ... replan = llm.calls[-1]["messages"][-1] assert "MARKER_THE_AGENT_MUST_SEE" in replan["content"] assert "P" * 500 not in replan["content"] ``` 그리고 **옛 표현으로 되돌렸을 때 그 테스트가 실제로 실패하는지 확인하라.** 확인 안 하면 그 테스트가 무엇을 잡는지 아무도 모른다. ## Key Insights - **사람과 에이전트의 실패 모드가 다르다.** 사람은 안 보이면 딴 데를 뒤지거나 사람을 부른다. 에이전트는 **주어진 것만 본다** — 우회하지 않고 예산이 끝날 때까지 같은 실패를 반복한다. 그래서 "볼 수 없게 만드는 결함"은 인프라에서 **치명적**이다. - **권위와 직렬화 순서는 별개다.** 어느 필드가 판정을 내리는지 코드가 명시해도, `dict` 삽입 순서는 그것과 무관하게 정해진다. 자르는 순간 이 둘의 불일치가 결함이 된다. - **조용한 캡은 누락이 아니라 거짓 주장이다.** "FAILED" 헤드라인 아래 통과 목록을 놓으면 그 메시지는 사실과 반대되는 것을 단언한다. - 예산 숫자를 키우는 건 처방이 아니다. **순서가 처방**이고, 예산은 이미 올바른 리포트의 꼬리를 얼마나 남길지만 정한다. ## Red Flags - **같은 실패가 3회 이상 반복되는데 오류 메시지·라인 번호가 한 글자도 안 바뀐다.** → "어려운 작업"이 아니라 **피드백이 안 닿는 것**을 먼저 의심하라. 라인 번호 고정이 스모킹건이다. - 소비자가 실패 원인을 **자기 입으로 못 지목**한다("왜 실패했는지 모르겠다", "내 검사는 다 통과한다"). - 코드에 `json.dumps(...)[:N]` / `str(payload)[:N]` / `truncate(report, N)` 이 진단 경로에 있다. - 리포트에 **권위가 다른 소스가 섞여** 있는데(authoritative gate vs self-reported), 렌더링이 그 구분을 반영하지 않는다. - 비-ASCII(한글·일본어·이모지) 로그가 들어가는 경로에 `json.dumps` 기본값을 쓴다. - 소비자에게 가는 최종 문자열을 **아무도 출력해 본 적이 없다.** ## 확인 절차 (30초) 프로덕션 실제 payload 하나를 꺼내 **소비자가 받는 문자열을 그대로 찍어봐라.** ```python body = render(actual_production_result) print(body); print(len(body)) ``` 이 한 번이 이 결함을 즉시 드러낸다. 유닛 테스트 6000개가 못 잡은 것을 실물 출력 한 번이 잡는다. ## 관련 - `seam-must-assert-what-the-consumer-sees` — 생산자 쪽 추론만으로 소비자 계약을 정하는 실수 - `unit-test-supplies-what-production-withholds` — 조각을 단언하고 조립된 결과를 안 보는 실수 - `absence-measurement-validity-check` — 없음을 관측하기 전에 그것을 만드는 경로를 확인
GitHub에서 보기