claude-ketchup 0.1.0

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 (246) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +544 -0
  3. package/bin/cli.ts +6 -0
  4. package/bin/postinstall.ts +5 -0
  5. package/bin/preuninstall.ts +5 -0
  6. package/commands/ketchup.md +107 -0
  7. package/dist/bin/cli.d.ts +3 -0
  8. package/dist/bin/cli.d.ts.map +1 -0
  9. package/dist/bin/cli.js +7 -0
  10. package/dist/bin/cli.js.map +1 -0
  11. package/dist/bin/postinstall.d.ts +3 -0
  12. package/dist/bin/postinstall.d.ts.map +1 -0
  13. package/dist/bin/postinstall.js +6 -0
  14. package/dist/bin/postinstall.js.map +1 -0
  15. package/dist/bin/preuninstall.d.ts +3 -0
  16. package/dist/bin/preuninstall.d.ts.map +1 -0
  17. package/dist/bin/preuninstall.js +6 -0
  18. package/dist/bin/preuninstall.js.map +1 -0
  19. package/dist/scripts/pre-tool-use.d.ts +3 -0
  20. package/dist/scripts/pre-tool-use.d.ts.map +1 -0
  21. package/dist/scripts/pre-tool-use.js +43 -0
  22. package/dist/scripts/pre-tool-use.js.map +1 -0
  23. package/dist/scripts/session-start.d.ts +3 -0
  24. package/dist/scripts/session-start.d.ts.map +1 -0
  25. package/dist/scripts/session-start.js +42 -0
  26. package/dist/scripts/session-start.js.map +1 -0
  27. package/dist/scripts/user-prompt-submit.d.ts +3 -0
  28. package/dist/scripts/user-prompt-submit.d.ts.map +1 -0
  29. package/dist/scripts/user-prompt-submit.js +43 -0
  30. package/dist/scripts/user-prompt-submit.js.map +1 -0
  31. package/dist/src/clean-logs.d.ts +6 -0
  32. package/dist/src/clean-logs.d.ts.map +1 -0
  33. package/dist/src/clean-logs.js +38 -0
  34. package/dist/src/clean-logs.js.map +1 -0
  35. package/dist/src/clean-logs.test.d.ts +2 -0
  36. package/dist/src/clean-logs.test.d.ts.map +1 -0
  37. package/dist/src/clean-logs.test.js +101 -0
  38. package/dist/src/clean-logs.test.js.map +1 -0
  39. package/dist/src/cli/cli.d.ts +3 -0
  40. package/dist/src/cli/cli.d.ts.map +1 -0
  41. package/dist/src/cli/cli.js +24 -0
  42. package/dist/src/cli/cli.js.map +1 -0
  43. package/dist/src/cli/cli.test.d.ts +2 -0
  44. package/dist/src/cli/cli.test.d.ts.map +1 -0
  45. package/dist/src/cli/cli.test.js +20 -0
  46. package/dist/src/cli/cli.test.js.map +1 -0
  47. package/dist/src/cli/doctor.d.ts +7 -0
  48. package/dist/src/cli/doctor.d.ts.map +1 -0
  49. package/dist/src/cli/doctor.js +52 -0
  50. package/dist/src/cli/doctor.js.map +1 -0
  51. package/dist/src/cli/doctor.test.d.ts +2 -0
  52. package/dist/src/cli/doctor.test.d.ts.map +1 -0
  53. package/dist/src/cli/doctor.test.js +77 -0
  54. package/dist/src/cli/doctor.test.js.map +1 -0
  55. package/dist/src/cli/repair.d.ts +7 -0
  56. package/dist/src/cli/repair.d.ts.map +1 -0
  57. package/dist/src/cli/repair.js +67 -0
  58. package/dist/src/cli/repair.js.map +1 -0
  59. package/dist/src/cli/repair.test.d.ts +2 -0
  60. package/dist/src/cli/repair.test.d.ts.map +1 -0
  61. package/dist/src/cli/repair.test.js +72 -0
  62. package/dist/src/cli/repair.test.js.map +1 -0
  63. package/dist/src/cli/skills.d.ts +11 -0
  64. package/dist/src/cli/skills.d.ts.map +1 -0
  65. package/dist/src/cli/skills.js +53 -0
  66. package/dist/src/cli/skills.js.map +1 -0
  67. package/dist/src/cli/skills.test.d.ts +2 -0
  68. package/dist/src/cli/skills.test.d.ts.map +1 -0
  69. package/dist/src/cli/skills.test.js +89 -0
  70. package/dist/src/cli/skills.test.js.map +1 -0
  71. package/dist/src/cli/status.d.ts +10 -0
  72. package/dist/src/cli/status.d.ts.map +1 -0
  73. package/dist/src/cli/status.js +63 -0
  74. package/dist/src/cli/status.js.map +1 -0
  75. package/dist/src/cli/status.test.d.ts +2 -0
  76. package/dist/src/cli/status.test.d.ts.map +1 -0
  77. package/dist/src/cli/status.test.js +70 -0
  78. package/dist/src/cli/status.test.js.map +1 -0
  79. package/dist/src/clue-collector.d.ts +23 -0
  80. package/dist/src/clue-collector.d.ts.map +1 -0
  81. package/dist/src/clue-collector.js +226 -0
  82. package/dist/src/clue-collector.js.map +1 -0
  83. package/dist/src/clue-collector.test.d.ts +2 -0
  84. package/dist/src/clue-collector.test.d.ts.map +1 -0
  85. package/dist/src/clue-collector.test.js +213 -0
  86. package/dist/src/clue-collector.test.js.map +1 -0
  87. package/dist/src/debug-logger.d.ts +2 -0
  88. package/dist/src/debug-logger.d.ts.map +1 -0
  89. package/dist/src/debug-logger.js +23 -0
  90. package/dist/src/debug-logger.js.map +1 -0
  91. package/dist/src/debug-logger.test.d.ts +2 -0
  92. package/dist/src/debug-logger.test.d.ts.map +1 -0
  93. package/dist/src/debug-logger.test.js +63 -0
  94. package/dist/src/debug-logger.test.js.map +1 -0
  95. package/dist/src/deny-list.d.ts +3 -0
  96. package/dist/src/deny-list.d.ts.map +1 -0
  97. package/dist/src/deny-list.js +62 -0
  98. package/dist/src/deny-list.js.map +1 -0
  99. package/dist/src/deny-list.test.d.ts +2 -0
  100. package/dist/src/deny-list.test.d.ts.map +1 -0
  101. package/dist/src/deny-list.test.js +93 -0
  102. package/dist/src/deny-list.test.js.map +1 -0
  103. package/dist/src/e2e.test.d.ts +2 -0
  104. package/dist/src/e2e.test.d.ts.map +1 -0
  105. package/dist/src/e2e.test.js +88 -0
  106. package/dist/src/e2e.test.js.map +1 -0
  107. package/dist/src/gitignore-manager.d.ts +2 -0
  108. package/dist/src/gitignore-manager.d.ts.map +1 -0
  109. package/dist/src/gitignore-manager.js +45 -0
  110. package/dist/src/gitignore-manager.js.map +1 -0
  111. package/dist/src/gitignore-manager.test.d.ts +2 -0
  112. package/dist/src/gitignore-manager.test.d.ts.map +1 -0
  113. package/dist/src/gitignore-manager.test.js +70 -0
  114. package/dist/src/gitignore-manager.test.js.map +1 -0
  115. package/dist/src/hook-state.d.ts +43 -0
  116. package/dist/src/hook-state.d.ts.map +1 -0
  117. package/dist/src/hook-state.js +124 -0
  118. package/dist/src/hook-state.js.map +1 -0
  119. package/dist/src/hook-state.test.d.ts +2 -0
  120. package/dist/src/hook-state.test.d.ts.map +1 -0
  121. package/dist/src/hook-state.test.js +190 -0
  122. package/dist/src/hook-state.test.js.map +1 -0
  123. package/dist/src/hooks/auto-continue.d.ts +9 -0
  124. package/dist/src/hooks/auto-continue.d.ts.map +1 -0
  125. package/dist/src/hooks/auto-continue.js +56 -0
  126. package/dist/src/hooks/auto-continue.js.map +1 -0
  127. package/dist/src/hooks/auto-continue.test.d.ts +2 -0
  128. package/dist/src/hooks/auto-continue.test.d.ts.map +1 -0
  129. package/dist/src/hooks/auto-continue.test.js +141 -0
  130. package/dist/src/hooks/auto-continue.test.js.map +1 -0
  131. package/dist/src/hooks/pre-tool-use.d.ts +8 -0
  132. package/dist/src/hooks/pre-tool-use.d.ts.map +1 -0
  133. package/dist/src/hooks/pre-tool-use.js +19 -0
  134. package/dist/src/hooks/pre-tool-use.js.map +1 -0
  135. package/dist/src/hooks/pre-tool-use.test.d.ts +2 -0
  136. package/dist/src/hooks/pre-tool-use.test.d.ts.map +1 -0
  137. package/dist/src/hooks/pre-tool-use.test.js +84 -0
  138. package/dist/src/hooks/pre-tool-use.test.js.map +1 -0
  139. package/dist/src/hooks/session-start.d.ts +6 -0
  140. package/dist/src/hooks/session-start.d.ts.map +1 -0
  141. package/dist/src/hooks/session-start.js +49 -0
  142. package/dist/src/hooks/session-start.js.map +1 -0
  143. package/dist/src/hooks/session-start.test.d.ts +2 -0
  144. package/dist/src/hooks/session-start.test.d.ts.map +1 -0
  145. package/dist/src/hooks/session-start.test.js +96 -0
  146. package/dist/src/hooks/session-start.test.js.map +1 -0
  147. package/dist/src/hooks/user-prompt-submit.d.ts +6 -0
  148. package/dist/src/hooks/user-prompt-submit.d.ts.map +1 -0
  149. package/dist/src/hooks/user-prompt-submit.js +54 -0
  150. package/dist/src/hooks/user-prompt-submit.js.map +1 -0
  151. package/dist/src/hooks/user-prompt-submit.test.d.ts +2 -0
  152. package/dist/src/hooks/user-prompt-submit.test.d.ts.map +1 -0
  153. package/dist/src/hooks/user-prompt-submit.test.js +92 -0
  154. package/dist/src/hooks/user-prompt-submit.test.js.map +1 -0
  155. package/dist/src/hooks/validate-commit.d.ts +12 -0
  156. package/dist/src/hooks/validate-commit.d.ts.map +1 -0
  157. package/dist/src/hooks/validate-commit.js +58 -0
  158. package/dist/src/hooks/validate-commit.js.map +1 -0
  159. package/dist/src/hooks/validate-commit.test.d.ts +2 -0
  160. package/dist/src/hooks/validate-commit.test.d.ts.map +1 -0
  161. package/dist/src/hooks/validate-commit.test.js +150 -0
  162. package/dist/src/hooks/validate-commit.test.js.map +1 -0
  163. package/dist/src/index.d.ts +13 -0
  164. package/dist/src/index.d.ts.map +1 -0
  165. package/dist/src/index.js +38 -0
  166. package/dist/src/index.js.map +1 -0
  167. package/dist/src/linker.d.ts +6 -0
  168. package/dist/src/linker.d.ts.map +1 -0
  169. package/dist/src/linker.js +78 -0
  170. package/dist/src/linker.js.map +1 -0
  171. package/dist/src/linker.test.d.ts +2 -0
  172. package/dist/src/linker.test.d.ts.map +1 -0
  173. package/dist/src/linker.test.js +192 -0
  174. package/dist/src/linker.test.js.map +1 -0
  175. package/dist/src/logger.d.ts +21 -0
  176. package/dist/src/logger.d.ts.map +1 -0
  177. package/dist/src/logger.js +117 -0
  178. package/dist/src/logger.js.map +1 -0
  179. package/dist/src/logger.test.d.ts +2 -0
  180. package/dist/src/logger.test.d.ts.map +1 -0
  181. package/dist/src/logger.test.js +159 -0
  182. package/dist/src/logger.test.js.map +1 -0
  183. package/dist/src/postinstall.d.ts +7 -0
  184. package/dist/src/postinstall.d.ts.map +1 -0
  185. package/dist/src/postinstall.js +81 -0
  186. package/dist/src/postinstall.js.map +1 -0
  187. package/dist/src/postinstall.test.d.ts +2 -0
  188. package/dist/src/postinstall.test.d.ts.map +1 -0
  189. package/dist/src/postinstall.test.js +125 -0
  190. package/dist/src/postinstall.test.js.map +1 -0
  191. package/dist/src/preuninstall.d.ts +2 -0
  192. package/dist/src/preuninstall.d.ts.map +1 -0
  193. package/dist/src/preuninstall.js +62 -0
  194. package/dist/src/preuninstall.js.map +1 -0
  195. package/dist/src/preuninstall.test.d.ts +2 -0
  196. package/dist/src/preuninstall.test.d.ts.map +1 -0
  197. package/dist/src/preuninstall.test.js +97 -0
  198. package/dist/src/preuninstall.test.js.map +1 -0
  199. package/dist/src/root-finder.d.ts +2 -0
  200. package/dist/src/root-finder.d.ts.map +1 -0
  201. package/dist/src/root-finder.js +71 -0
  202. package/dist/src/root-finder.js.map +1 -0
  203. package/dist/src/root-finder.test.d.ts +2 -0
  204. package/dist/src/root-finder.test.d.ts.map +1 -0
  205. package/dist/src/root-finder.test.js +111 -0
  206. package/dist/src/root-finder.test.js.map +1 -0
  207. package/dist/src/settings-merger.d.ts +2 -0
  208. package/dist/src/settings-merger.d.ts.map +1 -0
  209. package/dist/src/settings-merger.js +136 -0
  210. package/dist/src/settings-merger.js.map +1 -0
  211. package/dist/src/settings-merger.test.d.ts +2 -0
  212. package/dist/src/settings-merger.test.d.ts.map +1 -0
  213. package/dist/src/settings-merger.test.js +387 -0
  214. package/dist/src/settings-merger.test.js.map +1 -0
  215. package/dist/src/skills-loader.d.ts +14 -0
  216. package/dist/src/skills-loader.d.ts.map +1 -0
  217. package/dist/src/skills-loader.js +90 -0
  218. package/dist/src/skills-loader.js.map +1 -0
  219. package/dist/src/skills-loader.test.d.ts +2 -0
  220. package/dist/src/skills-loader.test.d.ts.map +1 -0
  221. package/dist/src/skills-loader.test.js +222 -0
  222. package/dist/src/skills-loader.test.js.map +1 -0
  223. package/dist/src/state-manager.d.ts +5 -0
  224. package/dist/src/state-manager.d.ts.map +1 -0
  225. package/dist/src/state-manager.js +55 -0
  226. package/dist/src/state-manager.js.map +1 -0
  227. package/dist/src/state-manager.test.d.ts +2 -0
  228. package/dist/src/state-manager.test.d.ts.map +1 -0
  229. package/dist/src/state-manager.test.js +85 -0
  230. package/dist/src/state-manager.test.js.map +1 -0
  231. package/dist/src/subagent-classifier.d.ts +4 -0
  232. package/dist/src/subagent-classifier.d.ts.map +1 -0
  233. package/dist/src/subagent-classifier.js +53 -0
  234. package/dist/src/subagent-classifier.js.map +1 -0
  235. package/dist/src/subagent-classifier.test.d.ts +2 -0
  236. package/dist/src/subagent-classifier.test.d.ts.map +1 -0
  237. package/dist/src/subagent-classifier.test.js +88 -0
  238. package/dist/src/subagent-classifier.test.js.map +1 -0
  239. package/package.json +59 -0
  240. package/scripts/pre-tool-use.ts +10 -0
  241. package/scripts/session-start.ts +9 -0
  242. package/scripts/tail-logs.sh +17 -0
  243. package/scripts/test-hooks.sh +910 -0
  244. package/scripts/user-prompt-submit.ts +10 -0
  245. package/skills/ketchup.enforced.md +23 -0
  246. package/templates/settings.json +57 -0
@@ -0,0 +1,910 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
6
+ TEMP_BASE=$(mktemp -d)
7
+ trap "rm -rf $TEMP_BASE" EXIT
8
+
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ YELLOW='\033[0;33m'
12
+ NC='\033[0m'
13
+
14
+ PASSED=0
15
+ FAILED=0
16
+
17
+ pass() { echo -e "${GREEN}✓${NC} $1"; ((PASSED++)) || true; }
18
+ fail() { echo -e "${RED}✗${NC} $1: $2"; ((FAILED++)) || true; }
19
+ skip() { echo -e "${YELLOW}○${NC} $1 (skipped)"; }
20
+
21
+ #-----------------------------------------------------------
22
+ # Test: auto-continue respects mode=off (no transcript needed)
23
+ #-----------------------------------------------------------
24
+ test_autocontinue_mode_off() {
25
+ local name="auto-continue respects mode=off"
26
+
27
+ # Backup current state
28
+ local state_file="$PROJECT_ROOT/.claude.hooks.json"
29
+ local backup=""
30
+ if [[ -f "$state_file" ]]; then
31
+ backup=$(cat "$state_file")
32
+ fi
33
+
34
+ # Set mode to off
35
+ echo '{"autoContinue":{"mode":"off"}}' > "$state_file"
36
+
37
+ local input='{"session_id":"test-off","permission_mode":"code"}'
38
+
39
+ cd "$PROJECT_ROOT"
40
+ local output
41
+ output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null) || true
42
+
43
+ # Restore state
44
+ if [[ -n "$backup" ]]; then
45
+ echo "$backup" > "$state_file"
46
+ else
47
+ rm -f "$state_file"
48
+ fi
49
+
50
+ if echo "$output" | grep -q '"decision":"block"'; then
51
+ fail "$name" "should not block when mode=off"
52
+ else
53
+ pass "$name"
54
+ fi
55
+ }
56
+
57
+ #-----------------------------------------------------------
58
+ # Test: auto-continue skips plan permission mode
59
+ #-----------------------------------------------------------
60
+ test_autocontinue_skips_plan_mode() {
61
+ local name="auto-continue skips plan permission mode"
62
+
63
+ # Backup current state
64
+ local state_file="$PROJECT_ROOT/.claude.hooks.json"
65
+ local backup=""
66
+ if [[ -f "$state_file" ]]; then
67
+ backup=$(cat "$state_file")
68
+ fi
69
+
70
+ # Set mode to smart (but plan mode should be skipped)
71
+ echo '{"autoContinue":{"mode":"smart"}}' > "$state_file"
72
+
73
+ # permission_mode="plan" should be skipped by default
74
+ local input='{"session_id":"test-plan","permission_mode":"plan"}'
75
+
76
+ cd "$PROJECT_ROOT"
77
+ local output
78
+ output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null) || true
79
+
80
+ # Restore state
81
+ if [[ -n "$backup" ]]; then
82
+ echo "$backup" > "$state_file"
83
+ else
84
+ rm -f "$state_file"
85
+ fi
86
+
87
+ if echo "$output" | grep -q '"decision":"block"'; then
88
+ fail "$name" "should skip plan mode"
89
+ else
90
+ pass "$name"
91
+ fi
92
+ }
93
+
94
+ #-----------------------------------------------------------
95
+ # Test: non-stop mode blocks and counts iterations
96
+ #-----------------------------------------------------------
97
+ test_autocontinue_nonstop_iterations() {
98
+ local name="auto-continue non-stop mode counts iterations"
99
+
100
+ # Backup current state
101
+ local state_file="$PROJECT_ROOT/.claude.hooks.json"
102
+ local backup=""
103
+ if [[ -f "$state_file" ]]; then
104
+ backup=$(cat "$state_file")
105
+ fi
106
+
107
+ # Set non-stop mode with limit of 2
108
+ echo '{"autoContinue":{"mode":"non-stop","maxIterations":2,"iteration":0}}' > "$state_file"
109
+
110
+ local input='{"session_id":"test-nonstop","permission_mode":"code"}'
111
+
112
+ cd "$PROJECT_ROOT"
113
+
114
+ # First call: should block and increment
115
+ local output1
116
+ output1=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null)
117
+
118
+ if ! echo "$output1" | grep -q '"decision":"block"'; then
119
+ # Restore state
120
+ if [[ -n "$backup" ]]; then
121
+ echo "$backup" > "$state_file"
122
+ else
123
+ rm -f "$state_file"
124
+ fi
125
+ fail "$name" "iteration 1 should block"
126
+ return
127
+ fi
128
+
129
+ # Second call: should block again
130
+ local output2
131
+ output2=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null)
132
+
133
+ if ! echo "$output2" | grep -q '"decision":"block"'; then
134
+ # Restore state
135
+ if [[ -n "$backup" ]]; then
136
+ echo "$backup" > "$state_file"
137
+ else
138
+ rm -f "$state_file"
139
+ fi
140
+ fail "$name" "iteration 2 should block"
141
+ return
142
+ fi
143
+
144
+ # Third call: should allow (maxIterations reached)
145
+ local output3
146
+ output3=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null) || true
147
+
148
+ # Restore state
149
+ if [[ -n "$backup" ]]; then
150
+ echo "$backup" > "$state_file"
151
+ else
152
+ rm -f "$state_file"
153
+ fi
154
+
155
+ if echo "$output3" | grep -q '"decision":"block"'; then
156
+ fail "$name" "iteration 3 should allow (limit reached)"
157
+ else
158
+ pass "$name"
159
+ fi
160
+ }
161
+
162
+ #-----------------------------------------------------------
163
+ # Test: deny-list denies paths on deny list
164
+ #-----------------------------------------------------------
165
+ test_denylist_denies() {
166
+ local name="deny-list denies paths on deny list"
167
+
168
+ # Backup state and deny list
169
+ local state_file="$PROJECT_ROOT/.claude.hooks.json"
170
+ local state_backup=""
171
+ if [[ -f "$state_file" ]]; then
172
+ state_backup=$(cat "$state_file")
173
+ fi
174
+
175
+ local deny_file="$PROJECT_ROOT/.claude/scripts/deny-list.txt"
176
+ local deny_backup=""
177
+ if [[ -f "$deny_file" ]]; then
178
+ deny_backup=$(cat "$deny_file")
179
+ fi
180
+
181
+ # Enable deny list in state and add test pattern
182
+ echo '{"denyList":{"enabled":true}}' > "$state_file"
183
+ echo "test-secret.txt" > "$deny_file"
184
+
185
+ # Simulate PreToolUse input for Edit tool
186
+ local input='{"tool_name":"Edit","tool_input":{"file_path":"test-secret.txt"}}'
187
+
188
+ cd "$PROJECT_ROOT"
189
+ local output
190
+ output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/deny-list.ts" 2>/dev/null) || true
191
+
192
+ # Restore state and deny list
193
+ if [[ -n "$state_backup" ]]; then
194
+ echo "$state_backup" > "$state_file"
195
+ else
196
+ rm -f "$state_file"
197
+ fi
198
+
199
+ if [[ -n "$deny_backup" ]]; then
200
+ echo "$deny_backup" > "$deny_file"
201
+ else
202
+ rm -f "$deny_file"
203
+ fi
204
+
205
+ if echo "$output" | grep -q '"permissionDecision":"deny"'; then
206
+ pass "$name"
207
+ else
208
+ fail "$name" "should deny access to test-secret.txt, got: $output"
209
+ fi
210
+ }
211
+
212
+ #-----------------------------------------------------------
213
+ # Test: deny-list allows non-denied paths
214
+ #-----------------------------------------------------------
215
+ test_denylist_allows_normal() {
216
+ local name="deny-list allows normal paths"
217
+
218
+ # Backup state and deny list
219
+ local state_file="$PROJECT_ROOT/.claude.hooks.json"
220
+ local state_backup=""
221
+ if [[ -f "$state_file" ]]; then
222
+ state_backup=$(cat "$state_file")
223
+ fi
224
+
225
+ local deny_file="$PROJECT_ROOT/.claude/scripts/deny-list.txt"
226
+ local deny_backup=""
227
+ if [[ -f "$deny_file" ]]; then
228
+ deny_backup=$(cat "$deny_file")
229
+ fi
230
+
231
+ # Enable deny list but only deny something-else.txt
232
+ echo '{"denyList":{"enabled":true}}' > "$state_file"
233
+ echo "something-else.txt" > "$deny_file"
234
+
235
+ local input='{"tool_name":"Edit","tool_input":{"file_path":"normal-file.txt"}}'
236
+
237
+ cd "$PROJECT_ROOT"
238
+ local output
239
+ output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/deny-list.ts" 2>/dev/null) || true
240
+
241
+ # Restore state and deny list
242
+ if [[ -n "$state_backup" ]]; then
243
+ echo "$state_backup" > "$state_file"
244
+ else
245
+ rm -f "$state_file"
246
+ fi
247
+
248
+ if [[ -n "$deny_backup" ]]; then
249
+ echo "$deny_backup" > "$deny_file"
250
+ else
251
+ rm -f "$deny_file"
252
+ fi
253
+
254
+ if echo "$output" | grep -q '"permissionDecision":"deny"'; then
255
+ fail "$name" "should not deny normal-file.txt"
256
+ else
257
+ pass "$name"
258
+ fi
259
+ }
260
+
261
+ #-----------------------------------------------------------
262
+ # Test: prompt-reminder returns context
263
+ #-----------------------------------------------------------
264
+ test_prompt_reminder_returns_context() {
265
+ local name="prompt-reminder returns context"
266
+
267
+ local input='{"prompt":"help me code"}'
268
+
269
+ cd "$PROJECT_ROOT"
270
+ local output
271
+ output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/prompt-reminder.ts" 2>/dev/null) || true
272
+
273
+ # Should contain some kind of response (not empty)
274
+ if [[ -n "$output" ]]; then
275
+ pass "$name"
276
+ else
277
+ fail "$name" "should return some context"
278
+ fi
279
+ }
280
+
281
+ #-----------------------------------------------------------
282
+ # Test: sub-agent inherits deny-list (permission_mode doesn't bypass)
283
+ #-----------------------------------------------------------
284
+ test_subagent_inherits_denylist() {
285
+ local name="sub-agent inherits deny-list protection"
286
+
287
+ # Backup state and deny list
288
+ local state_file="$PROJECT_ROOT/.claude.hooks.json"
289
+ local state_backup=""
290
+ if [[ -f "$state_file" ]]; then
291
+ state_backup=$(cat "$state_file")
292
+ fi
293
+
294
+ local deny_file="$PROJECT_ROOT/.claude/scripts/deny-list.txt"
295
+ local deny_backup=""
296
+ if [[ -f "$deny_file" ]]; then
297
+ deny_backup=$(cat "$deny_file")
298
+ fi
299
+
300
+ # Enable deny list and add test pattern
301
+ echo '{"denyList":{"enabled":true}}' > "$state_file"
302
+ echo "protected-file.txt" > "$deny_file"
303
+
304
+ # Simulate PreToolUse from a sub-agent (with permission_mode that might be different)
305
+ # Sub-agents should STILL be blocked by deny-list regardless of permission_mode
306
+ local input='{"tool_name":"Edit","tool_input":{"file_path":"protected-file.txt"},"permission_mode":"plan"}'
307
+
308
+ cd "$PROJECT_ROOT"
309
+ local output
310
+ output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/deny-list.ts" 2>/dev/null) || true
311
+
312
+ # Restore state and deny list
313
+ if [[ -n "$state_backup" ]]; then
314
+ echo "$state_backup" > "$state_file"
315
+ else
316
+ rm -f "$state_file"
317
+ fi
318
+
319
+ if [[ -n "$deny_backup" ]]; then
320
+ echo "$deny_backup" > "$deny_file"
321
+ else
322
+ rm -f "$deny_file"
323
+ fi
324
+
325
+ if echo "$output" | grep -q '"permissionDecision":"deny"'; then
326
+ pass "$name"
327
+ else
328
+ fail "$name" "sub-agent should still be blocked by deny-list"
329
+ fi
330
+ }
331
+
332
+ #-----------------------------------------------------------
333
+ # Test: sub-agent state modifications persist
334
+ #-----------------------------------------------------------
335
+ test_subagent_state_persists() {
336
+ local name="sub-agent state modifications persist"
337
+
338
+ # Backup current state
339
+ local state_file="$PROJECT_ROOT/.claude.hooks.json"
340
+ local backup=""
341
+ if [[ -f "$state_file" ]]; then
342
+ backup=$(cat "$state_file")
343
+ fi
344
+
345
+ # Set initial state with iteration=5
346
+ echo '{"autoContinue":{"mode":"non-stop","maxIterations":10,"iteration":5}}' > "$state_file"
347
+
348
+ # Simulate a sub-agent call that would increment iteration
349
+ # (non-stop mode increments on each block)
350
+ local input='{"session_id":"subagent-test","permission_mode":"code"}'
351
+
352
+ cd "$PROJECT_ROOT"
353
+ echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null || true
354
+
355
+ # Read back the state - iteration should now be 6
356
+ local new_state
357
+ new_state=$(cat "$state_file")
358
+
359
+ # Restore original state
360
+ if [[ -n "$backup" ]]; then
361
+ echo "$backup" > "$state_file"
362
+ else
363
+ rm -f "$state_file"
364
+ fi
365
+
366
+ if echo "$new_state" | grep -q '"iteration": 6'; then
367
+ pass "$name"
368
+ else
369
+ fail "$name" "state should persist iteration increment, got: $new_state"
370
+ fi
371
+ }
372
+
373
+ #-----------------------------------------------------------
374
+ # Test: sub-agent respects skipModes configuration
375
+ #-----------------------------------------------------------
376
+ test_subagent_respects_skipmodes() {
377
+ local name="sub-agent respects custom skipModes"
378
+
379
+ # Backup current state
380
+ local state_file="$PROJECT_ROOT/.claude.hooks.json"
381
+ local backup=""
382
+ if [[ -f "$state_file" ]]; then
383
+ backup=$(cat "$state_file")
384
+ fi
385
+
386
+ # Set mode to non-stop but add "explore" to skipModes
387
+ # This simulates skipping auto-continue for explore sub-agents
388
+ echo '{"autoContinue":{"mode":"non-stop","skipModes":["plan","explore"]}}' > "$state_file"
389
+
390
+ # Simulate a sub-agent with permission_mode="explore"
391
+ local input='{"session_id":"explore-agent","permission_mode":"explore"}'
392
+
393
+ cd "$PROJECT_ROOT"
394
+ local output
395
+ output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null) || true
396
+
397
+ # Restore state
398
+ if [[ -n "$backup" ]]; then
399
+ echo "$backup" > "$state_file"
400
+ else
401
+ rm -f "$state_file"
402
+ fi
403
+
404
+ # Should NOT block because "explore" is in skipModes
405
+ if echo "$output" | grep -q '"decision":"block"'; then
406
+ fail "$name" "should skip auto-continue for explore permission_mode"
407
+ else
408
+ pass "$name"
409
+ fi
410
+ }
411
+
412
+ #-----------------------------------------------------------
413
+ # Test: sub-agent non-skipped mode still blocks
414
+ #-----------------------------------------------------------
415
+ test_subagent_nonskipped_blocks() {
416
+ local name="sub-agent non-skipped mode blocks correctly"
417
+
418
+ # Backup current state
419
+ local state_file="$PROJECT_ROOT/.claude.hooks.json"
420
+ local backup=""
421
+ if [[ -f "$state_file" ]]; then
422
+ backup=$(cat "$state_file")
423
+ fi
424
+
425
+ # Set mode to non-stop with only "plan" in skipModes
426
+ echo '{"autoContinue":{"mode":"non-stop","maxIterations":10,"skipModes":["plan"]}}' > "$state_file"
427
+
428
+ # Simulate a sub-agent with permission_mode="code" (NOT in skipModes)
429
+ local input='{"session_id":"code-agent","permission_mode":"code"}'
430
+
431
+ cd "$PROJECT_ROOT"
432
+ local output
433
+ output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null) || true
434
+
435
+ # Restore state
436
+ if [[ -n "$backup" ]]; then
437
+ echo "$backup" > "$state_file"
438
+ else
439
+ rm -f "$state_file"
440
+ fi
441
+
442
+ # SHOULD block because "code" is not in skipModes
443
+ if echo "$output" | grep -q '"decision":"block"'; then
444
+ pass "$name"
445
+ else
446
+ fail "$name" "should block for non-skipped permission_mode"
447
+ fi
448
+ }
449
+
450
+ #-----------------------------------------------------------
451
+ # Test: subagent-classifier classifies explore tasks
452
+ #-----------------------------------------------------------
453
+ test_subagent_classifier_explore() {
454
+ local name="subagent-classifier classifies explore tasks"
455
+
456
+ cd "$PROJECT_ROOT"
457
+
458
+ # Create temp test file with absolute import
459
+ local test_file="$PROJECT_ROOT/scripts/_test-classify-explore.ts"
460
+ cat > "$test_file" << 'EOF'
461
+ import { classifySubagent } from '../src/subagent-classifier.js';
462
+ console.log(classifySubagent('Search for auth implementation'));
463
+ EOF
464
+
465
+ local output
466
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
467
+ rm -f "$test_file"
468
+
469
+ if [[ "$output" == "explore" ]]; then
470
+ pass "$name"
471
+ else
472
+ fail "$name" "expected 'explore', got '$output'"
473
+ fi
474
+ }
475
+
476
+ #-----------------------------------------------------------
477
+ # Test: subagent-classifier classifies work tasks
478
+ #-----------------------------------------------------------
479
+ test_subagent_classifier_work() {
480
+ local name="subagent-classifier classifies work tasks"
481
+
482
+ cd "$PROJECT_ROOT"
483
+
484
+ # Create temp test file with absolute import
485
+ local test_file="$PROJECT_ROOT/scripts/_test-classify-work.ts"
486
+ cat > "$test_file" << 'EOF'
487
+ import { classifySubagent } from '../src/subagent-classifier.js';
488
+ console.log(classifySubagent('Implement the new feature'));
489
+ EOF
490
+
491
+ local output
492
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
493
+ rm -f "$test_file"
494
+
495
+ if [[ "$output" == "work" ]]; then
496
+ pass "$name"
497
+ else
498
+ fail "$name" "expected 'work', got '$output'"
499
+ fi
500
+ }
501
+
502
+ #-----------------------------------------------------------
503
+ # Test: shouldValidateCommit respects subagentHooks state
504
+ #-----------------------------------------------------------
505
+ test_shouldvalidatecommit_respects_state() {
506
+ local name="shouldValidateCommit respects subagentHooks state"
507
+
508
+ cd "$PROJECT_ROOT"
509
+
510
+ # Create temp test file with absolute import
511
+ local test_file="$PROJECT_ROOT/scripts/_test-should-validate.ts"
512
+ cat > "$test_file" << 'EOF'
513
+ import { shouldValidateCommit } from '../src/hooks/validate-commit.js';
514
+ const state = { validateCommitOnExplore: false, validateCommitOnWork: true, validateCommitOnUnknown: true };
515
+ console.log(shouldValidateCommit('explore', state) ? 'true' : 'false');
516
+ console.log(shouldValidateCommit('work', state) ? 'true' : 'false');
517
+ EOF
518
+
519
+ local output
520
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
521
+ rm -f "$test_file"
522
+
523
+ local expected=$'false\ntrue'
524
+ if [[ "$output" == "$expected" ]]; then
525
+ pass "$name"
526
+ else
527
+ fail "$name" "expected 'false\\ntrue', got '$output'"
528
+ fi
529
+ }
530
+
531
+ #-----------------------------------------------------------
532
+ # Test: extractTaskDescription parses Task invocations
533
+ #-----------------------------------------------------------
534
+ test_extract_task_description() {
535
+ local name="extractTaskDescription parses Task invocations"
536
+
537
+ cd "$PROJECT_ROOT"
538
+
539
+ # Create temp test file with absolute import
540
+ local test_file="$PROJECT_ROOT/scripts/_test-extract.ts"
541
+ cat > "$test_file" << 'EOF'
542
+ import { extractTaskDescription } from '../src/subagent-classifier.js';
543
+ const transcript = '<invoke name="Task"><parameter name="description">Search for files</parameter></invoke>';
544
+ console.log(extractTaskDescription(transcript) || 'undefined');
545
+ EOF
546
+
547
+ local output
548
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
549
+ rm -f "$test_file"
550
+
551
+ if [[ "$output" == "Search for files" ]]; then
552
+ pass "$name"
553
+ else
554
+ fail "$name" "expected 'Search for files', got '$output'"
555
+ fi
556
+ }
557
+
558
+ #-----------------------------------------------------------
559
+ # Test: full integration - explore task skips validation
560
+ #-----------------------------------------------------------
561
+ test_integration_explore_skips_validation() {
562
+ local name="integration: explore task skips validation by default"
563
+
564
+ cd "$PROJECT_ROOT"
565
+
566
+ # Create integration test file
567
+ local test_file="$PROJECT_ROOT/scripts/_test-integration-explore.ts"
568
+ cat > "$test_file" << 'EOF'
569
+ import { classifySubagent, extractTaskDescription } from '../src/subagent-classifier.js';
570
+ import { shouldValidateCommit } from '../src/hooks/validate-commit.js';
571
+ import { DEFAULT_HOOK_STATE } from '../src/hook-state.js';
572
+
573
+ // Simulate a transcript with an explore task
574
+ const transcript = '<invoke name="Task"><parameter name="description">Search for auth implementation</parameter></invoke>';
575
+
576
+ // Extract and classify
577
+ const description = extractTaskDescription(transcript);
578
+ const subagentType = classifySubagent(description || '');
579
+
580
+ // Check if validation should run with default state
581
+ const shouldValidate = shouldValidateCommit(subagentType, DEFAULT_HOOK_STATE.subagentHooks);
582
+
583
+ console.log(`description: ${description}`);
584
+ console.log(`type: ${subagentType}`);
585
+ console.log(`shouldValidate: ${shouldValidate}`);
586
+ EOF
587
+
588
+ local output
589
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
590
+ rm -f "$test_file"
591
+
592
+ if echo "$output" | grep -q "type: explore" && echo "$output" | grep -q "shouldValidate: false"; then
593
+ pass "$name"
594
+ else
595
+ fail "$name" "explore task should skip validation, got: $output"
596
+ fi
597
+ }
598
+
599
+ #-----------------------------------------------------------
600
+ # Test: full integration - work task runs validation
601
+ #-----------------------------------------------------------
602
+ test_integration_work_runs_validation() {
603
+ local name="integration: work task runs validation by default"
604
+
605
+ cd "$PROJECT_ROOT"
606
+
607
+ # Create integration test file
608
+ local test_file="$PROJECT_ROOT/scripts/_test-integration-work.ts"
609
+ cat > "$test_file" << 'EOF'
610
+ import { classifySubagent, extractTaskDescription } from '../src/subagent-classifier.js';
611
+ import { shouldValidateCommit } from '../src/hooks/validate-commit.js';
612
+ import { DEFAULT_HOOK_STATE } from '../src/hook-state.js';
613
+
614
+ // Simulate a transcript with a work task
615
+ const transcript = '<invoke name="Task"><parameter name="description">Implement the new feature</parameter></invoke>';
616
+
617
+ // Extract and classify
618
+ const description = extractTaskDescription(transcript);
619
+ const subagentType = classifySubagent(description || '');
620
+
621
+ // Check if validation should run with default state
622
+ const shouldValidate = shouldValidateCommit(subagentType, DEFAULT_HOOK_STATE.subagentHooks);
623
+
624
+ console.log(`description: ${description}`);
625
+ console.log(`type: ${subagentType}`);
626
+ console.log(`shouldValidate: ${shouldValidate}`);
627
+ EOF
628
+
629
+ local output
630
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
631
+ rm -f "$test_file"
632
+
633
+ if echo "$output" | grep -q "type: work" && echo "$output" | grep -q "shouldValidate: true"; then
634
+ pass "$name"
635
+ else
636
+ fail "$name" "work task should run validation, got: $output"
637
+ fi
638
+ }
639
+
640
+ #-----------------------------------------------------------
641
+ # Test: full integration - unknown task runs validation (safe default)
642
+ #-----------------------------------------------------------
643
+ test_integration_unknown_runs_validation() {
644
+ local name="integration: unknown task runs validation (safe default)"
645
+
646
+ cd "$PROJECT_ROOT"
647
+
648
+ # Create integration test file
649
+ local test_file="$PROJECT_ROOT/scripts/_test-integration-unknown.ts"
650
+ cat > "$test_file" << 'EOF'
651
+ import { classifySubagent, extractTaskDescription } from '../src/subagent-classifier.js';
652
+ import { shouldValidateCommit } from '../src/hooks/validate-commit.js';
653
+ import { DEFAULT_HOOK_STATE } from '../src/hook-state.js';
654
+
655
+ // Simulate a transcript with an ambiguous task
656
+ const transcript = '<invoke name="Task"><parameter name="description">Process the data</parameter></invoke>';
657
+
658
+ // Extract and classify
659
+ const description = extractTaskDescription(transcript);
660
+ const subagentType = classifySubagent(description || '');
661
+
662
+ // Check if validation should run with default state
663
+ const shouldValidate = shouldValidateCommit(subagentType, DEFAULT_HOOK_STATE.subagentHooks);
664
+
665
+ console.log(`description: ${description}`);
666
+ console.log(`type: ${subagentType}`);
667
+ console.log(`shouldValidate: ${shouldValidate}`);
668
+ EOF
669
+
670
+ local output
671
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
672
+ rm -f "$test_file"
673
+
674
+ if echo "$output" | grep -q "type: unknown" && echo "$output" | grep -q "shouldValidate: true"; then
675
+ pass "$name"
676
+ else
677
+ fail "$name" "unknown task should run validation (safe default), got: $output"
678
+ fi
679
+ }
680
+
681
+ #-----------------------------------------------------------
682
+ # Test: state file controls subagent validation behavior
683
+ #-----------------------------------------------------------
684
+ test_integration_state_controls_behavior() {
685
+ local name="integration: state file controls subagent validation behavior"
686
+
687
+ # Backup current state
688
+ local state_file="$PROJECT_ROOT/.claude.hooks.json"
689
+ local backup=""
690
+ if [[ -f "$state_file" ]]; then
691
+ backup=$(cat "$state_file")
692
+ fi
693
+
694
+ # Set custom state: enable validation for explore, disable for work
695
+ echo '{"subagentHooks":{"validateCommitOnExplore":true,"validateCommitOnWork":false,"validateCommitOnUnknown":true}}' > "$state_file"
696
+
697
+ cd "$PROJECT_ROOT"
698
+
699
+ # Create integration test file
700
+ local test_file="$PROJECT_ROOT/scripts/_test-integration-state.ts"
701
+ cat > "$test_file" << 'EOF'
702
+ import { classifySubagent } from '../src/subagent-classifier.js';
703
+ import { shouldValidateCommit } from '../src/hooks/validate-commit.js';
704
+ import { createHookState } from '../src/hook-state.js';
705
+
706
+ const hookState = createHookState(process.cwd());
707
+ const state = hookState.read();
708
+
709
+ // Test explore
710
+ const exploreType = classifySubagent('Search for files');
711
+ const exploreValidate = shouldValidateCommit(exploreType, state.subagentHooks);
712
+
713
+ // Test work
714
+ const workType = classifySubagent('Implement feature');
715
+ const workValidate = shouldValidateCommit(workType, state.subagentHooks);
716
+
717
+ console.log(`explore: ${exploreValidate}`);
718
+ console.log(`work: ${workValidate}`);
719
+ EOF
720
+
721
+ local output
722
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
723
+ rm -f "$test_file"
724
+
725
+ # Restore state
726
+ if [[ -n "$backup" ]]; then
727
+ echo "$backup" > "$state_file"
728
+ else
729
+ rm -f "$state_file"
730
+ fi
731
+
732
+ # With our custom state: explore=true, work=false (opposite of default)
733
+ if echo "$output" | grep -q "explore: true" && echo "$output" | grep -q "work: false"; then
734
+ pass "$name"
735
+ else
736
+ fail "$name" "state file should control behavior, got: $output"
737
+ fi
738
+ }
739
+
740
+ #-----------------------------------------------------------
741
+ # Test: classifier handles all explore patterns
742
+ #-----------------------------------------------------------
743
+ test_classifier_all_explore_patterns() {
744
+ local name="classifier handles all explore patterns"
745
+
746
+ cd "$PROJECT_ROOT"
747
+
748
+ local test_file="$PROJECT_ROOT/scripts/_test-all-explore.ts"
749
+ cat > "$test_file" << 'EOF'
750
+ import { classifySubagent } from '../src/subagent-classifier.js';
751
+
752
+ const patterns = [
753
+ 'Search for files matching pattern',
754
+ 'Find the implementation',
755
+ 'Understand how it works',
756
+ 'Investigate the error',
757
+ 'Analyze the codebase',
758
+ 'Look for usages',
759
+ 'Research existing patterns',
760
+ 'Explore the architecture',
761
+ 'Discover dependencies',
762
+ 'Locate the config file',
763
+ ];
764
+
765
+ let allExplore = true;
766
+ for (const p of patterns) {
767
+ const result = classifySubagent(p);
768
+ if (result !== 'explore') {
769
+ console.log(`FAIL: "${p}" classified as ${result}`);
770
+ allExplore = false;
771
+ }
772
+ }
773
+ console.log(allExplore ? 'ALL_EXPLORE' : 'SOME_FAILED');
774
+ EOF
775
+
776
+ local output
777
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
778
+ rm -f "$test_file"
779
+
780
+ if [[ "$output" == "ALL_EXPLORE" ]]; then
781
+ pass "$name"
782
+ else
783
+ fail "$name" "not all explore patterns classified correctly: $output"
784
+ fi
785
+ }
786
+
787
+ #-----------------------------------------------------------
788
+ # Test: classifier handles all work patterns
789
+ #-----------------------------------------------------------
790
+ test_classifier_all_work_patterns() {
791
+ local name="classifier handles all work patterns"
792
+
793
+ cd "$PROJECT_ROOT"
794
+
795
+ local test_file="$PROJECT_ROOT/scripts/_test-all-work.ts"
796
+ cat > "$test_file" << 'EOF'
797
+ import { classifySubagent } from '../src/subagent-classifier.js';
798
+
799
+ const patterns = [
800
+ 'Implement the new feature',
801
+ 'Create a user form',
802
+ 'Write tests for login',
803
+ 'Fix the bug in parser',
804
+ 'Refactor the database layer',
805
+ 'Update the configuration',
806
+ 'Add error handling',
807
+ 'Build the API endpoint',
808
+ 'Modify the schema',
809
+ 'Change the default value',
810
+ 'Remove unused code',
811
+ 'Delete the deprecated file',
812
+ ];
813
+
814
+ let allWork = true;
815
+ for (const p of patterns) {
816
+ const result = classifySubagent(p);
817
+ if (result !== 'work') {
818
+ console.log(`FAIL: "${p}" classified as ${result}`);
819
+ allWork = false;
820
+ }
821
+ }
822
+ console.log(allWork ? 'ALL_WORK' : 'SOME_FAILED');
823
+ EOF
824
+
825
+ local output
826
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
827
+ rm -f "$test_file"
828
+
829
+ if [[ "$output" == "ALL_WORK" ]]; then
830
+ pass "$name"
831
+ else
832
+ fail "$name" "not all work patterns classified correctly: $output"
833
+ fi
834
+ }
835
+
836
+ #-----------------------------------------------------------
837
+ # Test: DEFAULT_HOOK_STATE has correct subagentHooks defaults
838
+ #-----------------------------------------------------------
839
+ test_default_state_subagent_hooks() {
840
+ local name="DEFAULT_HOOK_STATE has correct subagentHooks defaults"
841
+
842
+ cd "$PROJECT_ROOT"
843
+
844
+ local test_file="$PROJECT_ROOT/scripts/_test-default-state.ts"
845
+ cat > "$test_file" << 'EOF'
846
+ import { DEFAULT_HOOK_STATE } from '../src/hook-state.js';
847
+
848
+ const sh = DEFAULT_HOOK_STATE.subagentHooks;
849
+ const correct =
850
+ sh.validateCommitOnExplore === false &&
851
+ sh.validateCommitOnWork === true &&
852
+ sh.validateCommitOnUnknown === true;
853
+
854
+ console.log(correct ? 'CORRECT' : `WRONG: explore=${sh.validateCommitOnExplore}, work=${sh.validateCommitOnWork}, unknown=${sh.validateCommitOnUnknown}`);
855
+ EOF
856
+
857
+ local output
858
+ output=$(npx tsx "$test_file" 2>/dev/null) || true
859
+ rm -f "$test_file"
860
+
861
+ if [[ "$output" == "CORRECT" ]]; then
862
+ pass "$name"
863
+ else
864
+ fail "$name" "$output"
865
+ fi
866
+ }
867
+
868
+ #-----------------------------------------------------------
869
+ # Run all tests
870
+ #-----------------------------------------------------------
871
+ echo "Running hook E2E tests..."
872
+ echo ""
873
+
874
+ echo "=== Basic Hook Tests ==="
875
+ test_autocontinue_mode_off
876
+ test_autocontinue_skips_plan_mode
877
+ test_autocontinue_nonstop_iterations
878
+ test_denylist_denies
879
+ test_denylist_allows_normal
880
+ test_prompt_reminder_returns_context
881
+
882
+ echo ""
883
+ echo "=== Sub-Agent Hook Tests ==="
884
+ test_subagent_inherits_denylist
885
+ test_subagent_state_persists
886
+ test_subagent_respects_skipmodes
887
+ test_subagent_nonskipped_blocks
888
+
889
+ echo ""
890
+ echo "=== Sub-Agent Classification Tests ==="
891
+ test_subagent_classifier_explore
892
+ test_subagent_classifier_work
893
+ test_shouldvalidatecommit_respects_state
894
+ test_extract_task_description
895
+
896
+ echo ""
897
+ echo "=== Classification Integration Tests ==="
898
+ test_integration_explore_skips_validation
899
+ test_integration_work_runs_validation
900
+ test_integration_unknown_runs_validation
901
+ test_integration_state_controls_behavior
902
+ test_classifier_all_explore_patterns
903
+ test_classifier_all_work_patterns
904
+ test_default_state_subagent_hooks
905
+
906
+ echo ""
907
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
908
+ echo "Results: $PASSED passed, $FAILED failed"
909
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
910
+ exit $((FAILED > 0 ? 1 : 0))