@hustle-together/api-dev-tools 3.12.3 → 3.12.16

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 (96) hide show
  1. package/.claude/commands/hustle-build.md +259 -0
  2. package/.claude/commands/hustle-combine.md +1089 -0
  3. package/.claude/commands/hustle-ui-create-page.md +1078 -0
  4. package/.claude/commands/hustle-ui-create.md +1058 -0
  5. package/.claude/hooks/auto-answer.py +305 -0
  6. package/.claude/hooks/cache-research.py +337 -0
  7. package/.claude/hooks/check-api-routes.py +168 -0
  8. package/.claude/hooks/check-playwright-setup.py +103 -0
  9. package/.claude/hooks/check-storybook-setup.py +81 -0
  10. package/.claude/hooks/check-update.py +132 -0
  11. package/.claude/hooks/completion-promise-detector.py +293 -0
  12. package/.claude/hooks/context-capacity-warning.py +171 -0
  13. package/.claude/hooks/detect-interruption.py +165 -0
  14. package/.claude/hooks/docs-update-check.py +120 -0
  15. package/.claude/hooks/enforce-a11y-audit.py +202 -0
  16. package/.claude/hooks/enforce-brand-guide.py +241 -0
  17. package/.claude/hooks/enforce-component-type-confirm.py +97 -0
  18. package/.claude/hooks/enforce-dry-run.py +134 -0
  19. package/.claude/hooks/enforce-freshness.py +184 -0
  20. package/.claude/hooks/enforce-page-components.py +186 -0
  21. package/.claude/hooks/enforce-page-data-schema.py +155 -0
  22. package/.claude/hooks/enforce-questions-sourced.py +146 -0
  23. package/.claude/hooks/enforce-schema-from-interview.py +248 -0
  24. package/.claude/hooks/enforce-ui-disambiguation.py +108 -0
  25. package/.claude/hooks/enforce-ui-interview.py +130 -0
  26. package/.claude/hooks/generate-adr-options.py +282 -0
  27. package/.claude/hooks/generate-manifest-entry.py +1161 -0
  28. package/.claude/hooks/hook_utils.py +609 -0
  29. package/.claude/hooks/lib/__init__.py +1 -0
  30. package/.claude/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  31. package/.claude/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  32. package/.claude/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  33. package/.claude/hooks/lib/greptile.py +355 -0
  34. package/.claude/hooks/lib/ntfy.py +209 -0
  35. package/.claude/hooks/notify-input-needed.py +73 -0
  36. package/.claude/hooks/notify-phase-complete.py +90 -0
  37. package/.claude/hooks/ntfy-on-question.py +240 -0
  38. package/.claude/hooks/orchestrator-completion.py +313 -0
  39. package/.claude/hooks/orchestrator-handoff.py +267 -0
  40. package/.claude/hooks/orchestrator-session-startup.py +146 -0
  41. package/.claude/hooks/parallel-orchestrator.py +451 -0
  42. package/.claude/hooks/project-document-prompt.py +302 -0
  43. package/.claude/hooks/remote-question-proxy.py +284 -0
  44. package/.claude/hooks/remote-question-server.py +1224 -0
  45. package/.claude/hooks/run-code-review.py +393 -0
  46. package/.claude/hooks/run-visual-qa.py +338 -0
  47. package/.claude/hooks/session-logger.py +323 -0
  48. package/.claude/hooks/test-orchestrator-reground.py +248 -0
  49. package/.claude/hooks/track-scope-coverage.py +220 -0
  50. package/.claude/hooks/track-token-usage.py +121 -0
  51. package/.claude/hooks/update-adr-decision.py +236 -0
  52. package/.claude/hooks/update-api-showcase.py +161 -0
  53. package/.claude/hooks/update-registry.py +352 -0
  54. package/.claude/hooks/update-testing-checklist.py +195 -0
  55. package/.claude/hooks/update-ui-showcase.py +224 -0
  56. package/.claude/settings.local.json +7 -1
  57. package/.claude/test-auto-answer-bot.py +183 -0
  58. package/.claude/test-completion-detector.py +263 -0
  59. package/.claude/test-orchestrator-state.json +20 -0
  60. package/.claude/test-orchestrator.sh +271 -0
  61. package/.skills/api-create/SKILL.md +88 -3
  62. package/.skills/docs-sync/SKILL.md +260 -0
  63. package/.skills/hustle-build/SKILL.md +459 -0
  64. package/.skills/hustle-build-review/SKILL.md +518 -0
  65. package/CHANGELOG.md +87 -0
  66. package/README.md +86 -9
  67. package/bin/cli.js +1302 -88
  68. package/commands/hustle-api-create.md +22 -0
  69. package/commands/hustle-combine.md +81 -2
  70. package/commands/hustle-ui-create-page.md +84 -2
  71. package/commands/hustle-ui-create.md +82 -2
  72. package/hooks/auto-answer.py +228 -0
  73. package/hooks/check-update.py +132 -0
  74. package/hooks/ntfy-on-question.py +227 -0
  75. package/hooks/orchestrator-completion.py +313 -0
  76. package/hooks/orchestrator-handoff.py +189 -0
  77. package/hooks/orchestrator-session-startup.py +146 -0
  78. package/hooks/periodic-reground.py +230 -67
  79. package/hooks/update-api-showcase.py +13 -1
  80. package/hooks/update-ui-showcase.py +13 -1
  81. package/package.json +7 -3
  82. package/scripts/extract-schema-docs.cjs +322 -0
  83. package/templates/CLAUDE-SECTION.md +89 -64
  84. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  85. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  86. package/templates/api-showcase/_components/APITester.tsx +367 -58
  87. package/templates/docs/page.tsx +230 -0
  88. package/templates/hustle-build-defaults.json +84 -0
  89. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  90. package/templates/playwright-report/page.tsx +258 -0
  91. package/templates/settings.json +88 -7
  92. package/templates/test-results/page.tsx +237 -0
  93. package/templates/typedoc.json +19 -0
  94. package/templates/ui-showcase/_components/UIShowcase.tsx +1 -1
  95. package/templates/ui-showcase/page.tsx +1 -1
  96. package/.claude/api-dev-state.json +0 -466
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PostToolUse for Write/Edit
4
+ Purpose: Update .claude/registry.json when workflow completes Phase 13
5
+
6
+ This hook runs AFTER Claude writes/edits files. When it detects that
7
+ the completion phase status was just set to "complete" in api-dev-state.json,
8
+ it automatically updates the registry.json with the new entry.
9
+
10
+ Supports:
11
+ - API workflows (api-create) -> registry.apis
12
+ - Component workflows (ui-create-component) -> registry.components
13
+ - Page workflows (ui-create-page) -> registry.pages
14
+ - Combined workflows (combine-api, combine-ui) -> registry.combined
15
+
16
+ Version: 3.9.0
17
+
18
+ Returns:
19
+ - {"continue": true} - Always continues (logging only, no blocking)
20
+ - For UI workflows, includes notify message with UI Showcase link
21
+ """
22
+ import json
23
+ import sys
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+
27
+ # State file is in .claude/ directory (sibling to hooks/)
28
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
29
+ REGISTRY_FILE = Path(__file__).parent.parent / "registry.json"
30
+
31
+
32
+ def get_active_endpoint(state):
33
+ """Get active endpoint - supports both old and new state formats."""
34
+ # New format (v3.6.7+): endpoints object with active_endpoint pointer
35
+ if "endpoints" in state and "active_endpoint" in state:
36
+ active = state.get("active_endpoint")
37
+ if active and active in state["endpoints"]:
38
+ return active, state["endpoints"][active]
39
+ return None, None
40
+
41
+ # Old format: single endpoint field
42
+ endpoint = state.get("endpoint")
43
+ if endpoint:
44
+ return endpoint, state
45
+
46
+ return None, None
47
+
48
+
49
+ def load_registry():
50
+ """Load existing registry or create default."""
51
+ if REGISTRY_FILE.exists():
52
+ try:
53
+ return json.loads(REGISTRY_FILE.read_text())
54
+ except json.JSONDecodeError:
55
+ pass
56
+
57
+ return {
58
+ "version": "1.0.0",
59
+ "updated_at": "",
60
+ "description": "Central registry tracking all APIs, components, and pages created through Hustle Dev Tools",
61
+ "apis": {},
62
+ "components": {},
63
+ "pages": {},
64
+ "combined": {}
65
+ }
66
+
67
+
68
+ def save_registry(registry):
69
+ """Save registry to file."""
70
+ registry["updated_at"] = datetime.now().isoformat()
71
+ REGISTRY_FILE.write_text(json.dumps(registry, indent=2))
72
+
73
+
74
+ def extract_api_entry(endpoint_name, endpoint_state, state):
75
+ """Extract registry entry from state for a standard API."""
76
+ phases = endpoint_state.get("phases", state.get("phases", {}))
77
+ interview = phases.get("interview", {})
78
+ decisions = interview.get("decisions", {})
79
+
80
+ # Get purpose from scope or interview
81
+ scope = phases.get("scope", {})
82
+ purpose = scope.get("purpose", decisions.get("purpose", {}).get("response", ""))
83
+
84
+ # Get schema file path
85
+ schema = phases.get("schema_creation", {})
86
+ schema_file = schema.get("schema_file", f"src/app/api/v2/{endpoint_name}/schemas.ts")
87
+
88
+ # Get test file path
89
+ tdd_red = phases.get("tdd_red", {})
90
+ test_file = tdd_red.get("test_file", f"src/app/api/v2/{endpoint_name}/__tests__/{endpoint_name}.api.test.ts")
91
+
92
+ # Get implementation file path
93
+ tdd_green = phases.get("tdd_green", {})
94
+ impl_file = tdd_green.get("implementation_file", f"src/app/api/v2/{endpoint_name}/route.ts")
95
+
96
+ # Determine methods from interview decisions or default
97
+ methods = ["POST"]
98
+ if decisions.get("methods"):
99
+ methods = decisions.get("methods", {}).get("value", ["POST"])
100
+
101
+ return {
102
+ "name": endpoint_name.replace("-", " ").title(),
103
+ "description": purpose[:200] if purpose else f"API endpoint for {endpoint_name}",
104
+ "route": impl_file,
105
+ "schemas": schema_file,
106
+ "tests": test_file,
107
+ "methods": methods if isinstance(methods, list) else [methods],
108
+ "created_at": datetime.now().strftime("%Y-%m-%d"),
109
+ "status": "complete"
110
+ }
111
+
112
+
113
+ def extract_combined_entry(endpoint_name, endpoint_state, state):
114
+ """Extract registry entry for a combined API."""
115
+ combine_config = state.get("combine_config", endpoint_state.get("combine_config", {}))
116
+ phases = endpoint_state.get("phases", state.get("phases", {}))
117
+ interview = phases.get("interview", {})
118
+ decisions = interview.get("decisions", {})
119
+
120
+ # Get source APIs
121
+ source_elements = combine_config.get("source_elements", [])
122
+ combines = [elem.get("name") for elem in source_elements if elem.get("type") == "api"]
123
+
124
+ # Get purpose from scope
125
+ scope = phases.get("scope", {})
126
+ purpose = scope.get("purpose", "")
127
+
128
+ # Get flow type from interview
129
+ flow_type = decisions.get("execution_order", decisions.get("flow_type", "sequential"))
130
+ if isinstance(flow_type, dict):
131
+ flow_type = flow_type.get("value", "sequential")
132
+
133
+ return {
134
+ "name": endpoint_name.replace("-", " ").title(),
135
+ "type": "api",
136
+ "description": purpose[:200] if purpose else f"Combined API: {', '.join(combines)}",
137
+ "combines": combines,
138
+ "route": f"src/app/api/v2/{endpoint_name}/route.ts",
139
+ "schemas": f"src/app/api/v2/{endpoint_name}/schemas.ts",
140
+ "tests": f"src/app/api/v2/{endpoint_name}/__tests__/",
141
+ "flow_type": flow_type,
142
+ "created_at": datetime.now().strftime("%Y-%m-%d"),
143
+ "status": "complete"
144
+ }
145
+
146
+
147
+ def extract_component_entry(element_name, element_state, state):
148
+ """Extract registry entry for a UI component."""
149
+ phases = element_state.get("phases", state.get("phases", {}))
150
+ ui_config = state.get("ui_config", element_state.get("ui_config", {}))
151
+ interview = phases.get("interview", {})
152
+ decisions = interview.get("decisions", {})
153
+
154
+ # Get description from scope
155
+ scope = phases.get("scope", {})
156
+ description = scope.get("component_purpose", scope.get("purpose", ""))
157
+
158
+ # Get component type (atom, molecule, organism)
159
+ component_type = ui_config.get("component_type", decisions.get("component_type", {}).get("value", "atom"))
160
+
161
+ # Get variants from interview decisions
162
+ variants = ui_config.get("variants", [])
163
+ if not variants and decisions.get("variants"):
164
+ variants = decisions.get("variants", {}).get("value", [])
165
+
166
+ # Get accessibility level
167
+ accessibility = ui_config.get("accessibility_level", "wcag2aa")
168
+
169
+ # File paths (PascalCase for component name)
170
+ pascal_name = "".join(word.capitalize() for word in element_name.replace("-", " ").split())
171
+ base_path = f"src/components/{pascal_name}"
172
+
173
+ return {
174
+ "name": pascal_name,
175
+ "description": description[:200] if description else f"UI component: {pascal_name}",
176
+ "type": component_type,
177
+ "file": f"{base_path}/{pascal_name}.tsx",
178
+ "story": f"{base_path}/{pascal_name}.stories.tsx",
179
+ "tests": f"{base_path}/{pascal_name}.test.tsx",
180
+ "props_interface": f"{pascal_name}Props",
181
+ "variants": variants if isinstance(variants, list) else [],
182
+ "accessibility": accessibility,
183
+ "responsive": True,
184
+ "status": "complete",
185
+ "created_at": datetime.now().strftime("%Y-%m-%d")
186
+ }
187
+
188
+
189
+ def extract_page_entry(element_name, element_state, state):
190
+ """Extract registry entry for a page."""
191
+ phases = element_state.get("phases", state.get("phases", {}))
192
+ ui_config = state.get("ui_config", element_state.get("ui_config", {}))
193
+ interview = phases.get("interview", {})
194
+ decisions = interview.get("decisions", {})
195
+
196
+ # Get description from scope
197
+ scope = phases.get("scope", {})
198
+ description = scope.get("page_purpose", scope.get("purpose", ""))
199
+
200
+ # Get page type (landing, dashboard, form, list)
201
+ page_type = ui_config.get("page_type", decisions.get("page_type", {}).get("value", "landing"))
202
+
203
+ # Get components used (from component analysis phase)
204
+ component_analysis = phases.get("component_analysis", {})
205
+ uses_components = component_analysis.get("selected_components", [])
206
+ if not uses_components:
207
+ uses_components = ui_config.get("uses_components", [])
208
+
209
+ # Get data fetching type from interview
210
+ data_fetching = decisions.get("data_fetching", {}).get("value", "server")
211
+ if isinstance(data_fetching, dict):
212
+ data_fetching = data_fetching.get("value", "server")
213
+
214
+ # Check auth requirement
215
+ auth_required = decisions.get("auth_required", {}).get("value", False)
216
+ if isinstance(auth_required, dict):
217
+ auth_required = auth_required.get("value", False)
218
+
219
+ # Route path (kebab-case)
220
+ route_path = element_name.lower().replace(" ", "-").replace("_", "-")
221
+
222
+ return {
223
+ "name": element_name.replace("-", " ").title(),
224
+ "description": description[:200] if description else f"Page: {element_name}",
225
+ "type": page_type,
226
+ "file": f"src/app/{route_path}/page.tsx",
227
+ "route": f"/{route_path}",
228
+ "tests": f"tests/e2e/{route_path}.spec.ts",
229
+ "uses_components": uses_components if isinstance(uses_components, list) else [],
230
+ "data_fetching": data_fetching,
231
+ "auth_required": auth_required,
232
+ "status": "complete",
233
+ "created_at": datetime.now().strftime("%Y-%m-%d")
234
+ }
235
+
236
+
237
+ def get_active_element(state):
238
+ """Get active element - supports both API and UI workflows."""
239
+ # UI workflow format: elements object with active_element pointer
240
+ if "elements" in state and "active_element" in state:
241
+ active = state.get("active_element")
242
+ if active and active in state["elements"]:
243
+ return active, state["elements"][active]
244
+
245
+ # Fall back to API endpoint format
246
+ return get_active_endpoint(state)
247
+
248
+
249
+ def main():
250
+ # Read hook input from stdin
251
+ try:
252
+ input_data = json.load(sys.stdin)
253
+ except json.JSONDecodeError:
254
+ print(json.dumps({"continue": True}))
255
+ sys.exit(0)
256
+
257
+ tool_name = input_data.get("tool_name", "")
258
+
259
+ # Only process Write/Edit operations
260
+ if tool_name not in ["Write", "Edit"]:
261
+ print(json.dumps({"continue": True}))
262
+ sys.exit(0)
263
+
264
+ # Check if state file exists
265
+ if not STATE_FILE.exists():
266
+ print(json.dumps({"continue": True}))
267
+ sys.exit(0)
268
+
269
+ # Load state
270
+ try:
271
+ state = json.loads(STATE_FILE.read_text())
272
+ except json.JSONDecodeError:
273
+ print(json.dumps({"continue": True}))
274
+ sys.exit(0)
275
+
276
+ # Determine workflow type
277
+ workflow = state.get("workflow", "api-create")
278
+
279
+ # Get active element based on workflow type
280
+ if workflow in ["ui-create-component", "ui-create-page"]:
281
+ element_name, element_state = get_active_element(state)
282
+ else:
283
+ element_name, element_state = get_active_endpoint(state)
284
+
285
+ if not element_name or not element_state:
286
+ print(json.dumps({"continue": True}))
287
+ sys.exit(0)
288
+
289
+ # Check if completion phase just became "complete"
290
+ phases = element_state.get("phases", state.get("phases", {}))
291
+ completion = phases.get("completion", {})
292
+
293
+ if completion.get("status") != "complete":
294
+ print(json.dumps({"continue": True}))
295
+ sys.exit(0)
296
+
297
+ # Check if already in registry (avoid duplicates)
298
+ registry = load_registry()
299
+
300
+ # Result object - may include notify for UI workflows
301
+ result = {"continue": True}
302
+
303
+ # Route to appropriate handler based on workflow
304
+ if workflow == "ui-create-component":
305
+ # Component workflow
306
+ if element_name in registry.get("components", {}):
307
+ print(json.dumps(result))
308
+ sys.exit(0)
309
+
310
+ entry = extract_component_entry(element_name, element_state, state)
311
+ registry.setdefault("components", {})[element_name] = entry
312
+ result["notify"] = f"🎨 View in UI Showcase: http://localhost:3000/ui-showcase"
313
+
314
+ elif workflow == "ui-create-page":
315
+ # Page workflow
316
+ if element_name in registry.get("pages", {}):
317
+ print(json.dumps(result))
318
+ sys.exit(0)
319
+
320
+ entry = extract_page_entry(element_name, element_state, state)
321
+ registry.setdefault("pages", {})[element_name] = entry
322
+ result["notify"] = f"🎨 View in UI Showcase: http://localhost:3000/ui-showcase"
323
+
324
+ elif workflow in ["combine-api", "combine-ui"]:
325
+ # Combined workflow
326
+ if element_name in registry.get("combined", {}):
327
+ print(json.dumps(result))
328
+ sys.exit(0)
329
+
330
+ entry = extract_combined_entry(element_name, element_state, state)
331
+ registry.setdefault("combined", {})[element_name] = entry
332
+
333
+ else:
334
+ # Default: API workflow
335
+ if element_name in registry.get("apis", {}):
336
+ print(json.dumps(result))
337
+ sys.exit(0)
338
+
339
+ entry = extract_api_entry(element_name, element_state, state)
340
+ registry.setdefault("apis", {})[element_name] = entry
341
+ result["notify"] = f"🔌 View in API Showcase: http://localhost:3000/api-showcase"
342
+
343
+ # Save registry
344
+ save_registry(registry)
345
+
346
+ # Return success (with optional notify for UI workflows)
347
+ print(json.dumps(result))
348
+ sys.exit(0)
349
+
350
+
351
+ if __name__ == "__main__":
352
+ main()
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Auto-update TESTING_CHECKLIST.md when tests pass.
4
+
5
+ Hook Type: PostToolUse (matcher: Bash)
6
+
7
+ Detects test pass patterns and updates the checklist file with:
8
+ - Test results (PASS/FAIL)
9
+ - Timestamp
10
+ - Comments
11
+
12
+ Works by:
13
+ 1. Detecting test-related Bash commands
14
+ 2. Parsing output for pass/fail patterns
15
+ 3. Updating the corresponding checklist rows
16
+ """
17
+
18
+ import json
19
+ import os
20
+ import re
21
+ import sys
22
+ from datetime import datetime
23
+ from pathlib import Path
24
+
25
+
26
+ def get_tool_result():
27
+ """Get the tool result from environment"""
28
+ result = os.environ.get("CLAUDE_TOOL_RESULT", "")
29
+ return result
30
+
31
+
32
+ def get_tool_input():
33
+ """Get the tool input from environment"""
34
+ try:
35
+ input_json = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
36
+ return json.loads(input_json)
37
+ except Exception:
38
+ return {}
39
+
40
+
41
+ def detect_test_type(command: str, output: str) -> dict:
42
+ """Detect what type of test was run and if it passed"""
43
+ result = {
44
+ "is_test": False,
45
+ "test_type": None,
46
+ "passed": None,
47
+ "hook_name": None,
48
+ "details": None
49
+ }
50
+
51
+ command_lower = command.lower()
52
+
53
+ # Hook compilation test
54
+ if "python3" in command_lower and ".py" in command_lower:
55
+ if "hooks/" in command or ".claude/hooks/" in command:
56
+ result["is_test"] = True
57
+ result["test_type"] = "hook_compile"
58
+ # Extract hook name
59
+ match = re.search(r'(?:hooks/|\.claude/hooks/)([^/\s]+\.py)', command)
60
+ if match:
61
+ result["hook_name"] = match.group(1)
62
+ # Check for pass/fail
63
+ if "Traceback" in output or "Error" in output or "SyntaxError" in output:
64
+ result["passed"] = False
65
+ result["details"] = "Syntax/import error"
66
+ elif "exit code" in output.lower():
67
+ exit_match = re.search(r'exit code[:\s]+(\d+)', output.lower())
68
+ if exit_match:
69
+ result["passed"] = exit_match.group(1) == "0"
70
+ else:
71
+ result["passed"] = True
72
+ result["details"] = "Compiles"
73
+
74
+ # Hook enforcement test
75
+ if "python3" in command_lower and ("enforce" in command_lower or "verify" in command_lower):
76
+ result["is_test"] = True
77
+ result["test_type"] = "hook_enforcement"
78
+ match = re.search(r'(?:hooks/|\.claude/hooks/)([^/\s]+\.py)', command)
79
+ if match:
80
+ result["hook_name"] = match.group(1)
81
+
82
+ # Check for blocking behavior
83
+ if '"permissionDecision": "deny"' in output or "BLOCKED" in output:
84
+ result["passed"] = True
85
+ result["details"] = "BLOCKS correctly"
86
+ elif '"permissionDecision": "allow"' in output:
87
+ result["passed"] = True
88
+ result["details"] = "ALLOWS correctly"
89
+ elif '"continue": true' in output:
90
+ result["passed"] = True
91
+ result["details"] = "Continues"
92
+
93
+ # pnpm test
94
+ if "pnpm test" in command_lower or "npm test" in command_lower:
95
+ result["is_test"] = True
96
+ result["test_type"] = "unit_test"
97
+ if "PASS" in output or "passed" in output.lower():
98
+ result["passed"] = True
99
+ elif "FAIL" in output or "failed" in output.lower():
100
+ result["passed"] = False
101
+
102
+ return result
103
+
104
+
105
+ def update_checklist(hook_name: str, status: str, comment: str):
106
+ """Update the TESTING_CHECKLIST.md file with test results"""
107
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
108
+ checklist_path = Path(project_dir) / "TESTING_CHECKLIST.md"
109
+
110
+ if not checklist_path.exists():
111
+ return False
112
+
113
+ try:
114
+ content = checklist_path.read_text()
115
+ today = datetime.now().strftime("%Y-%m-%d")
116
+
117
+ # Pattern to find hook row in table (with empty Status column)
118
+ # Format: | `hook_name` | Type | Phase/Trigger | | |
119
+ pattern = rf'(\| `{re.escape(hook_name)}` \|[^|]+\|[^|]+\|)\s*\|\s*\|'
120
+ replacement = rf'\1 {status} | {comment} ({today}) |'
121
+
122
+ new_content = re.sub(pattern, replacement, content)
123
+
124
+ if new_content != content:
125
+ checklist_path.write_text(new_content)
126
+ return True
127
+
128
+ # Try alternate pattern for already-filled rows (update existing)
129
+ pattern2 = rf'(\| `{re.escape(hook_name)}` \|[^|]+\|[^|]+\|)[^|]+\|[^|]+\|'
130
+ replacement2 = rf'\1 {status} | {comment} ({today}) |'
131
+
132
+ new_content = re.sub(pattern2, replacement2, content)
133
+ if new_content != content:
134
+ checklist_path.write_text(new_content)
135
+ return True
136
+
137
+ except Exception as e:
138
+ # Log error but don't fail
139
+ pass
140
+
141
+ return False
142
+
143
+
144
+ def main():
145
+ tool_input = get_tool_input()
146
+ command = tool_input.get("command", "")
147
+ output = get_tool_result()
148
+
149
+ # Detect what test was run
150
+ test_info = detect_test_type(command, output)
151
+
152
+ if not test_info["is_test"]:
153
+ print(json.dumps({"continue": True}))
154
+ return
155
+
156
+ # Update checklist if we have a hook name
157
+ if test_info["hook_name"] and test_info["passed"] is not None:
158
+ status = "PASS" if test_info["passed"] else "FAIL"
159
+ comment = test_info["details"] or ("Tested" if test_info["passed"] else "Failed")
160
+
161
+ updated = update_checklist(
162
+ test_info["hook_name"],
163
+ status,
164
+ comment
165
+ )
166
+
167
+ if updated:
168
+ # Log the update
169
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
170
+ logs_dir = Path(project_dir) / ".claude" / "workflow-logs"
171
+ logs_dir.mkdir(parents=True, exist_ok=True)
172
+
173
+ log_file = logs_dir / "checklist-updates.json"
174
+ try:
175
+ if log_file.exists():
176
+ log = json.loads(log_file.read_text())
177
+ else:
178
+ log = {"updates": []}
179
+
180
+ log["updates"].append({
181
+ "timestamp": datetime.now().isoformat(),
182
+ "hook": test_info["hook_name"],
183
+ "status": status,
184
+ "comment": comment
185
+ })
186
+
187
+ log_file.write_text(json.dumps(log, indent=2))
188
+ except Exception:
189
+ pass
190
+
191
+ print(json.dumps({"continue": True}))
192
+
193
+
194
+ if __name__ == "__main__":
195
+ main()