@simplysm/sd-claude 14.0.91 → 14.0.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/claude/references/sd-simplysm14/README.md +7 -6
  2. package/claude/references/sd-simplysm14/apis/angular/README.md +59 -39
  3. package/claude/references/sd-simplysm14/apis/angular/controls.md +119 -186
  4. package/claude/references/sd-simplysm14/apis/angular/crud.md +70 -31
  5. package/claude/references/sd-simplysm14/apis/angular/directives.md +55 -57
  6. package/claude/references/sd-simplysm14/apis/angular/features.md +86 -105
  7. package/claude/references/sd-simplysm14/apis/angular/infra.md +48 -57
  8. package/claude/references/sd-simplysm14/apis/angular/layout.md +37 -47
  9. package/claude/references/sd-simplysm14/apis/angular/overlay.md +82 -74
  10. package/claude/references/sd-simplysm14/apis/angular/routing-appstructure.md +61 -50
  11. package/claude/references/sd-simplysm14/apis/angular/shared-data.md +74 -57
  12. package/claude/references/sd-simplysm14/apis/angular/sheet.md +63 -72
  13. package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +23 -18
  14. package/claude/references/sd-simplysm14/apis/capacitor-plugin-file-system/README.md +21 -19
  15. package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +23 -18
  16. package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +72 -32
  17. package/claude/references/sd-simplysm14/apis/core-browser/README.md +18 -18
  18. package/claude/references/sd-simplysm14/apis/core-browser/dom-element.md +29 -29
  19. package/claude/references/sd-simplysm14/apis/core-browser/indexed-db.md +41 -41
  20. package/claude/references/sd-simplysm14/apis/core-common/README.md +97 -90
  21. package/claude/references/sd-simplysm14/apis/core-common/async-runtime.md +75 -51
  22. package/claude/references/sd-simplysm14/apis/core-common/collection-ext.md +81 -0
  23. package/claude/references/sd-simplysm14/apis/core-common/errors.md +27 -29
  24. package/claude/references/sd-simplysm14/apis/core-common/obj.md +44 -45
  25. package/claude/references/sd-simplysm14/apis/core-common/serialization.md +34 -33
  26. package/claude/references/sd-simplysm14/apis/core-common/value-types.md +86 -0
  27. package/claude/references/sd-simplysm14/apis/core-node/README.md +6 -6
  28. package/claude/references/sd-simplysm14/apis/core-node/consola.md +3 -0
  29. package/claude/references/sd-simplysm14/apis/core-node/cpx.md +2 -2
  30. package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +1 -1
  31. package/claude/references/sd-simplysm14/apis/core-node/fsx.md +2 -2
  32. package/claude/references/sd-simplysm14/apis/core-node/worker.md +6 -3
  33. package/claude/references/sd-simplysm14/apis/excel/README.md +10 -10
  34. package/claude/references/sd-simplysm14/apis/excel/conditional-format.md +4 -2
  35. package/claude/references/sd-simplysm14/apis/excel/utils.md +1 -1
  36. package/claude/references/sd-simplysm14/apis/excel/workbook-worksheet.md +6 -6
  37. package/claude/references/sd-simplysm14/apis/lint/README.md +6 -32
  38. package/claude/references/sd-simplysm14/apis/lint/recommended.md +60 -0
  39. package/claude/references/sd-simplysm14/apis/lint/rules.md +17 -17
  40. package/claude/references/sd-simplysm14/apis/orm-common/README.md +15 -6
  41. package/claude/references/sd-simplysm14/apis/orm-common/db-context.md +68 -102
  42. package/claude/references/sd-simplysm14/apis/orm-common/expr.md +75 -89
  43. package/claude/references/sd-simplysm14/apis/orm-common/queryable.md +87 -99
  44. package/claude/references/sd-simplysm14/apis/orm-common/schema.md +110 -147
  45. package/claude/references/sd-simplysm14/apis/orm-common/types.md +48 -51
  46. package/claude/references/sd-simplysm14/apis/orm-node/README.md +8 -13
  47. package/claude/references/sd-simplysm14/apis/orm-node/db-conn.md +5 -5
  48. package/claude/references/sd-simplysm14/apis/sd-cli/README.md +9 -6
  49. package/claude/references/sd-simplysm14/apis/sd-cli/SdTsCompiler.md +9 -8
  50. package/claude/references/sd-simplysm14/apis/sd-cli/sd-config-types.md +23 -19
  51. package/claude/references/sd-simplysm14/apis/service-client/README.md +20 -12
  52. package/claude/references/sd-simplysm14/apis/service-client/orm.md +6 -6
  53. package/claude/references/sd-simplysm14/apis/service-client/transport.md +1 -1
  54. package/claude/references/sd-simplysm14/apis/service-common/README.md +35 -32
  55. package/claude/references/sd-simplysm14/apis/service-common/app-structure.md +23 -22
  56. package/claude/references/sd-simplysm14/apis/service-common/protocol.md +23 -23
  57. package/claude/references/sd-simplysm14/apis/service-server/README.md +51 -43
  58. package/claude/references/sd-simplysm14/apis/service-server/service-authoring.md +6 -6
  59. package/claude/references/sd-simplysm14/apis/service-server/transport-internals.md +31 -21
  60. package/claude/references/sd-simplysm14/apis/service-server/v1-legacy.md +8 -8
  61. package/claude/references/sd-simplysm14/apis/storage/README.md +55 -49
  62. package/claude/references/sd-simplysm14/manuals/client-component.md +843 -740
  63. package/claude/references/sd-simplysm14/manuals/client-crud.md +8 -0
  64. package/claude/references/sd-simplysm14/manuals/client-demo.md +6 -16
  65. package/claude/references/sd-simplysm14/manuals/client-shared-data.md +26 -0
  66. package/claude/references/sd-simplysm14/manuals/logging.md +1 -1
  67. package/claude/references/sd-simplysm14/manuals/orm.md +15 -1
  68. package/claude/rules/sd-design-rules.md +7 -0
  69. package/claude/sd-system-prompt.md +5 -8
  70. package/claude/skills/sd-debug/SKILL.md +43 -0
  71. package/claude/skills/sd-debug/workflow.js +390 -0
  72. package/claude/skills/sd-demo/SKILL.md +18 -20
  73. package/claude/skills/sd-dev/SKILL.md +127 -24
  74. package/claude/skills/sd-docs/SKILL.md +5 -3
  75. package/claude/skills/sd-docs/references/subagent-prompt.md +2 -3
  76. package/claude/skills/sd-impl/SKILL.md +18 -18
  77. package/claude/skills/sd-manual/SKILL.md +1 -0
  78. package/claude/skills/sd-review/SKILL.md +24 -18
  79. package/claude/skills/sd-review/workflow.js +324 -0
  80. package/claude/skills/sd-spec/SKILL.md +96 -679
  81. package/claude/skills/sd-spec/references/example-spec.md +28 -50
  82. package/claude/skills/sd-spec/references/format-analyze.md +232 -0
  83. package/claude/skills/sd-spec/references/format-design.md +248 -0
  84. package/claude/skills/sd-spec/workflow-analyze.js +615 -0
  85. package/claude/skills/sd-spec/workflow-design.js +667 -0
  86. package/claude/skills/sd-unpack/scripts/handlers/office_com.py +5 -1
  87. package/package.json +1 -1
  88. package/scripts/postinstall.mjs +157 -18
  89. package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +0 -68
  90. package/claude/references/sd-simplysm14/apis/core-common/array-ext.md +0 -77
  91. package/claude/references/sd-simplysm14/apis/core-common/datetime.md +0 -86
  92. package/claude/skills/sd-skill/SKILL.md +0 -245
  93. package/claude/skills/sd-skill/scripts/run_eval.py +0 -380
@@ -1,245 +0,0 @@
1
- ---
2
- name: sd-skill
3
- description: 사용자가 정의한 작업 도메인을 SKILL.md + (필요 시) scripts 묶음으로 생성·수정. Use when 새 스킬을 작성하거나 기존 스킬을 수정할 때.
4
- ---
5
-
6
- # 스킬 작성
7
-
8
- 워크플로 = 아래 "1단계 ~ 5단계" 순서로 처음부터 끝까지 읽으며 그대로 수행.
9
-
10
- 사용자에게 질문하는 시점 = 1단계(의도 정의)의 의도 합의 + 2단계(Eval 케이스 정의)의 case `input` 발화 선택. 2~5단계의 그 외 작업은 1단계 합의를 기반으로 자동 진행, 부족 발견 시 1단계로 회귀.
11
-
12
- ## 1단계. 의도 정의
13
-
14
- 다음 파악:
15
-
16
- - 스킬 목적은? (한 줄 동사형) + 커버 유즈케이스는?
17
- - 주요 사용자 입력 시나리오는? (호출 트리거 발화 형태)
18
- - 핵심 산출물은? (생성·수정 대상 파일·디렉토리 형태) — 2단계(Eval 케이스 정의)의 rubric 형식 검증 근거
19
- - 실행 스크립트 필요 / 지침만으로 충분?
20
- - 함께 포함할 참고 자료?
21
-
22
- ## 2단계. Eval 케이스 정의
23
-
24
- 채점 케이스: `evals/golden.jsonl`. 케이스 초기 워크스페이스: `evals/fixtures/<name>/`.
25
-
26
- ### 자동답변 환경
27
-
28
- Eval(자동 평가) 실행 시 사용자 응답 불가. 대상 스킬은 입력 필요 시점마다 **스스로 답변**하며 끝까지 진행 (대화 흐름·산출물 형식 검증용). 케이스 설계는 이 제약을 전제로 함.
29
-
30
- - `input` = 1턴 사용자 발화. 후속 응답 가정 안 함.
31
- - rubric(채점 항목) = 자체 답변 가능 영역만 검증: 산출물 존재 여부·형식·구조, 흐름 진행 여부, frontmatter 키 존재 여부 등. *"사용자가 X 를 골랐을 때 Y 가 나오는가"* 처럼 특정 사용자 응답 값에 의존하는 항목 금지 (자체 답변값은 매번 달라짐).
32
- - **사용자 응답 발생 자체에 의존하는 rubric 금지**: 대상 스킬이 "사용자 질문"·"OPEN 처리/대기"·"임의 채움 금지" 룰을 보유해도, 룰 발현 자체는 Eval 검증 불가. 자동답변 환경은 사용자 답변을 즉시 생성·진행 → "사용자 질문/대기" 흐름 본질적으로 발생하지 않음.
33
- - ❌ `"사용자에게 질문하거나 OPEN 처리하는 흐름이 등장하는가"` (자체 답변으로 채운 뒤 진행 = 위반 아니라 환경 정상 동작).
34
- - ❌ `"임의 채움 흔적이 없는가"` (자체 답변 자체가 "임의 채움" 으로 보임).
35
- - 대응: 케이스 재설계 또는 `input` 본문에 룰 강조 명시 (예: "모호 발견 시 보류 마커만 산출물에 박고 종료").
36
- - fixture = 자체 답변이 차단되지 않게 구성. 외부 시크릿·실시간 API 없이 진행 가능한 초기 상태로.
37
-
38
- ### 근거 제약
39
-
40
- Eval 입력 및 rubric 의 근거 = **1단계(의도 정의)에서 합의된 의도뿐**. 이전 버전 동작·대화 메모리의 옛 맥락, **타 스킬의 `evals/golden.jsonl` rubric** 인용 금지.
41
-
42
- - 합의된 의도에 없는 동작은 입력·rubric 둘 다 등장 금지.
43
- - "이전 버전과 다르게 X 하는가" 형태의 부정형(negative) rubric 금지. 현재 의도상 X 가 요구되면 `"X 하는가"` 로 직접 검증.
44
- - **타 스킬 rubric 답습 금지**: 비슷한 도메인의 스킬이라도 rubric 은 본 스킬의 1단계에서 합의된 의도에서 직접 도출. 타 스킬 rubric 을 참고용으로 열어보는 것도 금지 (복제 유혹 차단).
45
-
46
- ### 골든 케이스
47
-
48
- `evals/golden.jsonl`, 한 줄 한 케이스:
49
-
50
- ```json
51
- {"id": "case-001", "input": "/<skill-name> ...", "rubric": ["rubric 항목 1", "rubric 항목 2"], "fixture": "<fixture-dir-name>"}
52
- ```
53
-
54
- - `id`: 케이스 식별자.
55
- - `input`: 평가 대상 스킬에 전달할 사용자 입력. 1단계의 시나리오를 기반으로 LLM 이 발화 후보를 제시한 뒤 사용자가 선택.
56
- - `rubric`: PASS/FAIL 판정 항목 목록.
57
- - `fixture`: 케이스 시작 시점의 샌드박스 초기 상태 디렉토리 이름.
58
-
59
- **케이스 크기**: 한 케이스의 작업량이 단일 실행 컨텍스트를 소진할 정도로 크면 안 됨. Eval 목적 = 흐름·산출물 형식 검증 → 풀 구현·대량 분석을 요구하는 input 은 회피하고 최소 시연 수준으로 좁힘. 본질이 큰 풀 구현 스킬의 경우, input 에 평가 환경임을 알리는 단서를 포함해 rubric 검증에 불필요한 워크플로 단계를 명시적으로 건너뛰게 함.
60
-
61
- ### Rubric 작성
62
-
63
- 각 항목은 **PASS/FAIL 판정이 가능한 검증 항목**으로 작성. 추상 표현은 judge(채점 에이전트) 판단이 흔들리므로 회피.
64
-
65
- **모호 부사·형용사 회피** ("잘"·"적절히"·"합리적으로"·"명확히" → 기준이 사람마다 다름):
66
-
67
- - ❌ `"한국어 지원이 잘 되었는가?"` ("잘" 모호).
68
- - ✅ `"본문에 한국어 응답 강제 지시가 명시적 문장으로 들어갔는가?"`.
69
-
70
- **형식 검증 선호** (의미보다 형식·존재 여부 기준으로 작성하면 judge 흔들림이 줄어듦):
71
-
72
- - ❌ `"description 이 트리거 조건을 명확히 표현하는가?"`.
73
- - ✅ `"description 끝에 'Use when ~' 형식 문장이 포함되었는가?"`.
74
-
75
- **관찰 가능 산출물에 묶기** (파일 존재·특정 키 포함·특정 디렉토리 구조 등 tree/events 에서 직접 확인 가능한 사실):
76
-
77
- - ✅ `"기존 .claude/skills/review/SKILL.md 파일이 손실되지 않고 보존되었는가?"`.
78
- - ✅ `"SKILL.md frontmatter 에 name·description 키 모두 존재하는가?"`.
79
-
80
- **명세 어휘 매칭 금지**: 명세의 특정 단어를 rubric 에 그대로 박아 정확 매칭을 요구하지 말 것. LLM 응답은 동의어·다른 표현으로 동일 본질을 전달하므로, 어휘 정확 매칭은 본질과 무관한 FAIL 을 양산함. rubric 은 본질(형식·구조·존재 여부)만 검증.
81
-
82
- - ❌ `"분해 표 첫 컬럼이 '항목' 인가"` (LLM 이 'ID'·'식별자' 로 출력해도 본질 동일).
83
- - ✅ `"분해 표가 마크다운 표 형식으로 출력되고 컬럼 6개 모두 존재하는가"`.
84
-
85
- **도구명 매칭 금지**: "events(에이전트 이벤트 시퀀스)에 특정 도구(Glob/Grep/Read 등) 호출이 있는가" 형태의 rubric 은 그 도구 사용 자체가 본질일 때만 사용. 본질이 "탐색·조사·읽기" 등 행위라면 동등한 효과를 내는 다른 도구(Bash 의 ls/find/dir/cat 등)도 PASS 로 인정.
86
-
87
- - ❌ `"events 에 Glob 또는 Grep 호출이 1회 이상 있는가"` (Bash ls/find 로 동등 효과인데 FAIL).
88
- - ✅ `"events 에 코드베이스 탐색 흔적(Glob·Grep 호출 또는 Bash 의 ls/find/dir 등 동등 명령) 이 1회 이상 있는가"`.
89
-
90
- ### Fixtures
91
-
92
- `evals/fixtures/<name>/` 디렉토리는 케이스 시작 시점의 샌드박스 초기 상태. 케이스 실행 시 샌드박스로 통째로 복사됨.
93
-
94
- - **빈 워크스페이스**: 디렉토리만 둠 (`.gitkeep` 등으로 자리 표시).
95
- - **기존 스킬 수정 케이스**: 그 스킬의 SKILL.md 및 관련 파일을 미리 배치.
96
-
97
- 예: `with-existing-review/.claude/skills/review/SKILL.md` — 케이스 시작 시 review 스킬이 이미 존재하는 상태.
98
-
99
- ### 케이스 커버리지
100
-
101
- 골든 셋이 단순 PASS 외에 다음 분기를 커버하면 회귀 감지가 강화됨:
102
-
103
- - 신규 작성 케이스와 기존 수정 케이스 각각.
104
- - 워크플로 주요 분기점 (예: 스크립트 필요/불필요, 참조 파일 분리 필요/불필요).
105
- - 과거 실패 패턴 — FAIL 케이스의 reason(실패 사유)을 분석한 뒤 재발 방지용으로 추가.
106
-
107
- ## 3단계. 스킬 작성
108
-
109
- 간결·명확한 SKILL.md 와 별도 참고 파일(필요 시), 유틸리티 스크립트(필요 시)로 구성.
110
-
111
- ### 디렉토리 구조
112
-
113
- ```
114
- .claude/
115
- └── skills/
116
- └── <skill-name>/
117
- ├── SKILL.md # 스킬 본문 (필수)
118
- ├── evals/ # Eval 정의 (필수)
119
- │ ├── golden.jsonl # 케이스 목록
120
- │ └── fixtures/ # 케이스별 초기 워크스페이스
121
- │ └── <name>/
122
- ├── references/ # 상세 문서 (선택)
123
- │ └── *.md
124
- └── scripts/ # 유틸리티 (선택)
125
- └── *.py
126
- ```
127
-
128
- ### SKILL.md 템플릿
129
-
130
- frontmatter:
131
-
132
- ```markdown
133
- ---
134
- name: skill-name
135
- description: 기능 설명. Use when [활용상황]
136
- ---
137
- ```
138
-
139
- 본문은 다음 "본문 작성 원칙" 따름.
140
-
141
- ### description (frontmatter 의 description 필드)
142
-
143
- 에이전트의 라우팅 진입점. 에이전트가 description 을 보고 사용자 요청에 맞는 스킬을 호출함.
144
-
145
- **전달 정보**:
146
-
147
- - 이 스킬의 목적.
148
- - 트리거 맥락 (언제·왜).
149
- - 타 스킬과 구분 단서.
150
-
151
- **형식**:
152
-
153
- - 최대 200자, 한 줄로 작성.
154
- - 3인칭으로 작성.
155
- - 첫 문장: 입력에서 산출물(또는 효과)로의 변환을 기술. 내부 처리 단계 기술 금지.
156
- - 두 번째 문장: "Use when [활용상황]" 형식.
157
-
158
- **금지**: 내부 단계·알고리즘·사용 도구·로직 흐름은 SKILL.md 본문 워크플로의 몫. description 은 외부에서 관찰 가능한 경계(입력·산출물·트리거)만 노출.
159
-
160
- ### 본문 작성 원칙
161
-
162
- YAGNI(You Aren't Gonna Need It) 원칙 — 2단계 케이스가 통과되는 최소 본문만 작성. 케이스가 검증하지 않는 절·예시·옵션 추가 금지.
163
-
164
- - **워크플로 단위 어휘 통일**: "사이클"(반복형) 또는 "단계"(순차형) 중 하나를 선택해 본문 전체에서 일관 사용.
165
- - **본문 어휘 일관**: 같은 개념은 같은 단어로 표기. 동의어 혼용 금지.
166
- - **스스로 검증 게이트**(해당 시): 출력 직전 자문 항목을 본문에 명시하여 스킬 고유의 형식·근거·단순화 차단을 점검.
167
- - **모범 예시 인용**(해당 시): 형식을 따라야 할 산출물이 있으면 `references/example-X.md` 에 보관하고 본문에서 한 줄로 참조.
168
- - **도구명 박기 금지**: 본문에 Glob·Grep 등 도구명을 직접 박지 않음. "코드베이스 탐색"·"파일 읽기" 등 동등한 행위 표현으로 작성 (2단계 rubric 의 "도구명 매칭 금지" 와 일관).
169
-
170
- ### 스크립트 추가 기준
171
-
172
- 다음 조건에 해당하면 유틸리티 스크립트 추가:
173
-
174
- - 동작이 결정론적(deterministic)인 경우 (validation·formatting 등).
175
- - 코드 생성 결과가 매번 동일한 경우.
176
- - 에러를 명시적으로 처리해야 하는 경우.
177
-
178
- 스크립트는 토큰 절약과 안정성 개선에 기여.
179
-
180
- **작성 원칙**:
181
-
182
- - Python(`.py`) 으로 작성.
183
- - 내부 에러 처리 금지. 에러는 즉시 throw.
184
-
185
- ### 파일 분리 기준
186
-
187
- 다음 조건에 해당하면 별도 파일로 분리:
188
-
189
- - SKILL.md 분량이 에이전트가 한 자리에서 워크플로 흐름을 인식하기 어려울 만큼 누적된 경우.
190
- - 명백히 다른 도메인을 다루는 경우.
191
- - 거의 사용되지 않는 고급 기능인 경우.
192
-
193
- **참조 깊이 한 단계 제한**: SKILL.md 에서 `references/X.md` 까지만 참조 허용. references 파일 안에서 또 다른 파일을 참조하는 것 금지.
194
-
195
- ## 4단계. Eval 실행
196
-
197
- ### 명령
198
-
199
- `python .claude/skills/sd-skill/scripts/run_eval.py <대상-스킬-이름>`.
200
-
201
- 대상 스킬에 `evals/golden.jsonl` 과 `evals/fixtures/<fixture-name>/` 가 모두 존재해야 함.
202
-
203
- ### 동작
204
-
205
- 케이스마다 다음 순서로 실행:
206
-
207
- 1. 격리 작업 공간 준비 (`.claude/` 복사 후 fixture 오버레이).
208
- 2. 대상 스킬 실행. `EVAL_MODE_PREFIX`(자동 평가 모드 안내 문구)가 사용자 입력 앞에 붙어, 대상 스킬이 입력 필요 시점마다 스스로 답변하며 끝까지 진행하도록 지시. 자체 답변은 사용자의 명시적 발언과 동등하게 취급됨 (다이얼로그 기반 스킬도 평가 가능. 단, 자체 답변이므로 흐름·형식 검증용으로 한정).
209
- 3. 에이전트 동작 기록 및 종료 시점의 파일 트리 수집.
210
- 4. 별도의 평가 에이전트가 rubric 항목별로 PASS/FAIL 을 채점하고, 모든 항목이 PASS 일 때 케이스 PASS 로 판정.
211
-
212
- ### 출력 구조
213
-
214
- stdout 으로 summary JSON 출력. 포함 필드는 다음과 같음.
215
-
216
- - `run_id`, `results_dir`.
217
- - `summary`: total / pass / fail / error 카운트.
218
- - `cases[]`: 케이스별 verdict(판정 결과)와 결과 디렉토리 경로.
219
-
220
- 각 케이스 결과 파일 (`results_dir/cases/<id>/`):
221
-
222
- - `judge_output.json` — rubric 항목별 PASS/FAIL 판정과 reason(사유).
223
- - `events.json` — 에이전트 이벤트 시퀀스.
224
- - `tree.json` — 샌드박스 종료 시점의 파일 트리.
225
-
226
- ## 5단계. 개선 및 리뷰
227
-
228
- ### FAIL 처리
229
-
230
- - 보고: 전체 PASS/FAIL 카운트와 FAIL 케이스 목록.
231
- - FAIL reason(실패 사유) 분석:
232
- - 결과 파일(`judge_output.json`·`events.json`·`tree.json`) 읽기.
233
- - 스킬 본문과 Eval rubric 중 어느 쪽 문제인지 판단:
234
- - **스킬 측 문제**: 본문 명세대로 동작했는데도 FAIL → 명세가 비어있거나 모호함 → 본문 보강.
235
- - **Eval 측 문제**: 본문 명세를 만족했는데 rubric 이 본질 외 요소(어휘 매칭·도구명 매칭 등)로 FAIL → rubric 수정 (2단계 rubric 안티패턴 참조).
236
- - **모호한 경우**: 사용자에게 질문.
237
- - 수정 후 같은 골든 셋 전체로 4단계 재실행.
238
- - **수렴 한도**: 같은 케이스가 3회 연속 FAIL 시 진행 중단하고 사용자에게 보고 (무한 루프 방지).
239
- - 새로운 실패 패턴은 골든 셋에 케이스로 추가.
240
-
241
- ### 리뷰
242
-
243
- 전 케이스 PASS 후 sd-review 호출. 적용 룰: 행동 규칙과 3단계의 "본문 작성 원칙"·"파일 분리 기준".
244
-
245
- 리뷰 결과로 수정 발생 시 4단계 재실행.
@@ -1,380 +0,0 @@
1
- """sd-skill eval harness.
2
-
3
- 지정된 대상 스킬을 평가한다. 각 케이스를 격리된 샌드박스에서 실행하고,
4
- MCP submit_verdict 도구로 채점한 결과를 디스크에 저장한 뒤 stdout으로 summary JSON을 출력한다.
5
-
6
- Usage: python run_eval.py <target-skill-name>
7
- """
8
- from __future__ import annotations
9
-
10
- import asyncio
11
- import json
12
- import os
13
- import shutil
14
- import sys
15
- import time
16
- import traceback
17
- from datetime import datetime
18
- from pathlib import Path
19
- from typing import Annotated, Any
20
-
21
- EXCLUDED_DIRS = frozenset({"node_modules", ".git", "dist", ".cache", "__pycache__"})
22
-
23
-
24
- def _ensure_pip(import_name: str, pip_name: str | None = None) -> None:
25
- import importlib
26
- try:
27
- importlib.import_module(import_name)
28
- except ImportError:
29
- import subprocess
30
- subprocess.check_call(
31
- [sys.executable, "-m", "pip", "install", pip_name or import_name]
32
- )
33
-
34
-
35
- _ensure_pip("claude_agent_sdk", "claude-agent-sdk")
36
-
37
- from claude_agent_sdk import (
38
- AssistantMessage,
39
- ClaudeAgentOptions,
40
- ResultMessage,
41
- TextBlock,
42
- ToolUseBlock,
43
- create_sdk_mcp_server,
44
- query,
45
- tool,
46
- )
47
-
48
- sys.path.insert(0, ".claude/scripts")
49
- from sd_paths import resolve_tmp_base
50
-
51
- SCRIPT_DIR = Path(__file__).resolve().parent
52
- SKILLS_DIR = SCRIPT_DIR.parent.parent # .claude/skills/
53
- PROJECT_ROOT = SKILLS_DIR.parent.parent # repo root
54
-
55
-
56
- def load_eval_root(target_skill_name: str) -> Path:
57
- return resolve_tmp_base() / "evals" / target_skill_name
58
-
59
-
60
- def sweep_stale(runs_dir: Path, max_age_hours: int = 24) -> None:
61
- if not runs_dir.exists():
62
- return
63
- cutoff = time.time() - max_age_hours * 3600
64
- for d in runs_dir.iterdir():
65
- try:
66
- mtime = d.stat().st_mtime
67
- except OSError as e:
68
- sys.stderr.write(f"[sweep_stale] stat failed: {d}: {e}\n")
69
- continue
70
- if mtime < cutoff:
71
- shutil.rmtree(d, ignore_errors=True)
72
-
73
-
74
- def serialize_block(block: Any) -> dict:
75
- if isinstance(block, ToolUseBlock):
76
- return {"type": "tool_use", "name": block.name, "input": block.input}
77
- if isinstance(block, TextBlock):
78
- return {"type": "text", "text": block.text}
79
- return {"type": type(block).__name__}
80
-
81
-
82
- def serialize_message(msg: Any) -> dict:
83
- if isinstance(msg, AssistantMessage):
84
- return {"type": "assistant", "content": [serialize_block(b) for b in msg.content]}
85
- if isinstance(msg, ResultMessage):
86
- return {
87
- "type": "result",
88
- "subtype": getattr(msg, "subtype", None),
89
- "duration_ms": getattr(msg, "duration_ms", None),
90
- }
91
- return {"type": type(msg).__name__}
92
-
93
-
94
- def walk_tree(root: Path, exclude_skill_names: set[str], max_file_bytes: int = 20000) -> dict:
95
- files: dict[str, str] = {}
96
- for dirpath, dirnames, filenames in os.walk(root):
97
- rel_dir = Path(dirpath).relative_to(root).as_posix()
98
- rel_parts = rel_dir.split("/") if rel_dir != "." else []
99
- # prune .claude/skills/<excluded>/ subtrees
100
- if len(rel_parts) == 2 and rel_parts[0] == ".claude" and rel_parts[1] == "skills":
101
- dirnames[:] = [d for d in dirnames if d not in exclude_skill_names]
102
- # prune standard excluded dirs (node_modules, .git, etc.)
103
- dirnames[:] = [d for d in dirnames if d not in EXCLUDED_DIRS]
104
-
105
- for fname in filenames:
106
- p = Path(dirpath) / fname
107
- rel = p.relative_to(root).as_posix()
108
- try:
109
- content = p.read_text(encoding="utf-8")
110
- if len(content) > max_file_bytes:
111
- content = content[:max_file_bytes] + f"\n... <truncated, {p.stat().st_size} bytes total>"
112
- files[rel] = content
113
- except (UnicodeDecodeError, OSError):
114
- try:
115
- size = p.stat().st_size
116
- except OSError:
117
- size = -1
118
- files[rel] = f"<binary or unreadable, {size} bytes>"
119
- return files
120
-
121
-
122
- def copy_dot_claude(src_dot_claude: Path, dst_dot_claude: Path) -> None:
123
- """Copy .claude/ to sandbox, excluding each skill's evals/ subfolder."""
124
- def _ignore(dir_path: str, names: list[str]) -> list[str]:
125
- ignored = {"__pycache__"}
126
- d = Path(dir_path)
127
- try:
128
- rel = d.relative_to(src_dot_claude).as_posix()
129
- except ValueError:
130
- return list(ignored & set(names))
131
- parts = rel.split("/") if rel != "." else []
132
- # .claude/skills/<name>/ → ignore evals
133
- if len(parts) == 2 and parts[0] == "skills":
134
- ignored.add("evals")
135
- return [n for n in names if n in ignored]
136
-
137
- shutil.copytree(src_dot_claude, dst_dot_claude, ignore=_ignore, dirs_exist_ok=True)
138
-
139
-
140
- def merge_overlay(src: Path, dst: Path) -> None:
141
- """Copy src tree on top of dst, overwriting files where they collide."""
142
- for p in src.rglob("*"):
143
- rel = p.relative_to(src)
144
- target = dst / rel
145
- if p.is_dir():
146
- target.mkdir(parents=True, exist_ok=True)
147
- else:
148
- target.parent.mkdir(parents=True, exist_ok=True)
149
- shutil.copy2(p, target)
150
-
151
-
152
- EVAL_MODE_PREFIX = """<eval-mode>
153
- - 사용자 응답을 직접 받을 수 없습니다.
154
- - 사용자 입력이 필요한 시점이 오면 합리적인 답변을 자체 생성해서 자동 적용하고 진행하세요.
155
- - 자체 생성한 답변은 **사용자가 직접 명시한 발언과 동등**하게 취급합니다. 결정 근거(사용자 답변)로 그대로 사용하고, 같은 사안을 사용자에게 다시 묻지 마세요.
156
- - 자동 답변한 내용은 텍스트 출력에 명시하세요 (어떤 시점에 어떻게 답변했는지).
157
- - 워크플로 끝까지 완수 후 종료하세요.
158
- </eval-mode>
159
-
160
- """
161
-
162
-
163
- async def run_target(case_input: str, sandbox: Path) -> list[dict]:
164
- events: list[dict] = []
165
- async for msg in query(
166
- prompt=EVAL_MODE_PREFIX + case_input,
167
- options=ClaudeAgentOptions(
168
- cwd=str(sandbox),
169
- permission_mode="bypassPermissions",
170
- disallowed_tools=["WebFetch", "WebSearch"],
171
- ),
172
- ):
173
- events.append(serialize_message(msg))
174
- return events
175
-
176
-
177
- JUDGE_TOOL_SCHEMA = {
178
- "type": "object",
179
- "properties": {
180
- "verdict": {"type": "string", "enum": ["PASS", "FAIL"]},
181
- "items": {
182
- "type": "array",
183
- "items": {
184
- "type": "object",
185
- "properties": {
186
- "check": {"type": "string"},
187
- "result": {"type": "string", "enum": ["PASS", "FAIL"]},
188
- "reason": {"type": "string"},
189
- },
190
- "required": ["check", "result"],
191
- },
192
- },
193
- },
194
- "required": ["verdict", "items"],
195
- }
196
-
197
-
198
- def build_judge_prompt(case: dict, events: list[dict], tree: dict) -> str:
199
- rubric_lines = "\n".join(f"{i + 1}. {r}" for i, r in enumerate(case["rubric"]))
200
- events_str = json.dumps(events, ensure_ascii=False, indent=2)
201
- tree_str = json.dumps(tree, ensure_ascii=False, indent=2)
202
- return (
203
- "당신은 엄격한 평가자입니다.\n\n"
204
- "[케이스 입력]\n"
205
- f"{case['input']}\n\n"
206
- "[에이전트가 일으킨 이벤트]\n"
207
- f"{events_str}\n\n"
208
- "[샌드박스 종료 시 파일트리]\n"
209
- f"{tree_str}\n\n"
210
- "[Rubric 항목]\n"
211
- f"{rubric_lines}\n\n"
212
- "각 rubric 항목을 PASS 또는 FAIL 로 판정합니다.\n"
213
- "- 모든 항목 PASS → 케이스 verdict = PASS\n"
214
- "- 한 항목이라도 FAIL → 케이스 verdict = FAIL\n"
215
- "- FAIL 항목에는 짧은 reason 을 적습니다 (PASS는 reason 생략).\n\n"
216
- "submit_verdict 호출 시 'items' 배열의 각 'check' 필드에는 위 [Rubric 항목] 의 원문 문장을 "
217
- "번호 없이 그대로 복사해 넣어야 합니다. 요약·축약·번호화 금지.\n\n"
218
- "submit_verdict 도구를 정확히 한 번 호출해 결과를 제출하세요. 다른 텍스트 출력 금지."
219
- )
220
-
221
-
222
- async def run_judge(case: dict, events: list[dict], tree: dict) -> dict:
223
- captured: dict = {}
224
-
225
- @tool(
226
- "submit_verdict",
227
- "Submit final verdict after evaluating each rubric item.",
228
- JUDGE_TOOL_SCHEMA,
229
- )
230
- async def submit_verdict(args):
231
- captured.update(args)
232
- return {"content": [{"type": "text", "text": "ok"}]}
233
-
234
- server = create_sdk_mcp_server(name="judge", tools=[submit_verdict])
235
-
236
- async for _ in query(
237
- prompt=build_judge_prompt(case, events, tree),
238
- options=ClaudeAgentOptions(
239
- mcp_servers={"judge": server},
240
- allowed_tools=["mcp__judge__submit_verdict"],
241
- permission_mode="bypassPermissions",
242
- max_turns=5,
243
- ),
244
- ):
245
- pass
246
-
247
- return captured
248
-
249
-
250
- async def run_case(
251
- case: dict,
252
- run_id: str,
253
- eval_root: Path,
254
- fixtures_dir: Path,
255
- ) -> dict:
256
- case_id = case["id"]
257
- sandbox = eval_root / "runs" / run_id / case_id
258
- case_results = eval_root / "results" / run_id / "cases" / case_id
259
- case_results.mkdir(parents=True, exist_ok=True)
260
-
261
- fixture_dir = fixtures_dir / case["fixture"]
262
- src_dot_claude = PROJECT_ROOT / ".claude"
263
- src_skills_dir = src_dot_claude / "skills"
264
- fixture_skills_dir = fixture_dir / ".claude" / "skills"
265
-
266
- pre_existing_skills = {d.name for d in src_skills_dir.iterdir() if d.is_dir()}
267
- fixture_skills = (
268
- {d.name for d in fixture_skills_dir.iterdir() if d.is_dir()}
269
- if fixture_skills_dir.exists()
270
- else set()
271
- )
272
- exclude_skill_names = pre_existing_skills - fixture_skills
273
-
274
- try:
275
- if sandbox.exists():
276
- shutil.rmtree(sandbox, ignore_errors=True)
277
- sandbox.mkdir(parents=True, exist_ok=True)
278
-
279
- copy_dot_claude(src_dot_claude, sandbox / ".claude")
280
- merge_overlay(fixture_dir, sandbox)
281
-
282
- events = await run_target(case["input"], sandbox)
283
- tree = walk_tree(sandbox, exclude_skill_names=exclude_skill_names)
284
-
285
- (case_results / "events.json").write_text(
286
- json.dumps(events, ensure_ascii=False, indent=2), encoding="utf-8"
287
- )
288
- (case_results / "tree.json").write_text(
289
- json.dumps(tree, ensure_ascii=False, indent=2), encoding="utf-8"
290
- )
291
-
292
- verdict_data = await run_judge(case, events, tree)
293
- (case_results / "judge_output.json").write_text(
294
- json.dumps(verdict_data, ensure_ascii=False, indent=2), encoding="utf-8"
295
- )
296
-
297
- verdict = verdict_data.get("verdict", "ERROR")
298
-
299
- return {
300
- "id": case_id,
301
- "verdict": verdict,
302
- "dir": str(case_results),
303
- }
304
- except Exception as e:
305
- (case_results / "error.txt").write_text(
306
- f"{type(e).__name__}: {e}\n\n{traceback.format_exc()}",
307
- encoding="utf-8",
308
- )
309
- return {
310
- "id": case_id,
311
- "verdict": "ERROR",
312
- "error": f"{type(e).__name__}: {e}",
313
- "dir": str(case_results),
314
- }
315
- finally:
316
- shutil.rmtree(sandbox, ignore_errors=True)
317
-
318
-
319
- def load_cases(golden_path: Path) -> list[dict]:
320
- cases = []
321
- for line in golden_path.read_text(encoding="utf-8").splitlines():
322
- line = line.strip()
323
- if line:
324
- cases.append(json.loads(line))
325
- return cases
326
-
327
-
328
- async def main() -> None:
329
- if len(sys.argv) < 2:
330
- sys.stderr.write("Usage: python run_eval.py <target-skill-name>\n")
331
- sys.exit(2)
332
- target_skill_name = sys.argv[1]
333
- target_skill_dir = SKILLS_DIR / target_skill_name
334
- if not target_skill_dir.is_dir():
335
- sys.stderr.write(f"target skill not found: {target_skill_dir}\n")
336
- sys.exit(2)
337
-
338
- target_evals_dir = target_skill_dir / "evals"
339
- fixtures_dir = target_evals_dir / "fixtures"
340
- golden_path = target_evals_dir / "golden.jsonl"
341
-
342
- eval_root = load_eval_root(target_skill_name)
343
- runs_dir = eval_root / "runs"
344
- runs_dir.mkdir(parents=True, exist_ok=True)
345
- sweep_stale(runs_dir)
346
-
347
- run_id = datetime.now().strftime("%Y%m%d-%H%M%S")
348
- results_run_dir = eval_root / "results" / run_id
349
- results_run_dir.mkdir(parents=True, exist_ok=True)
350
-
351
- cases = load_cases(golden_path)
352
- case_results = await asyncio.gather(
353
- *(run_case(c, run_id, eval_root, fixtures_dir) for c in cases)
354
- )
355
-
356
- pass_count = sum(1 for r in case_results if r["verdict"] == "PASS")
357
- fail_count = sum(1 for r in case_results if r["verdict"] == "FAIL")
358
- error_count = sum(1 for r in case_results if r["verdict"] == "ERROR")
359
-
360
- summary = {
361
- "run_id": run_id,
362
- "results_dir": str(results_run_dir),
363
- "summary": {
364
- "total": len(case_results),
365
- "pass": pass_count,
366
- "fail": fail_count,
367
- "error": error_count,
368
- },
369
- "cases": case_results,
370
- }
371
-
372
- (results_run_dir / "summary.json").write_text(
373
- json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
374
- )
375
-
376
- sys.stdout.write(json.dumps(summary, ensure_ascii=False, indent=2))
377
-
378
-
379
- if __name__ == "__main__":
380
- asyncio.run(main())