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

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 (180) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +10 -0
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/settings.local.json +1 -7
  9. package/.claude/workflow-logs/None.json +49 -0
  10. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  11. package/.skills/adr-deep-research/SKILL.md +351 -0
  12. package/.skills/api-create/SKILL.md +34 -20
  13. package/.skills/api-research/SKILL.md +130 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +365 -38
  17. package/.skills/parallel-spawn/SKILL.md +212 -0
  18. package/.skills/ralph-continue/SKILL.md +151 -0
  19. package/.skills/ralph-loop/SKILL.md +341 -0
  20. package/.skills/ralph-status/SKILL.md +87 -0
  21. package/.skills/refactor/SKILL.md +59 -0
  22. package/.skills/shadcn/SKILL.md +522 -0
  23. package/.skills/test-all/SKILL.md +210 -0
  24. package/.skills/test-builds/SKILL.md +208 -0
  25. package/.skills/test-debug/SKILL.md +212 -0
  26. package/.skills/test-e2e/SKILL.md +168 -0
  27. package/.skills/test-review/SKILL.md +707 -0
  28. package/.skills/test-unit/SKILL.md +143 -0
  29. package/.skills/test-visual/SKILL.md +301 -0
  30. package/.skills/token-report/SKILL.md +132 -0
  31. package/CHANGELOG.md +488 -0
  32. package/README.md +346 -53
  33. package/bin/cli.js +359 -123
  34. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  35. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  36. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  37. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  38. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  39. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  40. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  95. package/hooks/api-workflow-check.py +34 -0
  96. package/hooks/auto-answer.py +97 -20
  97. package/{.claude/hooks → hooks}/completion-promise-detector.py +0 -0
  98. package/{.claude/hooks → hooks}/context-capacity-warning.py +0 -0
  99. package/{.claude/hooks → hooks}/docs-update-check.py +0 -0
  100. package/{.claude/hooks → hooks}/enforce-dry-run.py +0 -0
  101. package/hooks/enforce-external-research.py +25 -0
  102. package/hooks/enforce-interview.py +20 -0
  103. package/{.claude/hooks → hooks}/generate-adr-options.py +0 -0
  104. package/{.claude/hooks → hooks}/hook_utils.py +0 -0
  105. package/hooks/ntfy-on-question.py +15 -2
  106. package/hooks/orchestrator-handoff.py +81 -3
  107. package/{.claude/hooks → hooks}/parallel-orchestrator.py +0 -0
  108. package/hooks/periodic-reground.py +40 -0
  109. package/{.claude/hooks → hooks}/remote-question-server.py +0 -0
  110. package/hooks/run-code-review.py +176 -29
  111. package/{.claude/hooks → hooks}/run-visual-qa.py +0 -0
  112. package/hooks/session-logger.py +27 -1
  113. package/hooks/session-startup.py +113 -0
  114. package/{.claude/hooks → hooks}/update-adr-decision.py +0 -0
  115. package/package.json +1 -1
  116. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  117. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  118. package/templates/api-dev-state.json +33 -1
  119. package/templates/brand-page/page.tsx +645 -0
  120. package/templates/component/Component.visual.spec.ts +30 -24
  121. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  122. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  123. package/templates/github-workflows/security.yml +274 -0
  124. package/templates/hustle-build-defaults.json +53 -1
  125. package/templates/page/page.e2e.test.ts +30 -26
  126. package/templates/performance-budgets.json +63 -5
  127. package/templates/registry.json +279 -3
  128. package/templates/review-dashboard/page.tsx +510 -0
  129. package/templates/settings.json +74 -7
  130. package/templates/ui-showcase/_components/UIShowcase.tsx +47 -0
  131. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  132. package/.claude/commands/hustle-combine.md +0 -1089
  133. package/.claude/commands/hustle-ui-create-page.md +0 -1078
  134. package/.claude/commands/hustle-ui-create.md +0 -1058
  135. package/.claude/hooks/auto-answer.py +0 -305
  136. package/.claude/hooks/cache-research.py +0 -337
  137. package/.claude/hooks/check-api-routes.py +0 -168
  138. package/.claude/hooks/check-playwright-setup.py +0 -103
  139. package/.claude/hooks/check-storybook-setup.py +0 -81
  140. package/.claude/hooks/check-update.py +0 -132
  141. package/.claude/hooks/detect-interruption.py +0 -165
  142. package/.claude/hooks/enforce-a11y-audit.py +0 -202
  143. package/.claude/hooks/enforce-brand-guide.py +0 -241
  144. package/.claude/hooks/enforce-component-type-confirm.py +0 -97
  145. package/.claude/hooks/enforce-freshness.py +0 -184
  146. package/.claude/hooks/enforce-page-components.py +0 -186
  147. package/.claude/hooks/enforce-page-data-schema.py +0 -155
  148. package/.claude/hooks/enforce-questions-sourced.py +0 -146
  149. package/.claude/hooks/enforce-schema-from-interview.py +0 -248
  150. package/.claude/hooks/enforce-ui-disambiguation.py +0 -108
  151. package/.claude/hooks/enforce-ui-interview.py +0 -130
  152. package/.claude/hooks/generate-manifest-entry.py +0 -1161
  153. package/.claude/hooks/lib/__init__.py +0 -1
  154. package/.claude/hooks/lib/greptile.py +0 -355
  155. package/.claude/hooks/lib/ntfy.py +0 -209
  156. package/.claude/hooks/notify-input-needed.py +0 -73
  157. package/.claude/hooks/notify-phase-complete.py +0 -90
  158. package/.claude/hooks/ntfy-on-question.py +0 -240
  159. package/.claude/hooks/orchestrator-completion.py +0 -313
  160. package/.claude/hooks/orchestrator-handoff.py +0 -267
  161. package/.claude/hooks/orchestrator-session-startup.py +0 -146
  162. package/.claude/hooks/run-code-review.py +0 -393
  163. package/.claude/hooks/session-logger.py +0 -323
  164. package/.claude/hooks/test-orchestrator-reground.py +0 -248
  165. package/.claude/hooks/track-scope-coverage.py +0 -220
  166. package/.claude/hooks/track-token-usage.py +0 -121
  167. package/.claude/hooks/update-api-showcase.py +0 -161
  168. package/.claude/hooks/update-registry.py +0 -352
  169. package/.claude/hooks/update-ui-showcase.py +0 -224
  170. package/.claude/test-auto-answer-bot.py +0 -183
  171. package/.claude/test-completion-detector.py +0 -263
  172. package/.claude/test-orchestrator-state.json +0 -20
  173. package/.claude/test-orchestrator.sh +0 -271
  174. /package/{.claude/commands → commands}/hustle-build.md +0 -0
  175. /package/{.claude/hooks → hooks}/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  176. /package/{.claude/hooks → hooks}/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  177. /package/{.claude/hooks → hooks}/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  178. /package/{.claude/hooks → hooks}/project-document-prompt.py +0 -0
  179. /package/{.claude/hooks → hooks}/remote-question-proxy.py +0 -0
  180. /package/{.claude/hooks → hooks}/update-testing-checklist.py +0 -0
@@ -1,184 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Hook: PreToolUse (Write|Edit)
4
- Purpose: Enforce research freshness for the active endpoint
5
-
6
- This hook blocks Write/Edit operations if:
7
- 1. There is an active endpoint in api-dev-state.json
8
- 2. Research exists for that endpoint
9
- 3. Research is older than 7 days (configurable)
10
-
11
- The user can:
12
- - Run /hustle-api-research to refresh the research
13
- - Set "enforce_freshness": false in the endpoint config to disable
14
- - Research is only enforced for the ACTIVE endpoint
15
-
16
- Exit Codes:
17
- - 0: Continue (no active endpoint, research is fresh, or enforcement disabled)
18
- - 2: Block with message (research is stale, requires re-research)
19
-
20
- Added in v3.7.0:
21
- - User requested enforcement (not just warning) for stale research
22
- - Only enforces for the active endpoint being worked on
23
- """
24
- import json
25
- import sys
26
- import os
27
- from datetime import datetime
28
- from pathlib import Path
29
-
30
- # State file is in .claude/ directory (sibling to hooks/)
31
- STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
32
- RESEARCH_INDEX = Path(__file__).parent.parent / "research" / "index.json"
33
-
34
- # Default freshness threshold (days)
35
- FRESHNESS_THRESHOLD_DAYS = 7
36
-
37
-
38
- def get_active_endpoint(state):
39
- """Get active endpoint - supports both old and new state formats."""
40
- if "endpoints" in state and "active_endpoint" in state:
41
- active = state.get("active_endpoint")
42
- if active and active in state["endpoints"]:
43
- return active, state["endpoints"][active]
44
- return None, None
45
-
46
- # Old format
47
- endpoint = state.get("endpoint")
48
- if endpoint:
49
- return endpoint, state
50
-
51
- return None, None
52
-
53
-
54
- def load_research_index():
55
- """Load research index from .claude/research/index.json file."""
56
- if not RESEARCH_INDEX.exists():
57
- return {}
58
- try:
59
- index = json.loads(RESEARCH_INDEX.read_text())
60
- return index.get("apis", {})
61
- except (json.JSONDecodeError, IOError):
62
- return {}
63
-
64
-
65
- def calculate_days_old(timestamp_str):
66
- """Calculate how many days old a timestamp is."""
67
- if not timestamp_str:
68
- return 0
69
- try:
70
- last_updated = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
71
- now = datetime.now(last_updated.tzinfo) if last_updated.tzinfo else datetime.now()
72
- return (now - last_updated).days
73
- except (ValueError, TypeError):
74
- return 0
75
-
76
-
77
- def is_api_related_file(file_path):
78
- """Check if the file being written is API-related."""
79
- if not file_path:
80
- return False
81
-
82
- file_path = file_path.lower()
83
-
84
- # Files that indicate API development
85
- api_indicators = [
86
- '/api/',
87
- '/route.ts',
88
- '/route.js',
89
- '.api.test.',
90
- '/schemas/',
91
- 'api-tests-manifest',
92
- '/v2/'
93
- ]
94
-
95
- return any(indicator in file_path for indicator in api_indicators)
96
-
97
-
98
- def main():
99
- # Read hook input from stdin
100
- try:
101
- input_data = json.load(sys.stdin)
102
- except json.JSONDecodeError:
103
- input_data = {}
104
-
105
- # Get the file being written (if applicable)
106
- tool_input = input_data.get("toolInput", {})
107
- file_path = tool_input.get("file_path", "")
108
-
109
- # Only enforce for API-related files
110
- if not is_api_related_file(file_path):
111
- print(json.dumps({"continue": True}))
112
- sys.exit(0)
113
-
114
- # Check if state file exists
115
- if not STATE_FILE.exists():
116
- print(json.dumps({"continue": True}))
117
- sys.exit(0)
118
-
119
- try:
120
- state = json.loads(STATE_FILE.read_text())
121
- except json.JSONDecodeError:
122
- print(json.dumps({"continue": True}))
123
- sys.exit(0)
124
-
125
- # Get active endpoint
126
- endpoint, endpoint_data = get_active_endpoint(state)
127
- if not endpoint or not endpoint_data:
128
- # No active endpoint - allow
129
- print(json.dumps({"continue": True}))
130
- sys.exit(0)
131
-
132
- # Check if freshness enforcement is disabled for this endpoint
133
- if endpoint_data.get("enforce_freshness") is False:
134
- print(json.dumps({"continue": True}))
135
- sys.exit(0)
136
-
137
- # Check research freshness
138
- research_index = load_research_index()
139
-
140
- if endpoint not in research_index:
141
- # No research indexed yet - allow but note this is caught by enforce-research.py
142
- print(json.dumps({"continue": True}))
143
- sys.exit(0)
144
-
145
- entry = research_index[endpoint]
146
- last_updated = entry.get("last_updated", "")
147
- days_old = calculate_days_old(last_updated)
148
-
149
- # Get custom threshold if set
150
- threshold = endpoint_data.get("freshness_threshold_days", FRESHNESS_THRESHOLD_DAYS)
151
-
152
- if days_old > threshold:
153
- # Research is stale - block and require re-research
154
- output = {
155
- "decision": "block",
156
- "reason": f"""🔄 STALE RESEARCH DETECTED
157
-
158
- Research for '{endpoint}' is {days_old} days old (threshold: {threshold} days).
159
-
160
- **Action Required:**
161
- Run `/hustle-api-research {endpoint}` to refresh the research before continuing.
162
-
163
- **Why This Matters:**
164
- - API documentation may have changed
165
- - New parameters or features may be available
166
- - Breaking changes may have been introduced
167
- - Your implementation may not match current docs
168
-
169
- **To Skip (Not Recommended):**
170
- Set `"enforce_freshness": false` in api-dev-state.json for this endpoint.
171
-
172
- Last researched: {last_updated or 'Unknown'}
173
- Research location: .claude/research/{endpoint}/CURRENT.md"""
174
- }
175
- print(json.dumps(output))
176
- sys.exit(2) # Exit code 2 = block with message
177
-
178
- # Research is fresh - continue
179
- print(json.dumps({"continue": True}))
180
- sys.exit(0)
181
-
182
-
183
- if __name__ == "__main__":
184
- main()
@@ -1,186 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Hook: enforce-page-components.py
4
- Trigger: PreToolUse (Write|Edit)
5
- Purpose: Check that components from registry are considered before creating new ones
6
-
7
- For ui-create-page workflow, ensures Phase 5 (PAGE ANALYSIS) is complete and
8
- encourages reuse of existing components from the registry.
9
- """
10
-
11
- import json
12
- import sys
13
- import os
14
- import re
15
-
16
- def load_state():
17
- """Load the api-dev-state.json file"""
18
- state_paths = [
19
- ".claude/api-dev-state.json",
20
- os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", ""), ".claude/api-dev-state.json")
21
- ]
22
-
23
- for path in state_paths:
24
- if os.path.exists(path):
25
- with open(path, 'r') as f:
26
- return json.load(f)
27
- return None
28
-
29
- def load_registry():
30
- """Load the registry.json file"""
31
- registry_paths = [
32
- ".claude/registry.json",
33
- os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", ""), ".claude/registry.json")
34
- ]
35
-
36
- for path in registry_paths:
37
- if os.path.exists(path):
38
- with open(path, 'r') as f:
39
- return json.load(f)
40
- return {}
41
-
42
- def is_page_workflow(state):
43
- """Check if current workflow is ui-create-page"""
44
- workflow = state.get("workflow", "")
45
- return workflow == "ui-create-page"
46
-
47
- def get_active_element(state):
48
- """Get the active element being worked on"""
49
- active = state.get("active_element", "")
50
- if not active:
51
- active = state.get("endpoint", "")
52
- return active
53
-
54
- def is_creating_new_component(file_path):
55
- """Check if the file path suggests creating a new standalone component"""
56
- if not file_path:
57
- return False
58
-
59
- # Patterns that suggest a new standalone component (not page-specific)
60
- standalone_patterns = [
61
- r"src/components/[A-Z]",
62
- r"components/ui/",
63
- r"components/shared/",
64
- ]
65
-
66
- return any(re.search(pattern, file_path) for pattern in standalone_patterns)
67
-
68
- def is_page_specific_component(file_path, element_name):
69
- """Check if the file is a page-specific component (allowed)"""
70
- if not file_path or not element_name:
71
- return False
72
-
73
- # Page-specific components in _components folder are allowed
74
- patterns = [
75
- f"src/app/{element_name}/_components/",
76
- f"app/{element_name}/_components/",
77
- ]
78
-
79
- return any(pattern in file_path for pattern in patterns)
80
-
81
- def check_page_analysis_phase(state, element_name):
82
- """Check if page analysis phase is complete"""
83
- elements = state.get("elements", {})
84
- element = elements.get(element_name, {})
85
- phases = element.get("phases", {})
86
-
87
- page_analysis = phases.get("page_analysis", {})
88
- return page_analysis.get("status") == "complete"
89
-
90
- def get_available_components(registry):
91
- """Get list of available components from registry"""
92
- components = registry.get("components", {})
93
- return list(components.keys())
94
-
95
- def main():
96
- try:
97
- # Read tool input from stdin
98
- input_data = json.loads(sys.stdin.read())
99
- tool_name = input_data.get("tool_name", "")
100
- tool_input = input_data.get("tool_input", {})
101
-
102
- # Only check Write tool
103
- if tool_name != "Write":
104
- print(json.dumps({"decision": "allow"}))
105
- return
106
-
107
- file_path = tool_input.get("file_path", "")
108
-
109
- # Load state
110
- state = load_state()
111
- if not state:
112
- print(json.dumps({"decision": "allow"}))
113
- return
114
-
115
- # Only apply to ui-create-page workflow
116
- if not is_page_workflow(state):
117
- print(json.dumps({"decision": "allow"}))
118
- return
119
-
120
- element_name = get_active_element(state)
121
- if not element_name:
122
- print(json.dumps({"decision": "allow"}))
123
- return
124
-
125
- # Allow page-specific components (in _components folder)
126
- if is_page_specific_component(file_path, element_name):
127
- print(json.dumps({"decision": "allow"}))
128
- return
129
-
130
- # Check if creating a new standalone component
131
- if is_creating_new_component(file_path):
132
- # Check if page analysis phase is complete
133
- if not check_page_analysis_phase(state, element_name):
134
- # Load registry to show available components
135
- registry = load_registry()
136
- available = get_available_components(registry)
137
-
138
- component_list = "\n".join([f" - {c}" for c in available[:10]])
139
- if len(available) > 10:
140
- component_list += f"\n ... and {len(available) - 10} more"
141
-
142
- print(json.dumps({
143
- "decision": "block",
144
- "reason": f"""
145
- PAGE ANALYSIS REQUIRED (Phase 5)
146
-
147
- You are creating a new standalone component, but Page Analysis phase is not complete.
148
-
149
- Before creating new components:
150
- 1. Check the registry for existing components
151
- 2. Decide which existing components to reuse
152
- 3. Update state: phases.page_analysis.status = "complete"
153
-
154
- Available Components in Registry:
155
- {component_list if available else " (No components registered yet)"}
156
-
157
- If you need a NEW component, consider:
158
- - Using /ui-create to properly create and document it
159
- - Or create a page-specific component in src/app/{element_name}/_components/
160
- """
161
- }))
162
- return
163
-
164
- # Even if phase is complete, notify about registry
165
- registry = load_registry()
166
- available = get_available_components(registry)
167
-
168
- if available:
169
- print(json.dumps({
170
- "decision": "allow",
171
- "message": f"Note: {len(available)} components available in registry. Consider reusing existing components."
172
- }))
173
- return
174
-
175
- # Allow everything else
176
- print(json.dumps({"decision": "allow"}))
177
-
178
- except Exception as e:
179
- # On error, allow to avoid blocking workflow
180
- print(json.dumps({
181
- "decision": "allow",
182
- "error": str(e)
183
- }))
184
-
185
- if __name__ == "__main__":
186
- main()
@@ -1,155 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Hook: enforce-page-data-schema.py
4
- Trigger: PreToolUse (Write|Edit)
5
- Purpose: Validate that API response types are defined before page implementation
6
-
7
- For ui-create-page workflow, ensures Phase 6 (DATA SCHEMA) is complete before
8
- allowing page implementation in Phase 9.
9
- """
10
-
11
- import json
12
- import sys
13
- import os
14
- import re
15
-
16
- def load_state():
17
- """Load the api-dev-state.json file"""
18
- state_paths = [
19
- ".claude/api-dev-state.json",
20
- os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", ""), ".claude/api-dev-state.json")
21
- ]
22
-
23
- for path in state_paths:
24
- if os.path.exists(path):
25
- with open(path, 'r') as f:
26
- return json.load(f)
27
- return None
28
-
29
- def is_page_workflow(state):
30
- """Check if current workflow is ui-create-page"""
31
- workflow = state.get("workflow", "")
32
- return workflow == "ui-create-page"
33
-
34
- def get_active_element(state):
35
- """Get the active element being worked on"""
36
- active = state.get("active_element", "")
37
- if not active:
38
- # Fall back to endpoint for older state files
39
- active = state.get("endpoint", "")
40
- return active
41
-
42
- def is_page_file(file_path, element_name):
43
- """Check if the file being written is a page implementation file"""
44
- if not file_path or not element_name:
45
- return False
46
-
47
- patterns = [
48
- f"src/app/{element_name}/page.tsx",
49
- f"src/app/{element_name}/layout.tsx",
50
- f"src/app/{element_name}/_components/",
51
- f"app/{element_name}/page.tsx",
52
- ]
53
-
54
- return any(pattern in file_path for pattern in patterns)
55
-
56
- def is_types_file(file_path, element_name):
57
- """Check if the file being written is the types/schema file"""
58
- if not file_path or not element_name:
59
- return False
60
-
61
- patterns = [
62
- f"src/app/{element_name}/_types/",
63
- f"src/app/{element_name}/types.ts",
64
- f"src/lib/schemas/{element_name}",
65
- ]
66
-
67
- return any(pattern in file_path for pattern in patterns)
68
-
69
- def is_test_file(file_path):
70
- """Check if file is a test file"""
71
- return "__tests__" in file_path or ".test." in file_path or ".spec." in file_path
72
-
73
- def check_data_schema_phase(state, element_name):
74
- """Check if data schema phase is complete"""
75
- elements = state.get("elements", {})
76
- element = elements.get(element_name, {})
77
- phases = element.get("phases", {})
78
-
79
- # Check data_schema phase
80
- data_schema = phases.get("data_schema", {})
81
- return data_schema.get("status") == "complete"
82
-
83
- def main():
84
- try:
85
- # Read tool input from stdin
86
- input_data = json.loads(sys.stdin.read())
87
- tool_name = input_data.get("tool_name", "")
88
- tool_input = input_data.get("tool_input", {})
89
-
90
- # Only check Write and Edit tools
91
- if tool_name not in ["Write", "Edit"]:
92
- print(json.dumps({"decision": "allow"}))
93
- return
94
-
95
- file_path = tool_input.get("file_path", "")
96
-
97
- # Load state
98
- state = load_state()
99
- if not state:
100
- print(json.dumps({"decision": "allow"}))
101
- return
102
-
103
- # Only apply to ui-create-page workflow
104
- if not is_page_workflow(state):
105
- print(json.dumps({"decision": "allow"}))
106
- return
107
-
108
- element_name = get_active_element(state)
109
- if not element_name:
110
- print(json.dumps({"decision": "allow"}))
111
- return
112
-
113
- # Allow writing types/schema files (Phase 6)
114
- if is_types_file(file_path, element_name):
115
- print(json.dumps({"decision": "allow"}))
116
- return
117
-
118
- # Allow writing test files (Phase 8)
119
- if is_test_file(file_path):
120
- print(json.dumps({"decision": "allow"}))
121
- return
122
-
123
- # Check if writing page implementation file
124
- if is_page_file(file_path, element_name):
125
- # Verify data schema phase is complete
126
- if not check_data_schema_phase(state, element_name):
127
- print(json.dumps({
128
- "decision": "block",
129
- "reason": f"""
130
- DATA SCHEMA REQUIRED (Phase 6)
131
-
132
- You are trying to implement page code, but the data schema phase is not complete.
133
-
134
- Before writing page implementation:
135
- 1. Define TypeScript interfaces for API responses
136
- 2. Create types in src/app/{element_name}/_types/index.ts
137
- 3. Update state: phases.data_schema.status = "complete"
138
-
139
- Page implementation requires knowing the data structure first.
140
- """
141
- }))
142
- return
143
-
144
- # Allow everything else
145
- print(json.dumps({"decision": "allow"}))
146
-
147
- except Exception as e:
148
- # On error, allow to avoid blocking workflow
149
- print(json.dumps({
150
- "decision": "allow",
151
- "error": str(e)
152
- }))
153
-
154
- if __name__ == "__main__":
155
- main()
@@ -1,146 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Hook: PreToolUse for AskUserQuestion
4
- Purpose: Validate interview questions come from research, not templates
5
-
6
- This hook ensures that questions asked during the interview phase are
7
- generated from actual research findings, not generic template questions.
8
-
9
- Added in v3.6.7 for question quality enforcement.
10
-
11
- Returns:
12
- - {"permissionDecision": "allow"} - Question is properly sourced
13
- - {"permissionDecision": "allow", "message": "..."} - Allow with reminder
14
- """
15
- import json
16
- import sys
17
- from pathlib import Path
18
-
19
- STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
20
-
21
-
22
- def get_active_endpoint(state):
23
- """Get active endpoint - supports both old and new state formats."""
24
- if "endpoints" in state and "active_endpoint" in state:
25
- active = state.get("active_endpoint")
26
- if active and active in state["endpoints"]:
27
- return active, state["endpoints"][active]
28
- return None, None
29
-
30
- endpoint = state.get("endpoint")
31
- if endpoint:
32
- return endpoint, state
33
-
34
- return None, None
35
-
36
-
37
- def get_research_keywords(state, endpoint_data):
38
- """Extract keywords from research that should appear in questions."""
39
- keywords = set()
40
-
41
- # From research queries
42
- for query in state.get("research_queries", []):
43
- q = query.get("query", "")
44
- # Extract meaningful words (length > 3)
45
- words = [w.lower() for w in q.split() if len(w) > 3]
46
- keywords.update(words)
47
-
48
- # From initial research sources
49
- initial = endpoint_data.get("phases", {}).get("research_initial", {})
50
- for src in initial.get("sources", []):
51
- if isinstance(src, dict):
52
- summary = src.get("summary", "")
53
- words = [w.lower() for w in summary.split() if len(w) > 3]
54
- keywords.update(words)
55
-
56
- # From deep research sources
57
- deep = endpoint_data.get("phases", {}).get("research_deep", {})
58
- for src in deep.get("sources", []):
59
- if isinstance(src, dict):
60
- summary = src.get("summary", "")
61
- words = [w.lower() for w in summary.split() if len(w) > 3]
62
- keywords.update(words)
63
-
64
- return keywords
65
-
66
-
67
- def main():
68
- try:
69
- input_data = json.load(sys.stdin)
70
- except json.JSONDecodeError:
71
- print(json.dumps({"permissionDecision": "allow"}))
72
- sys.exit(0)
73
-
74
- tool_name = input_data.get("tool_name", "")
75
- tool_input = input_data.get("tool_input", {})
76
-
77
- if tool_name != "AskUserQuestion":
78
- print(json.dumps({"permissionDecision": "allow"}))
79
- sys.exit(0)
80
-
81
- if not STATE_FILE.exists():
82
- print(json.dumps({"permissionDecision": "allow"}))
83
- sys.exit(0)
84
-
85
- try:
86
- state = json.loads(STATE_FILE.read_text())
87
- except json.JSONDecodeError:
88
- print(json.dumps({"permissionDecision": "allow"}))
89
- sys.exit(0)
90
-
91
- endpoint, endpoint_data = get_active_endpoint(state)
92
- if not endpoint or not endpoint_data:
93
- print(json.dumps({"permissionDecision": "allow"}))
94
- sys.exit(0)
95
-
96
- # Only enforce during interview phase
97
- interview = endpoint_data.get("phases", {}).get("interview", {})
98
- if interview.get("status") != "in_progress":
99
- print(json.dumps({"permissionDecision": "allow"}))
100
- sys.exit(0)
101
-
102
- # Check if research has been done
103
- initial = endpoint_data.get("phases", {}).get("research_initial", {})
104
- if initial.get("status") != "complete":
105
- # Allow question but remind to do research first
106
- print(json.dumps({
107
- "permissionDecision": "allow",
108
- "message": "REMINDER: Initial research (Phase 3) should be complete before interview. Questions should be generated FROM research findings."
109
- }))
110
- sys.exit(0)
111
-
112
- # Get the question being asked
113
- question = tool_input.get("question", "")
114
-
115
- # Get research keywords
116
- keywords = get_research_keywords(state, endpoint_data)
117
-
118
- # Check if question contains any research-derived terms
119
- question_lower = question.lower()
120
- found_keywords = [k for k in keywords if k in question_lower]
121
-
122
- if not found_keywords and len(keywords) > 5:
123
- # No research keywords found - this might be a generic question
124
- print(json.dumps({
125
- "permissionDecision": "allow",
126
- "message": f"""NOTE: This question doesn't appear to reference terms discovered in research.
127
-
128
- Research-derived terms include: {', '.join(list(keywords)[:10])}...
129
-
130
- BEST PRACTICE: Interview questions should be generated FROM research findings.
131
- Example: "I discovered the API supports [feature]. Do you want to implement this?"
132
-
133
- Proceeding anyway, but consider revising the question."""
134
- }))
135
- sys.exit(0)
136
-
137
- # Question looks good
138
- print(json.dumps({
139
- "permissionDecision": "allow",
140
- "message": f"Question references research terms: {', '.join(found_keywords[:5])}"
141
- }))
142
- sys.exit(0)
143
-
144
-
145
- if __name__ == "__main__":
146
- main()