@hustle-together/api-dev-tools 3.6.4 → 3.9.2

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 (61) hide show
  1. package/README.md +5307 -258
  2. package/bin/cli.js +348 -20
  3. package/commands/README.md +459 -71
  4. package/commands/hustle-api-continue.md +158 -0
  5. package/commands/{api-create.md → hustle-api-create.md} +22 -2
  6. package/commands/{api-env.md → hustle-api-env.md} +4 -4
  7. package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
  8. package/commands/{api-research.md → hustle-api-research.md} +3 -3
  9. package/commands/hustle-api-sessions.md +149 -0
  10. package/commands/{api-status.md → hustle-api-status.md} +16 -16
  11. package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
  12. package/commands/hustle-combine.md +763 -0
  13. package/commands/hustle-ui-create.md +825 -0
  14. package/hooks/api-workflow-check.py +385 -19
  15. package/hooks/cache-research.py +337 -0
  16. package/hooks/check-playwright-setup.py +103 -0
  17. package/hooks/check-storybook-setup.py +81 -0
  18. package/hooks/detect-interruption.py +165 -0
  19. package/hooks/enforce-brand-guide.py +131 -0
  20. package/hooks/enforce-documentation.py +60 -8
  21. package/hooks/enforce-freshness.py +184 -0
  22. package/hooks/enforce-questions-sourced.py +146 -0
  23. package/hooks/enforce-schema-from-interview.py +248 -0
  24. package/hooks/enforce-ui-disambiguation.py +108 -0
  25. package/hooks/enforce-ui-interview.py +130 -0
  26. package/hooks/generate-manifest-entry.py +981 -0
  27. package/hooks/session-logger.py +297 -0
  28. package/hooks/session-startup.py +65 -10
  29. package/hooks/track-scope-coverage.py +220 -0
  30. package/hooks/track-tool-use.py +81 -1
  31. package/hooks/update-api-showcase.py +149 -0
  32. package/hooks/update-registry.py +352 -0
  33. package/hooks/update-ui-showcase.py +148 -0
  34. package/package.json +8 -2
  35. package/templates/BRAND_GUIDE.md +299 -0
  36. package/templates/CLAUDE-SECTION.md +56 -24
  37. package/templates/SPEC.json +640 -0
  38. package/templates/api-dev-state.json +179 -161
  39. package/templates/api-showcase/APICard.tsx +153 -0
  40. package/templates/api-showcase/APIModal.tsx +375 -0
  41. package/templates/api-showcase/APIShowcase.tsx +231 -0
  42. package/templates/api-showcase/APITester.tsx +522 -0
  43. package/templates/api-showcase/page.tsx +41 -0
  44. package/templates/component/Component.stories.tsx +172 -0
  45. package/templates/component/Component.test.tsx +237 -0
  46. package/templates/component/Component.tsx +86 -0
  47. package/templates/component/Component.types.ts +55 -0
  48. package/templates/component/index.ts +15 -0
  49. package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
  50. package/templates/dev-tools/page.tsx +10 -0
  51. package/templates/page/page.e2e.test.ts +218 -0
  52. package/templates/page/page.tsx +42 -0
  53. package/templates/performance-budgets.json +58 -0
  54. package/templates/registry.json +13 -0
  55. package/templates/settings.json +74 -0
  56. package/templates/shared/HeroHeader.tsx +261 -0
  57. package/templates/shared/index.ts +1 -0
  58. package/templates/ui-showcase/PreviewCard.tsx +315 -0
  59. package/templates/ui-showcase/PreviewModal.tsx +676 -0
  60. package/templates/ui-showcase/UIShowcase.tsx +262 -0
  61. package/templates/ui-showcase/page.tsx +26 -0
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PreToolUse for Write/Edit
4
+ Purpose: Inject brand guide content during UI implementation
5
+
6
+ This hook runs before writing component/page files. When use_brand_guide=true
7
+ in the state, it logs the brand guide summary to remind Claude to apply
8
+ consistent branding.
9
+
10
+ Version: 3.9.0
11
+
12
+ Returns:
13
+ - {"continue": true} - Always continues
14
+ - May include "notify" with brand guide summary
15
+ """
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ # State file is in .claude/ directory (sibling to hooks/)
21
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
22
+ BRAND_GUIDE_FILE = Path(__file__).parent.parent / "BRAND_GUIDE.md"
23
+
24
+
25
+ def extract_brand_summary(content):
26
+ """Extract key brand values from brand guide markdown."""
27
+ summary = []
28
+
29
+ lines = content.split("\n")
30
+ current_section = ""
31
+
32
+ for line in lines:
33
+ line = line.strip()
34
+
35
+ # Track section
36
+ if line.startswith("## "):
37
+ current_section = line[3:].lower()
38
+ continue
39
+
40
+ # Extract key values
41
+ if line.startswith("- **") and ":" in line:
42
+ # Parse "- **Key:** Value" format
43
+ try:
44
+ key_part = line.split(":**")[0].replace("- **", "")
45
+ value_part = line.split(":**")[1].strip()
46
+
47
+ # Only include primary brand values
48
+ if current_section == "colors" and key_part in ["Primary", "Accent", "Background"]:
49
+ summary.append(f"{key_part}: {value_part}")
50
+ elif current_section == "typography" and key_part in ["Headings", "Body"]:
51
+ summary.append(f"{key_part}: {value_part}")
52
+ elif current_section == "component styling" and key_part in ["Border Radius", "Focus Ring"]:
53
+ summary.append(f"{key_part}: {value_part}")
54
+ except IndexError:
55
+ continue
56
+
57
+ return summary
58
+
59
+
60
+ def main():
61
+ # Read hook input from stdin
62
+ try:
63
+ input_data = json.load(sys.stdin)
64
+ except json.JSONDecodeError:
65
+ print(json.dumps({"continue": True}))
66
+ sys.exit(0)
67
+
68
+ tool_name = input_data.get("tool_name", "")
69
+ tool_input = input_data.get("tool_input", {})
70
+
71
+ # Only check Write/Edit operations
72
+ if tool_name not in ["Write", "Edit"]:
73
+ print(json.dumps({"continue": True}))
74
+ sys.exit(0)
75
+
76
+ # Check if targeting component or page files
77
+ file_path = tool_input.get("file_path", "")
78
+ is_component = "/components/" in file_path and file_path.endswith(".tsx")
79
+ is_page = "/app/" in file_path and "page.tsx" in file_path
80
+
81
+ if not is_component and not is_page:
82
+ print(json.dumps({"continue": True}))
83
+ sys.exit(0)
84
+
85
+ # Check if state file exists
86
+ if not STATE_FILE.exists():
87
+ print(json.dumps({"continue": True}))
88
+ sys.exit(0)
89
+
90
+ # Load state
91
+ try:
92
+ state = json.loads(STATE_FILE.read_text())
93
+ except json.JSONDecodeError:
94
+ print(json.dumps({"continue": True}))
95
+ sys.exit(0)
96
+
97
+ workflow = state.get("workflow", "")
98
+
99
+ # Only apply for UI workflows
100
+ if workflow not in ["ui-create-component", "ui-create-page"]:
101
+ print(json.dumps({"continue": True}))
102
+ sys.exit(0)
103
+
104
+ # Check if brand guide is enabled
105
+ ui_config = state.get("ui_config", {})
106
+ use_brand_guide = ui_config.get("use_brand_guide", False)
107
+
108
+ if not use_brand_guide:
109
+ print(json.dumps({"continue": True}))
110
+ sys.exit(0)
111
+
112
+ # Check if brand guide file exists
113
+ if not BRAND_GUIDE_FILE.exists():
114
+ print(json.dumps({"continue": True}))
115
+ sys.exit(0)
116
+
117
+ # Extract brand summary
118
+ brand_content = BRAND_GUIDE_FILE.read_text()
119
+ summary = extract_brand_summary(brand_content)
120
+
121
+ if summary:
122
+ notify_msg = "Applying brand guide: " + " | ".join(summary[:5])
123
+ print(json.dumps({"continue": True, "notify": notify_msg}))
124
+ else:
125
+ print(json.dumps({"continue": True}))
126
+
127
+ sys.exit(0)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()
@@ -3,7 +3,7 @@
3
3
  Hook: PreToolUse for Write/Edit (and Stop)
4
4
  Purpose: Block completion until documentation confirmed WITH USER REVIEW
5
5
 
6
- Phase 21 (Documentation) requires:
6
+ Phase 12 (Documentation) requires:
7
7
  1. Update api-tests-manifest.json
8
8
  2. Cache research to .claude/research/
9
9
  3. Update OpenAPI spec if applicable
@@ -14,12 +14,50 @@ Phase 21 (Documentation) requires:
14
14
  Returns:
15
15
  - {"permissionDecision": "allow"} - Let the tool run
16
16
  - {"permissionDecision": "deny", "reason": "..."} - Block with explanation
17
+
18
+ Updated in v3.6.7:
19
+ - Support multi-API state structure
20
+ - Don't block on missing cache files (cache-research.py creates them)
21
+ - Check actual file existence, not just state flags
17
22
  """
18
23
  import json
19
24
  import sys
20
25
  from pathlib import Path
21
26
 
22
27
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
28
+ RESEARCH_DIR = Path(__file__).parent.parent / "research"
29
+
30
+
31
+ def get_active_endpoint(state):
32
+ """Get active endpoint - supports both old and new state formats."""
33
+ # New format (v3.6.7+): endpoints object with active_endpoint pointer
34
+ if "endpoints" in state and "active_endpoint" in state:
35
+ active = state.get("active_endpoint")
36
+ if active and active in state["endpoints"]:
37
+ return active, state["endpoints"][active]
38
+ return None, None
39
+
40
+ # Old format: single endpoint field
41
+ endpoint = state.get("endpoint")
42
+ if endpoint:
43
+ return endpoint, state
44
+
45
+ return None, None
46
+
47
+
48
+ def check_research_cache_exists(endpoint):
49
+ """Check if research cache files actually exist."""
50
+ cache_dir = RESEARCH_DIR / endpoint
51
+ if not cache_dir.exists():
52
+ return False, []
53
+
54
+ expected_files = ["sources.json", "interview.json", "schema.json"]
55
+ existing = []
56
+ for f in expected_files:
57
+ if (cache_dir / f).exists():
58
+ existing.append(f)
59
+
60
+ return len(existing) >= 2, existing # At least 2 of 3 files should exist
23
61
 
24
62
 
25
63
  def main():
@@ -54,8 +92,13 @@ def main():
54
92
  print(json.dumps({"permissionDecision": "allow"}))
55
93
  sys.exit(0)
56
94
 
57
- endpoint = state.get("endpoint", "unknown")
58
- phases = state.get("phases", {})
95
+ # Get active endpoint (supports both old and new formats)
96
+ endpoint, endpoint_data = get_active_endpoint(state)
97
+ if not endpoint or not endpoint_data:
98
+ print(json.dumps({"permissionDecision": "allow"}))
99
+ sys.exit(0)
100
+
101
+ phases = endpoint_data.get("phases", {})
59
102
  tdd_refactor = phases.get("tdd_refactor", {})
60
103
  documentation = phases.get("documentation", {})
61
104
 
@@ -74,14 +117,19 @@ def main():
74
117
  user_confirmed = documentation.get("user_confirmed", False)
75
118
  checklist_shown = documentation.get("checklist_shown", False)
76
119
  manifest_updated = documentation.get("manifest_updated", False)
77
- research_cached = documentation.get("research_cached", False)
78
120
  openapi_updated = documentation.get("openapi_updated", False)
79
121
 
122
+ # v3.6.7: Check actual file existence for research cache
123
+ # (cache-research.py PostToolUse hook creates these files automatically)
124
+ cache_exists, cache_files = check_research_cache_exists(endpoint)
125
+ research_cached = cache_exists or documentation.get("research_cached", False)
126
+
80
127
  missing = []
81
128
  if not manifest_updated:
82
129
  missing.append("api-tests-manifest.json not updated")
83
130
  if not research_cached:
84
- missing.append("Research not cached to .claude/research/")
131
+ # Don't block - cache-research.py will create files when docs are written
132
+ missing.append(f"Research cache pending (will be created automatically)")
85
133
  if not checklist_shown:
86
134
  missing.append("Documentation checklist not shown to user")
87
135
  if not user_question_asked:
@@ -93,7 +141,7 @@ def main():
93
141
 
94
142
  print(json.dumps({
95
143
  "permissionDecision": "deny",
96
- "reason": f"""❌ BLOCKED: Documentation (Phase 21) not complete.
144
+ "reason": f"""❌ BLOCKED: Documentation (Phase 12) not complete.
97
145
 
98
146
  Status: {status}
99
147
  Manifest updated: {manifest_updated}
@@ -176,12 +224,16 @@ WHY: Documentation ensures next developer (or future Claude) has context."""
176
224
  }))
177
225
  sys.exit(0)
178
226
 
179
- # Documentation complete
227
+ # Documentation complete - check actual file existence for status
228
+ cache_exists, cache_files = check_research_cache_exists(endpoint)
229
+ manifest_updated = documentation.get("manifest_updated", False)
230
+ openapi_updated = documentation.get("openapi_updated", False)
231
+
180
232
  print(json.dumps({
181
233
  "permissionDecision": "allow",
182
234
  "message": f"""✅ Documentation complete for {endpoint}.
183
235
  Manifest updated: {manifest_updated}
184
- Research cached: {research_cached}
236
+ Research cached: {cache_exists} ({', '.join(cache_files) if cache_files else 'no files'})
185
237
  OpenAPI updated: {openapi_updated}
186
238
  User confirmed documentation is complete."""
187
239
  }))
@@ -0,0 +1,184 @@
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()
@@ -0,0 +1,146 @@
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()