moai-adk 0.4.7__py3-none-any.whl → 0.4.10__py3-none-any.whl
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.
Potentially problematic release.
This version of moai-adk might be problematic. Click here for more details.
- moai_adk/templates/.claude/agents/alfred/cc-manager.md +188 -954
- moai_adk/templates/.claude/agents/alfred/debug-helper.md +4 -5
- moai_adk/templates/.claude/agents/alfred/doc-syncer.md +2 -2
- moai_adk/templates/.claude/agents/alfred/git-manager.md +2 -2
- moai_adk/templates/.claude/agents/alfred/implementation-planner.md +3 -3
- moai_adk/templates/.claude/agents/alfred/project-manager.md +6 -6
- moai_adk/templates/.claude/agents/alfred/quality-gate.md +4 -6
- moai_adk/templates/.claude/agents/alfred/skill-factory.md +36 -36
- moai_adk/templates/.claude/agents/alfred/spec-builder.md +2 -2
- moai_adk/templates/.claude/agents/alfred/tag-agent.md +2 -2
- moai_adk/templates/.claude/agents/alfred/tdd-implementer.md +3 -3
- moai_adk/templates/.claude/agents/alfred/trust-checker.md +5 -5
- moai_adk/templates/.claude/commands/alfred/0-project.md +98 -42
- moai_adk/templates/.claude/commands/alfred/1-plan.md +7 -7
- moai_adk/templates/.claude/commands/alfred/2-run.md +36 -36
- moai_adk/templates/.claude/commands/alfred/3-sync.md +10 -10
- moai_adk/templates/.claude/hooks/alfred/HOOK_SCHEMA_VALIDATION.md +313 -0
- moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +14 -1
- moai_adk/templates/.claude/hooks/alfred/core/__init__.py +117 -33
- moai_adk/templates/.claude/hooks/alfred/handlers/session.py +8 -9
- moai_adk/templates/.claude/hooks/alfred/handlers/tool.py +6 -6
- moai_adk/templates/.claude/hooks/alfred/handlers/user.py +5 -4
- moai_adk/templates/.claude/hooks/alfred/test_hook_output.py +175 -0
- moai_adk/templates/.claude/output-styles/alfred/agentic-coding.md +1 -1
- moai_adk/templates/.claude/output-styles/alfred/moai-adk-learning.md +1 -1
- moai_adk/templates/.claude/output-styles/alfred/study-with-alfred.md +1 -1
- moai_adk/templates/.claude/settings.json +42 -2
- moai_adk/templates/.claude/skills/moai-alfred-interactive-questions/SKILL.md +480 -0
- moai_adk/templates/.claude/skills/moai-alfred-interactive-questions/examples.md +257 -0
- moai_adk/templates/.claude/skills/moai-alfred-interactive-questions/reference.md +120 -0
- moai_adk/templates/.claude/skills/moai-cc-agents/SKILL.md +249 -0
- moai_adk/templates/.claude/skills/moai-cc-agents/templates/agent-template.md +32 -0
- moai_adk/templates/.claude/skills/moai-cc-claude-md/SKILL.md +278 -0
- moai_adk/templates/.claude/skills/moai-cc-claude-md/templates/CLAUDE-template.md +26 -0
- moai_adk/templates/.claude/skills/moai-cc-commands/SKILL.md +287 -0
- moai_adk/templates/.claude/skills/moai-cc-commands/templates/command-template.md +21 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/SKILL.md +232 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/pre-bash-check.sh +19 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/preserve-permissions.sh +19 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/validate-bash-command.py +24 -0
- moai_adk/templates/.claude/skills/moai-cc-mcp-plugins/SKILL.md +179 -0
- moai_adk/templates/.claude/skills/moai-cc-mcp-plugins/templates/settings-mcp-template.json +39 -0
- moai_adk/templates/.claude/skills/moai-cc-memory/SKILL.md +296 -0
- moai_adk/templates/.claude/skills/moai-cc-memory/templates/session-summary-template.md +18 -0
- moai_adk/templates/.claude/skills/moai-cc-settings/SKILL.md +243 -0
- moai_adk/templates/.claude/skills/moai-cc-settings/templates/settings-complete-template.json +30 -0
- moai_adk/templates/.claude/skills/moai-cc-skills/SKILL.md +271 -0
- moai_adk/templates/.claude/skills/moai-cc-skills/templates/SKILL-template.md +15 -0
- moai_adk/templates/.claude/skills/moai-skill-factory/EXAMPLES.md +261 -35
- moai_adk/templates/.claude/skills/moai-skill-factory/INTERACTIVE-DISCOVERY.md +15 -15
- moai_adk/templates/.claude/skills/moai-skill-factory/PARALLEL-ANALYSIS-REPORT.md +429 -0
- moai_adk/templates/.claude/skills/moai-skill-factory/PYTHON-VERSION-MATRIX.md +391 -0
- moai_adk/templates/.claude/skills/moai-skill-factory/SKILL-FACTORY-WORKFLOW.md +431 -0
- moai_adk/templates/.claude/skills/moai-skill-factory/SKILL.md +33 -33
- moai_adk/templates/.claude/skills/moai-skill-factory/STEP-BY-STEP-GUIDE.md +466 -0
- moai_adk/templates/.claude/skills/moai-skill-factory/reference.md +438 -42
- moai_adk/templates/.claude/skills/moai-spec-authoring/README.md +129 -0
- moai_adk/templates/.claude/skills/moai-spec-authoring/SKILL.md +1300 -0
- moai_adk/templates/.claude/skills/moai-spec-authoring/examples/validate-spec.sh +161 -0
- moai_adk/templates/CLAUDE.md +111 -92
- {moai_adk-0.4.7.dist-info → moai_adk-0.4.10.dist-info}/METADATA +542 -150
- {moai_adk-0.4.7.dist-info → moai_adk-0.4.10.dist-info}/RECORD +65 -73
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/SKILL.md +0 -113
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/examples.md +0 -29
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/reference.md +0 -28
- moai_adk/templates/.claude/skills/moai-alfred-debugger-pro/SKILL.md +0 -113
- moai_adk/templates/.claude/skills/moai-alfred-debugger-pro/examples.md +0 -29
- moai_adk/templates/.claude/skills/moai-alfred-debugger-pro/reference.md +0 -28
- moai_adk/templates/.claude/skills/moai-alfred-performance-optimizer/SKILL.md +0 -113
- moai_adk/templates/.claude/skills/moai-alfred-performance-optimizer/examples.md +0 -29
- moai_adk/templates/.claude/skills/moai-alfred-performance-optimizer/reference.md +0 -28
- moai_adk/templates/.claude/skills/moai-alfred-refactoring-coach/SKILL.md +0 -113
- moai_adk/templates/.claude/skills/moai-alfred-refactoring-coach/examples.md +0 -29
- moai_adk/templates/.claude/skills/moai-alfred-refactoring-coach/reference.md +0 -28
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/SKILL.md +0 -635
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/examples.md +0 -992
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/reference.md +0 -801
- moai_adk/templates/.claude/skills/moai-claude-code/SKILL.md +0 -121
- moai_adk/templates/.claude/skills/moai-claude-code/examples.md +0 -29
- moai_adk/templates/.claude/skills/moai-claude-code/reference.md +0 -28
- moai_adk/templates/.claude/skills/moai-claude-code/templates/agent-full.md +0 -332
- moai_adk/templates/.claude/skills/moai-claude-code/templates/command-full.md +0 -384
- moai_adk/templates/.claude/skills/moai-claude-code/templates/plugin-full.json +0 -363
- moai_adk/templates/.claude/skills/moai-claude-code/templates/settings-full.json +0 -595
- moai_adk/templates/.claude/skills/moai-claude-code/templates/skill-full.md +0 -496
- moai_adk/templates/.claude/skills/moai-lang-clojure/SKILL.md +0 -123
- moai_adk/templates/.claude/skills/moai-lang-clojure/examples.md +0 -29
- moai_adk/templates/.claude/skills/moai-lang-clojure/reference.md +0 -30
- moai_adk/templates/.claude/skills/moai-lang-elixir/SKILL.md +0 -124
- moai_adk/templates/.claude/skills/moai-lang-elixir/examples.md +0 -29
- moai_adk/templates/.claude/skills/moai-lang-elixir/reference.md +0 -31
- moai_adk/templates/.claude/skills/moai-lang-haskell/SKILL.md +0 -124
- moai_adk/templates/.claude/skills/moai-lang-haskell/examples.md +0 -29
- moai_adk/templates/.claude/skills/moai-lang-haskell/reference.md +0 -31
- moai_adk/templates/.claude/skills/moai-lang-julia/SKILL.md +0 -123
- moai_adk/templates/.claude/skills/moai-lang-julia/examples.md +0 -29
- moai_adk/templates/.claude/skills/moai-lang-julia/reference.md +0 -30
- moai_adk/templates/.claude/skills/moai-lang-lua/SKILL.md +0 -123
- moai_adk/templates/.claude/skills/moai-lang-lua/examples.md +0 -29
- moai_adk/templates/.claude/skills/moai-lang-lua/reference.md +0 -30
- {moai_adk-0.4.7.dist-info → moai_adk-0.4.10.dist-info}/WHEEL +0 -0
- {moai_adk-0.4.7.dist-info → moai_adk-0.4.10.dist-info}/entry_points.txt +0 -0
- {moai_adk-0.4.7.dist-info → moai_adk-0.4.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Hook JSON 스키마 검증 및 해결 보고서
|
|
2
|
+
|
|
3
|
+
**작성일**: 2025-10-23
|
|
4
|
+
**태그**: @CODE:HOOKS-REFACTOR-001
|
|
5
|
+
**상태**: ✅ 해결 완료
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📋 문제 요약
|
|
10
|
+
|
|
11
|
+
### 초기 오류
|
|
12
|
+
```
|
|
13
|
+
SessionStart:startup hook error: JSON validation failed: Hook JSON output validation failed
|
|
14
|
+
Expected schema: { ... "systemMessage": ... }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 근본 원인
|
|
18
|
+
Claude Code Hook 스키마에서 `systemMessage`가 **최상위 필드**여야 하지만, 일부 구현에서는 이를 `hookSpecificOutput` 내부에 중첩시키고 있었습니다.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 🔍 분석 결과
|
|
23
|
+
|
|
24
|
+
### Claude Code 공식 Hook 스키마
|
|
25
|
+
|
|
26
|
+
#### 1. 일반 Hook 이벤트 (SessionStart, PreToolUse, PostToolUse, SessionEnd 등)
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"continue": true|false, // ✅ 기본 필드
|
|
31
|
+
"systemMessage": "string", // ✅ 최상위 필드 (NOT in hookSpecificOutput)
|
|
32
|
+
"decision": "approve"|"block"|undefined, // ✅ 선택적
|
|
33
|
+
"reason": "string", // ✅ 선택적
|
|
34
|
+
"permissionDecision": "allow"|"deny"|"ask"|undefined, // ✅ 선택적
|
|
35
|
+
"suppressOutput": true|false // ✅ 선택적
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### 2. UserPromptSubmit 전용 스키마
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"continue": true,
|
|
44
|
+
"hookSpecificOutput": { // ✅ UserPromptSubmit에만 사용
|
|
45
|
+
"hookEventName": "UserPromptSubmit",
|
|
46
|
+
"additionalContext": "string"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 핵심 규칙
|
|
52
|
+
|
|
53
|
+
| 규칙 | 설명 |
|
|
54
|
+
|------|------|
|
|
55
|
+
| **systemMessage 위치** | 최상위 필드 (`output["systemMessage"]`) |
|
|
56
|
+
| **hookSpecificOutput** | UserPromptSubmit 전용 |
|
|
57
|
+
| **내부 필드** | `context_files`, `suggestions`, `exit_code`는 Python 로직용 (JSON 출력 제외) |
|
|
58
|
+
| **JSON 직렬화** | 모든 필드는 JSON 직렬화 가능해야 함 |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## ✅ 해결 방안
|
|
63
|
+
|
|
64
|
+
### 1. 코드 수정
|
|
65
|
+
|
|
66
|
+
**파일**: `.claude/hooks/alfred/core/__init__.py`
|
|
67
|
+
|
|
68
|
+
#### `to_dict()` 메서드 (라인 63-118)
|
|
69
|
+
```python
|
|
70
|
+
def to_dict(self) -> dict[str, Any]:
|
|
71
|
+
"""Claude Code 표준 Hook 출력 스키마로 변환"""
|
|
72
|
+
output: dict[str, Any] = {}
|
|
73
|
+
|
|
74
|
+
# 1. decision 또는 continue 추가
|
|
75
|
+
if self.decision:
|
|
76
|
+
output["decision"] = self.decision
|
|
77
|
+
else:
|
|
78
|
+
output["continue"] = self.continue_execution
|
|
79
|
+
|
|
80
|
+
# 2. reason 추가 (decision 또는 permissionDecision과 함께)
|
|
81
|
+
if self.reason:
|
|
82
|
+
output["reason"] = self.reason
|
|
83
|
+
|
|
84
|
+
# 3. suppressOutput 추가 (True인 경우만)
|
|
85
|
+
if self.suppress_output:
|
|
86
|
+
output["suppressOutput"] = True
|
|
87
|
+
|
|
88
|
+
# 4. permissionDecision 추가
|
|
89
|
+
if self.permission_decision:
|
|
90
|
+
output["permissionDecision"] = self.permission_decision
|
|
91
|
+
|
|
92
|
+
# 5. ⭐ systemMessage를 최상위 필드로 추가 (NOT in hookSpecificOutput)
|
|
93
|
+
if self.system_message:
|
|
94
|
+
output["systemMessage"] = self.system_message
|
|
95
|
+
|
|
96
|
+
# 🚫 내부 필드는 JSON 출력에서 제외
|
|
97
|
+
# - context_files: JIT 문맥 로드 (내부용)
|
|
98
|
+
# - suggestions: 제안 (내부용)
|
|
99
|
+
# - exit_code: 진단 (내부용)
|
|
100
|
+
|
|
101
|
+
return output
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### `to_user_prompt_submit_dict()` 메서드 (라인 120-160)
|
|
105
|
+
```python
|
|
106
|
+
def to_user_prompt_submit_dict(self) -> dict[str, Any]:
|
|
107
|
+
"""UserPromptSubmit Hook 전용 스키마"""
|
|
108
|
+
if self.context_files:
|
|
109
|
+
context_str = "\n".join([f"📎 Context: {f}" for f in self.context_files])
|
|
110
|
+
else:
|
|
111
|
+
context_str = ""
|
|
112
|
+
|
|
113
|
+
if self.system_message:
|
|
114
|
+
if context_str:
|
|
115
|
+
context_str = f"{self.system_message}\n\n{context_str}"
|
|
116
|
+
else:
|
|
117
|
+
context_str = self.system_message
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"continue": self.continue_execution,
|
|
121
|
+
"hookSpecificOutput": {
|
|
122
|
+
"hookEventName": "UserPromptSubmit",
|
|
123
|
+
"additionalContext": context_str
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 2. 설정 검증
|
|
129
|
+
|
|
130
|
+
**파일**: `.claude/settings.json` (라인 8-60)
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"hooks": {
|
|
135
|
+
"SessionStart": [
|
|
136
|
+
{
|
|
137
|
+
"hooks": [
|
|
138
|
+
{
|
|
139
|
+
"command": "uv run \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/alfred/alfred_hooks.py SessionStart",
|
|
140
|
+
"type": "command"
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
"UserPromptSubmit": [
|
|
146
|
+
{
|
|
147
|
+
"hooks": [
|
|
148
|
+
{
|
|
149
|
+
"command": "uv run \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/alfred/alfred_hooks.py UserPromptSubmit",
|
|
150
|
+
"type": "command"
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 🧪 검증 결과
|
|
162
|
+
|
|
163
|
+
### 1. 자동 테스트 (8/8 통과)
|
|
164
|
+
|
|
165
|
+
**파일**: `.claude/hooks/alfred/test_hook_output.py`
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
$ cd .claude/hooks/alfred && python test_hook_output.py
|
|
169
|
+
|
|
170
|
+
✅ Test 1: Basic output - PASSED
|
|
171
|
+
✅ Test 2: systemMessage (top-level) - PASSED
|
|
172
|
+
✅ Test 3: decision + reason - PASSED
|
|
173
|
+
✅ Test 4: UserPromptSubmit schema - PASSED
|
|
174
|
+
✅ Test 5: permissionDecision - PASSED
|
|
175
|
+
✅ Test 6: SessionStart typical output - PASSED
|
|
176
|
+
✅ Test 7: JSON serializable - PASSED
|
|
177
|
+
✅ Test 8: UserPromptSubmit with system_message - PASSED
|
|
178
|
+
|
|
179
|
+
✅ ALL 8 TESTS PASSED
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 2. 실제 Hook 실행 검증
|
|
183
|
+
|
|
184
|
+
#### SessionStart (compact phase)
|
|
185
|
+
```bash
|
|
186
|
+
$ echo '{"cwd": ".", "phase": "compact"}' | uv run .claude/hooks/alfred/alfred_hooks.py SessionStart
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
"continue": true,
|
|
190
|
+
"systemMessage": "🚀 MoAI-ADK Session Started\n Language: python\n Branch: develop (d905363)\n Changes: 215\n SPEC Progress: 30/31 (96%)\n Checkpoints: 2 available\n - delete-20251022-134841\n - critical-file-20251019-230247\n Restore: /alfred:0-project restore"
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
✅ **검증**:
|
|
195
|
+
- `systemMessage`가 최상위 필드
|
|
196
|
+
- JSON 유효성 확인
|
|
197
|
+
- `hookSpecificOutput` 없음 (올바름)
|
|
198
|
+
|
|
199
|
+
#### SessionStart (clear phase)
|
|
200
|
+
```bash
|
|
201
|
+
$ echo '{"cwd": ".", "phase": "clear"}' | uv run .claude/hooks/alfred/alfred_hooks.py SessionStart
|
|
202
|
+
|
|
203
|
+
{"continue": true}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
✅ **검증**:
|
|
207
|
+
- 최소 스키마 (continue만)
|
|
208
|
+
- clear 단계에서 중복 출력 방지
|
|
209
|
+
|
|
210
|
+
#### UserPromptSubmit
|
|
211
|
+
```bash
|
|
212
|
+
$ echo '{"cwd": ".", "userPrompt": "test"}' | uv run .claude/hooks/alfred/alfred_hooks.py UserPromptSubmit
|
|
213
|
+
|
|
214
|
+
{
|
|
215
|
+
"continue": true,
|
|
216
|
+
"hookSpecificOutput": {
|
|
217
|
+
"hookEventName": "UserPromptSubmit",
|
|
218
|
+
"additionalContext": "📎 Loaded 1 context file(s)\n\n📎 Context: tests/"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
✅ **검증**:
|
|
224
|
+
- UserPromptSubmit 특수 스키마
|
|
225
|
+
- `hookSpecificOutput` 사용 (올바름)
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 📚 각 Hook 이벤트별 스키마 가이드
|
|
230
|
+
|
|
231
|
+
| 이벤트 | 최소 JSON | 예시 | 차단 가능 |
|
|
232
|
+
|--------|-----------|------|----------|
|
|
233
|
+
| **SessionStart** | `{"continue": true}` | 프로젝트 상태 표시 | ❌ No |
|
|
234
|
+
| **SessionEnd** | `{"continue": true}` | 정리 작업 | ❌ No |
|
|
235
|
+
| **PreToolUse** | `{"continue": true}` | 도구 실행 승인/차단 | ✅ Yes |
|
|
236
|
+
| **PostToolUse** | `{"continue": true}` | 도구 실행 후 피드백 | ❌ No* |
|
|
237
|
+
| **UserPromptSubmit** | 특수 스키마 | 프롬프트 문맥 추가 | ✅ Yes |
|
|
238
|
+
| **Notification** | `{"continue": true}` | 알림 처리 | ❌ No |
|
|
239
|
+
| **Stop** | `{"continue": true}` | 종료 차단 | ✅ Yes |
|
|
240
|
+
| **SubagentStop** | `{"continue": true}` | 서브에이전트 종료 차단 | ✅ Yes |
|
|
241
|
+
|
|
242
|
+
*: PostToolUse는 도구가 이미 실행되었으므로 차단 불가능하지만, 피드백 제공 가능
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 🔧 구현 세부사항
|
|
247
|
+
|
|
248
|
+
### HookResult 클래스 필드
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
@dataclass
|
|
252
|
+
class HookResult:
|
|
253
|
+
# ✅ Claude Code 표준 필드 (JSON에 포함)
|
|
254
|
+
continue_execution: bool = True
|
|
255
|
+
suppress_output: bool = False
|
|
256
|
+
decision: Literal["approve", "block"] | None = None
|
|
257
|
+
reason: str | None = None
|
|
258
|
+
permission_decision: Literal["allow", "deny", "ask"] | None = None
|
|
259
|
+
system_message: str | None = None # ⭐ TOP-LEVEL in JSON
|
|
260
|
+
|
|
261
|
+
# 🚫 내부 필드 (JSON 출력 제외)
|
|
262
|
+
context_files: list[str] = field(default_factory=list)
|
|
263
|
+
suggestions: list[str] = field(default_factory=list)
|
|
264
|
+
exit_code: int = 0
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 메서드별 역할
|
|
268
|
+
|
|
269
|
+
| 메서드 | 사용 사건 | 반환 스키마 |
|
|
270
|
+
|--------|---------|----------|
|
|
271
|
+
| `to_dict()` | 일반 Hook 이벤트 | 표준 Claude Code 스키마 |
|
|
272
|
+
| `to_user_prompt_submit_dict()` | UserPromptSubmit 이벤트 | 특수 스키마 + hookSpecificOutput |
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 📖 참고 문서
|
|
277
|
+
|
|
278
|
+
### 공식 Claude Code 문서
|
|
279
|
+
- **Claude Code Hooks**: https://docs.claude.com/en/docs/claude-code/hooks
|
|
280
|
+
- **Hook Output Schema**: https://docs.claude.com/en/docs/claude-code/hooks#output-schema
|
|
281
|
+
|
|
282
|
+
### Context7 참고 자료
|
|
283
|
+
- **Claude Code Hooks Mastery** (Trust Score: 8.3, 100+ 코드 스니펫)
|
|
284
|
+
- **Claude Code Templates** (Trust Score: 10)
|
|
285
|
+
|
|
286
|
+
### 프로젝트 문서
|
|
287
|
+
- **CLAUDE.md**: `Error Message Standard (Shared)` 섹션
|
|
288
|
+
- **Hook 구현**: `.claude/hooks/alfred/handlers/` 디렉토리
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 🎯 결론
|
|
293
|
+
|
|
294
|
+
✅ **상태**: 해결 완료
|
|
295
|
+
✅ **검증**: 8/8 자동 테스트 통과
|
|
296
|
+
✅ **실제 실행**: 모든 Hook 이벤트 정상 작동
|
|
297
|
+
|
|
298
|
+
### 핵심 수정사항
|
|
299
|
+
1. `systemMessage`를 최상위 필드로 이동 (NOT in hookSpecificOutput)
|
|
300
|
+
2. UserPromptSubmit 특수 스키마 분리
|
|
301
|
+
3. 내부 필드 JSON 출력 제외
|
|
302
|
+
4. 모든 Hook 이벤트 스키마 정규화
|
|
303
|
+
|
|
304
|
+
### 다음 단계
|
|
305
|
+
- ✅ Hook 스키마 검증 자동화
|
|
306
|
+
- ✅ 테스트 스크립트 작성
|
|
307
|
+
- ⏭️ 현재 상태 유지 및 모니터링
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
**검증 완료**: 2025-10-23
|
|
312
|
+
**담당자**: @agent-cc-manager
|
|
313
|
+
**참고**: @CODE:HOOKS-REFACTOR-001
|
|
@@ -136,7 +136,8 @@ def main() -> None:
|
|
|
136
136
|
handler = handlers.get(event_name)
|
|
137
137
|
result = handler({"cwd": cwd, **data}) if handler else HookResult()
|
|
138
138
|
|
|
139
|
-
#
|
|
139
|
+
# Output Hook result as JSON
|
|
140
|
+
# Note: UserPromptSubmit uses to_user_prompt_submit_dict() for special schema
|
|
140
141
|
if event_name == "UserPromptSubmit":
|
|
141
142
|
print(json.dumps(result.to_user_prompt_submit_dict()))
|
|
142
143
|
else:
|
|
@@ -145,9 +146,21 @@ def main() -> None:
|
|
|
145
146
|
sys.exit(0)
|
|
146
147
|
|
|
147
148
|
except json.JSONDecodeError as e:
|
|
149
|
+
# Return valid Hook response even on JSON parse error
|
|
150
|
+
error_response: dict[str, Any] = {
|
|
151
|
+
"continue": True,
|
|
152
|
+
"hookSpecificOutput": {"error": f"JSON parse error: {e}"}
|
|
153
|
+
}
|
|
154
|
+
print(json.dumps(error_response))
|
|
148
155
|
print(f"JSON parse error: {e}", file=sys.stderr)
|
|
149
156
|
sys.exit(1)
|
|
150
157
|
except Exception as e:
|
|
158
|
+
# Return valid Hook response even on unexpected error
|
|
159
|
+
error_response: dict[str, Any] = {
|
|
160
|
+
"continue": True,
|
|
161
|
+
"hookSpecificOutput": {"error": f"Hook error: {e}"}
|
|
162
|
+
}
|
|
163
|
+
print(json.dumps(error_response))
|
|
151
164
|
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
152
165
|
sys.exit(1)
|
|
153
166
|
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
Common type definitions and utility functions
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from dataclasses import
|
|
8
|
-
from typing import Any, NotRequired, TypedDict
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Literal, NotRequired, TypedDict
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class HookPayload(TypedDict):
|
|
@@ -16,63 +16,147 @@ class HookPayload(TypedDict):
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
cwd: str
|
|
19
|
-
userPrompt: NotRequired[str]
|
|
19
|
+
userPrompt: NotRequired[str] # Includes only UserPromptSubmit events
|
|
20
20
|
tool: NotRequired[str] # PreToolUse/PostToolUse events
|
|
21
21
|
arguments: NotRequired[dict[str, Any]] # Tool arguments
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@dataclass
|
|
25
25
|
class HookResult:
|
|
26
|
-
"""Hook execution result
|
|
26
|
+
"""Hook execution result following Claude Code standard schema.
|
|
27
|
+
|
|
28
|
+
Attributes conform to Claude Code Hook output specification:
|
|
29
|
+
https://docs.claude.com/en/docs/claude-code/hooks
|
|
30
|
+
|
|
31
|
+
Standard Fields (Claude Code schema - included in JSON output):
|
|
32
|
+
continue_execution: Allow execution to continue (default True)
|
|
33
|
+
suppress_output: Suppress hook output display (default False)
|
|
34
|
+
decision: "approve" or "block" operation (optional)
|
|
35
|
+
reason: Explanation for decision (optional)
|
|
36
|
+
permission_decision: "allow", "deny", or "ask" (optional)
|
|
37
|
+
system_message: Message displayed to user (top-level field)
|
|
38
|
+
|
|
39
|
+
Internal Fields (MoAI-ADK only - NOT in JSON output):
|
|
40
|
+
context_files: List of context files to load (internal use only)
|
|
41
|
+
suggestions: Suggestions for user (internal use only)
|
|
42
|
+
exit_code: Exit code for diagnostics (internal use only)
|
|
43
|
+
|
|
44
|
+
Note:
|
|
45
|
+
- systemMessage appears at TOP LEVEL in JSON output
|
|
46
|
+
- hookSpecificOutput is ONLY used for UserPromptSubmit events
|
|
47
|
+
- Internal fields are used for Python logic but not serialized to JSON
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Claude Code standard fields
|
|
51
|
+
continue_execution: bool = True
|
|
52
|
+
suppress_output: bool = False
|
|
53
|
+
decision: Literal["approve", "block"] | None = None
|
|
54
|
+
reason: str | None = None
|
|
55
|
+
permission_decision: Literal["allow", "deny", "ask"] | None = None
|
|
27
56
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
contextFiles: list[str] = field(default_factory=list) # noqa: N815
|
|
57
|
+
# MoAI-ADK custom fields (wrapped in hookSpecificOutput)
|
|
58
|
+
system_message: str | None = None
|
|
59
|
+
context_files: list[str] = field(default_factory=list)
|
|
32
60
|
suggestions: list[str] = field(default_factory=list)
|
|
33
|
-
|
|
61
|
+
exit_code: int = 0
|
|
34
62
|
|
|
35
63
|
def to_dict(self) -> dict[str, Any]:
|
|
36
|
-
"""
|
|
37
|
-
|
|
64
|
+
"""Convert to Claude Code standard Hook output schema.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dictionary conforming to Claude Code Hook specification with:
|
|
68
|
+
- Top-level fields: continue, suppressOutput, decision, reason,
|
|
69
|
+
permissionDecision, systemMessage
|
|
70
|
+
- MoAI-ADK internal fields (context_files, suggestions, exit_code)
|
|
71
|
+
are NOT included in JSON output (used for internal logic only)
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> result = HookResult(continue_execution=True)
|
|
75
|
+
>>> result.to_dict()
|
|
76
|
+
{'continue': True}
|
|
77
|
+
|
|
78
|
+
>>> result = HookResult(decision="block", reason="Dangerous")
|
|
79
|
+
>>> result.to_dict()
|
|
80
|
+
{'decision': 'block', 'reason': 'Dangerous'}
|
|
81
|
+
|
|
82
|
+
>>> result = HookResult(system_message="Test")
|
|
83
|
+
>>> result.to_dict()
|
|
84
|
+
{'continue': True, 'systemMessage': 'Test'}
|
|
85
|
+
|
|
86
|
+
Note:
|
|
87
|
+
- systemMessage is a TOP-LEVEL field (not nested in hookSpecificOutput)
|
|
88
|
+
- hookSpecificOutput is ONLY used for UserPromptSubmit events
|
|
89
|
+
- context_files, suggestions, exit_code are internal-only fields
|
|
90
|
+
"""
|
|
91
|
+
output: dict[str, Any] = {}
|
|
92
|
+
|
|
93
|
+
# Add decision or continue flag
|
|
94
|
+
if self.decision:
|
|
95
|
+
output["decision"] = self.decision
|
|
96
|
+
else:
|
|
97
|
+
output["continue"] = self.continue_execution
|
|
98
|
+
|
|
99
|
+
# Add reason if provided (works with both decision and permissionDecision)
|
|
100
|
+
if self.reason:
|
|
101
|
+
output["reason"] = self.reason
|
|
102
|
+
|
|
103
|
+
# Add suppressOutput if True
|
|
104
|
+
if self.suppress_output:
|
|
105
|
+
output["suppressOutput"] = True
|
|
106
|
+
|
|
107
|
+
# Add permissionDecision if set
|
|
108
|
+
if self.permission_decision:
|
|
109
|
+
output["permissionDecision"] = self.permission_decision
|
|
110
|
+
|
|
111
|
+
# Add systemMessage at TOP LEVEL (required by Claude Code schema)
|
|
112
|
+
if self.system_message:
|
|
113
|
+
output["systemMessage"] = self.system_message
|
|
114
|
+
|
|
115
|
+
# Note: context_files, suggestions, exit_code are internal-only fields
|
|
116
|
+
# and are NOT included in the JSON output per Claude Code schema
|
|
117
|
+
|
|
118
|
+
return output
|
|
38
119
|
|
|
39
120
|
def to_user_prompt_submit_dict(self) -> dict[str, Any]:
|
|
40
|
-
"""UserPromptSubmit Hook-specific output format
|
|
121
|
+
"""UserPromptSubmit Hook-specific output format.
|
|
41
122
|
|
|
42
|
-
Claude Code requires a special schema for UserPromptSubmit
|
|
43
|
-
|
|
44
|
-
"hookEventName": "UserPromptSubmit",
|
|
45
|
-
"additionalContext": "string (required)"
|
|
46
|
-
}
|
|
123
|
+
Claude Code requires a special schema for UserPromptSubmit events.
|
|
124
|
+
The result is wrapped in the standard Hook schema with hookSpecificOutput.
|
|
47
125
|
|
|
48
126
|
Returns:
|
|
49
|
-
Claude Code UserPromptSubmit Hook Dictionary matching schema
|
|
127
|
+
Claude Code UserPromptSubmit Hook Dictionary matching schema:
|
|
128
|
+
{
|
|
129
|
+
"continue": true,
|
|
130
|
+
"hookSpecificOutput": {
|
|
131
|
+
"hookEventName": "UserPromptSubmit",
|
|
132
|
+
"additionalContext": "string"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
50
135
|
|
|
51
136
|
Examples:
|
|
52
|
-
>>> result = HookResult(
|
|
137
|
+
>>> result = HookResult(context_files=["tests/"])
|
|
53
138
|
>>> result.to_user_prompt_submit_dict()
|
|
54
|
-
{'hookEventName': 'UserPromptSubmit', 'additionalContext': '📎 Context: tests/'}
|
|
139
|
+
{'continue': True, 'hookSpecificOutput': {'hookEventName': 'UserPromptSubmit', 'additionalContext': '📎 Context: tests/'}}
|
|
55
140
|
"""
|
|
56
|
-
# Convert
|
|
57
|
-
if self.
|
|
58
|
-
context_str = "\n".join([f"📎 Context: {f}" for f in self.
|
|
141
|
+
# Convert context_files to additionalContext string
|
|
142
|
+
if self.context_files:
|
|
143
|
+
context_str = "\n".join([f"📎 Context: {f}" for f in self.context_files])
|
|
59
144
|
else:
|
|
60
145
|
context_str = ""
|
|
61
146
|
|
|
62
|
-
# Add
|
|
63
|
-
if self.
|
|
147
|
+
# Add system_message if there is one
|
|
148
|
+
if self.system_message:
|
|
64
149
|
if context_str:
|
|
65
|
-
context_str = f"{self.
|
|
150
|
+
context_str = f"{self.system_message}\n\n{context_str}"
|
|
66
151
|
else:
|
|
67
|
-
context_str = self.
|
|
68
|
-
|
|
69
|
-
# If the string is empty, use default
|
|
70
|
-
if not context_str:
|
|
71
|
-
context_str = ""
|
|
152
|
+
context_str = self.system_message
|
|
72
153
|
|
|
73
154
|
return {
|
|
74
|
-
"
|
|
75
|
-
"
|
|
155
|
+
"continue": self.continue_execution,
|
|
156
|
+
"hookSpecificOutput": {
|
|
157
|
+
"hookEventName": "UserPromptSubmit",
|
|
158
|
+
"additionalContext": context_str
|
|
159
|
+
}
|
|
76
160
|
}
|
|
77
161
|
|
|
78
162
|
|
|
@@ -19,7 +19,7 @@ def handle_session_start(payload: HookPayload) -> HookResult:
|
|
|
19
19
|
payload: Claude Code event payload (cwd key required)
|
|
20
20
|
|
|
21
21
|
Returns:
|
|
22
|
-
HookResult(
|
|
22
|
+
HookResult(system_message=project status summary message)
|
|
23
23
|
|
|
24
24
|
Message Format:
|
|
25
25
|
🚀 MoAI-ADK Session Started
|
|
@@ -31,14 +31,15 @@ def handle_session_start(payload: HookPayload) -> HookResult:
|
|
|
31
31
|
|
|
32
32
|
Note:
|
|
33
33
|
- Claude Code processes SessionStart in several stages (clear → compact)
|
|
34
|
-
- Display message only at
|
|
35
|
-
- "clear" step returns
|
|
34
|
+
- Display message only at "compact" stage to prevent duplicate output
|
|
35
|
+
- "clear" step returns minimal result (empty hookSpecificOutput)
|
|
36
36
|
|
|
37
37
|
TDD History:
|
|
38
38
|
- RED: Session startup message format test
|
|
39
39
|
- GREEN: Generate status message by combining helper functions
|
|
40
40
|
- REFACTOR: Improved message format, improved readability, added checkpoint list
|
|
41
41
|
- FIX: Prevent duplicate output of clear step (only compact step is displayed)
|
|
42
|
+
- UPDATE: Migrated to Claude Code standard Hook schema
|
|
42
43
|
|
|
43
44
|
@TAG:CHECKPOINT-EVENT-001
|
|
44
45
|
"""
|
|
@@ -46,7 +47,8 @@ def handle_session_start(payload: HookPayload) -> HookResult:
|
|
|
46
47
|
# Ignore the "clear" stage and output messages only at the "compact" stage
|
|
47
48
|
event_phase = payload.get("phase", "")
|
|
48
49
|
if event_phase == "clear":
|
|
49
|
-
|
|
50
|
+
# Return minimal valid Hook result for clear phase
|
|
51
|
+
return HookResult(continue_execution=True)
|
|
50
52
|
|
|
51
53
|
cwd = payload.get("cwd", ".")
|
|
52
54
|
language = detect_language(cwd)
|
|
@@ -59,7 +61,7 @@ def handle_session_start(payload: HookPayload) -> HookResult:
|
|
|
59
61
|
changes = git_info.get("changes", 0)
|
|
60
62
|
spec_progress = f"{specs['completed']}/{specs['total']}"
|
|
61
63
|
|
|
62
|
-
#
|
|
64
|
+
# system_message: displayed directly to the user
|
|
63
65
|
lines = [
|
|
64
66
|
"🚀 MoAI-ADK Session Started",
|
|
65
67
|
f" Language: {language}",
|
|
@@ -78,10 +80,7 @@ def handle_session_start(payload: HookPayload) -> HookResult:
|
|
|
78
80
|
|
|
79
81
|
system_message = "\n".join(lines)
|
|
80
82
|
|
|
81
|
-
return HookResult(
|
|
82
|
-
message=system_message, # for Claude context
|
|
83
|
-
systemMessage=system_message, # For user display
|
|
84
|
-
)
|
|
83
|
+
return HookResult(system_message=system_message)
|
|
85
84
|
|
|
86
85
|
|
|
87
86
|
def handle_session_end(payload: HookPayload) -> HookResult:
|
|
@@ -20,8 +20,8 @@ def handle_pre_tool_use(payload: HookPayload) -> HookResult:
|
|
|
20
20
|
|
|
21
21
|
Returns:
|
|
22
22
|
HookResult(
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
system_message=checkpoint creation notification (when danger is detected);
|
|
24
|
+
continue_execution=True (always continue operation)
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
Checkpoint Triggers:
|
|
@@ -34,7 +34,7 @@ def handle_pre_tool_use(payload: HookPayload) -> HookResult:
|
|
|
34
34
|
→ "🛡️ Checkpoint created: before-delete-20251015-143000"
|
|
35
35
|
|
|
36
36
|
Notes:
|
|
37
|
-
- Return
|
|
37
|
+
- Return continue_execution=True even after detection of danger (continue operation)
|
|
38
38
|
- Work continues even when checkpoint fails (ignores)
|
|
39
39
|
- Transparent background operation
|
|
40
40
|
|
|
@@ -52,14 +52,14 @@ def handle_pre_tool_use(payload: HookPayload) -> HookResult:
|
|
|
52
52
|
checkpoint_branch = create_checkpoint(cwd, operation_type)
|
|
53
53
|
|
|
54
54
|
if checkpoint_branch != "checkpoint-failed":
|
|
55
|
-
|
|
55
|
+
system_message = (
|
|
56
56
|
f"🛡️ Checkpoint created: {checkpoint_branch}\n"
|
|
57
57
|
f" Operation: {operation_type}"
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
-
return HookResult(
|
|
60
|
+
return HookResult(system_message=system_message, continue_execution=True)
|
|
61
61
|
|
|
62
|
-
return HookResult(
|
|
62
|
+
return HookResult(continue_execution=True)
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def handle_post_tool_use(payload: HookPayload) -> HookResult:
|
|
@@ -20,22 +20,23 @@ def handle_user_prompt_submit(payload: HookPayload) -> HookResult:
|
|
|
20
20
|
|
|
21
21
|
Returns:
|
|
22
22
|
HookResult(
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
system_message=Number of Files loaded (or None),
|
|
24
|
+
context_files=Recommended document path list
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
TDD History:
|
|
28
28
|
- RED: JIT document loading scenario testing
|
|
29
29
|
- GREEN: Recommend documents by calling get_jit_context()
|
|
30
30
|
- REFACTOR: Message conditional display (only when there is a file)
|
|
31
|
+
- UPDATE: Migrated to Claude Code standard Hook schema with snake_case fields
|
|
31
32
|
"""
|
|
32
33
|
user_prompt = payload.get("userPrompt", "")
|
|
33
34
|
cwd = payload.get("cwd", ".")
|
|
34
35
|
context_files = get_jit_context(user_prompt, cwd)
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
system_message = f"📎 Loaded {len(context_files)} context file(s)" if context_files else None
|
|
37
38
|
|
|
38
|
-
return HookResult(
|
|
39
|
+
return HookResult(system_message=system_message, context_files=context_files)
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
__all__ = ["handle_user_prompt_submit"]
|