@hustle-together/api-dev-tools 3.10.1 → 3.12.1

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 (178) hide show
  1. package/.claude/agents/code-reviewer.md +170 -0
  2. package/.claude/agents/docs-generator.md +80 -0
  3. package/.claude/agents/implementation-reviewer.md +119 -0
  4. package/.claude/agents/parallel-researcher.md +52 -0
  5. package/.claude/agents/research-validator.md +116 -0
  6. package/.claude/agents/schema-generator.md +70 -0
  7. package/.claude/agents/test-writer.md +104 -0
  8. package/.claude/api-dev-state.json +331 -0
  9. package/.claude/commands/README.md +196 -0
  10. package/.claude/commands/add-command.md +212 -0
  11. package/.claude/commands/api-create.md +510 -0
  12. package/.claude/commands/api-env.md +51 -0
  13. package/.claude/commands/api-interview.md +344 -0
  14. package/.claude/commands/api-research.md +357 -0
  15. package/.claude/commands/api-status.md +279 -0
  16. package/.claude/commands/api-verify.md +232 -0
  17. package/.claude/commands/beepboop.md +96 -0
  18. package/.claude/commands/busycommit.md +111 -0
  19. package/.claude/commands/commit.md +82 -0
  20. package/.claude/commands/cycle.md +137 -0
  21. package/.claude/commands/gap.md +85 -0
  22. package/.claude/commands/green.md +137 -0
  23. package/.claude/commands/issue.md +187 -0
  24. package/.claude/commands/ntfy-setup.md +91 -0
  25. package/.claude/commands/ntfy-test.md +74 -0
  26. package/.claude/commands/plan.md +167 -0
  27. package/.claude/commands/pr.md +121 -0
  28. package/.claude/commands/publish.md +40 -0
  29. package/.claude/commands/red.md +137 -0
  30. package/.claude/commands/refactor.md +137 -0
  31. package/.claude/commands/spike.md +137 -0
  32. package/.claude/commands/summarize.md +93 -0
  33. package/.claude/commands/tdd.md +139 -0
  34. package/.claude/commands/worktree-add.md +307 -0
  35. package/.claude/commands/worktree-cleanup.md +275 -0
  36. package/.claude/hooks/api-workflow-check.py +227 -0
  37. package/.claude/hooks/enforce-deep-research.py +185 -0
  38. package/.claude/hooks/enforce-disambiguation.py +155 -0
  39. package/.claude/hooks/enforce-documentation.py +192 -0
  40. package/.claude/hooks/enforce-environment.py +253 -0
  41. package/.claude/hooks/enforce-external-research.py +328 -0
  42. package/.claude/hooks/enforce-interview.py +421 -0
  43. package/.claude/hooks/enforce-refactor.py +189 -0
  44. package/.claude/hooks/enforce-research.py +159 -0
  45. package/.claude/hooks/enforce-schema.py +186 -0
  46. package/.claude/hooks/enforce-scope.py +160 -0
  47. package/.claude/hooks/enforce-tdd-red.py +250 -0
  48. package/.claude/hooks/enforce-verify.py +186 -0
  49. package/.claude/hooks/periodic-reground.py +154 -0
  50. package/.claude/hooks/session-startup.py +151 -0
  51. package/.claude/hooks/track-tool-use.py +626 -0
  52. package/.claude/hooks/verify-after-green.py +282 -0
  53. package/.claude/hooks/verify-implementation.py +225 -0
  54. package/.claude/research/index.json +6 -0
  55. package/.claude/settings.json +144 -0
  56. package/.claude/settings.local.json +12 -0
  57. package/.claude-plugin/marketplace.json +103 -0
  58. package/.skills/README.md +293 -0
  59. package/.skills/_shared/convert-commands.py +192 -0
  60. package/.skills/_shared/hooks/api-workflow-check.py +227 -0
  61. package/.skills/_shared/hooks/enforce-deep-research.py +185 -0
  62. package/.skills/_shared/hooks/enforce-disambiguation.py +155 -0
  63. package/.skills/_shared/hooks/enforce-documentation.py +192 -0
  64. package/.skills/_shared/hooks/enforce-environment.py +253 -0
  65. package/.skills/_shared/hooks/enforce-external-research.py +328 -0
  66. package/.skills/_shared/hooks/enforce-interview.py +421 -0
  67. package/.skills/_shared/hooks/enforce-refactor.py +189 -0
  68. package/.skills/_shared/hooks/enforce-research.py +159 -0
  69. package/.skills/_shared/hooks/enforce-schema.py +186 -0
  70. package/.skills/_shared/hooks/enforce-scope.py +160 -0
  71. package/.skills/_shared/hooks/enforce-tdd-red.py +250 -0
  72. package/.skills/_shared/hooks/enforce-verify.py +186 -0
  73. package/.skills/_shared/hooks/periodic-reground.py +154 -0
  74. package/.skills/_shared/hooks/session-startup.py +151 -0
  75. package/.skills/_shared/hooks/track-tool-use.py +626 -0
  76. package/.skills/_shared/hooks/verify-after-green.py +282 -0
  77. package/.skills/_shared/hooks/verify-implementation.py +225 -0
  78. package/.skills/_shared/install.sh +114 -0
  79. package/.skills/_shared/settings.json +93 -0
  80. package/.skills/add-command/SKILL.md +227 -0
  81. package/.skills/api-create/SKILL.md +623 -0
  82. package/.skills/api-env/SKILL.md +64 -0
  83. package/.skills/api-interview/SKILL.md +357 -0
  84. package/.skills/api-research/SKILL.md +370 -0
  85. package/.skills/api-status/SKILL.md +292 -0
  86. package/.skills/api-verify/SKILL.md +245 -0
  87. package/.skills/beepboop/SKILL.md +111 -0
  88. package/.skills/busycommit/SKILL.md +126 -0
  89. package/.skills/commit/SKILL.md +97 -0
  90. package/.skills/cycle/SKILL.md +152 -0
  91. package/.skills/gap/SKILL.md +100 -0
  92. package/.skills/green/SKILL.md +152 -0
  93. package/.skills/issue/SKILL.md +202 -0
  94. package/.skills/plan/SKILL.md +182 -0
  95. package/.skills/pr/SKILL.md +136 -0
  96. package/.skills/publish/SKILL.md +160 -0
  97. package/.skills/red/SKILL.md +152 -0
  98. package/.skills/refactor/SKILL.md +152 -0
  99. package/.skills/spike/SKILL.md +152 -0
  100. package/.skills/summarize/SKILL.md +108 -0
  101. package/.skills/tdd/SKILL.md +154 -0
  102. package/.skills/update-todos/SKILL.md +250 -0
  103. package/.skills/worktree-add/SKILL.md +322 -0
  104. package/.skills/worktree-cleanup/SKILL.md +290 -0
  105. package/CHANGELOG.md +115 -0
  106. package/README.md +161 -7101
  107. package/bin/cli.js +448 -805
  108. package/commands/README.md +66 -31
  109. package/commands/add-command.md +8 -5
  110. package/commands/beepboop.md +4 -5
  111. package/commands/busycommit.md +2 -3
  112. package/commands/commit.md +2 -3
  113. package/commands/cycle.md +2 -7
  114. package/commands/gap.md +2 -3
  115. package/commands/green.md +2 -7
  116. package/commands/hustle-api-continue.md +8 -5
  117. package/commands/hustle-api-create.md +70 -29
  118. package/commands/hustle-api-env.md +1 -0
  119. package/commands/hustle-api-interview.md +32 -19
  120. package/commands/hustle-api-research.md +47 -21
  121. package/commands/hustle-api-sessions.md +8 -7
  122. package/commands/hustle-api-status.md +21 -1
  123. package/commands/hustle-api-verify.md +14 -13
  124. package/commands/hustle-combine.md +488 -241
  125. package/commands/hustle-ui-create-page.md +113 -50
  126. package/commands/hustle-ui-create.md +179 -26
  127. package/commands/issue.md +3 -8
  128. package/commands/plan.md +2 -3
  129. package/commands/pr.md +2 -3
  130. package/commands/red.md +2 -7
  131. package/commands/refactor.md +2 -7
  132. package/commands/spike.md +2 -7
  133. package/commands/summarize.md +2 -3
  134. package/commands/tdd.md +2 -7
  135. package/commands/worktree-add.md +208 -216
  136. package/commands/worktree-cleanup.md +172 -178
  137. package/hooks/api-workflow-check.py +5 -3
  138. package/hooks/enforce-component-type-confirm.py +97 -0
  139. package/hooks/lib/__init__.py +1 -0
  140. package/hooks/lib/greptile.py +355 -0
  141. package/hooks/lib/ntfy.py +209 -0
  142. package/hooks/notify-input-needed.py +73 -0
  143. package/hooks/notify-phase-complete.py +90 -0
  144. package/hooks/run-code-review.py +246 -0
  145. package/hooks/track-token-usage.py +121 -0
  146. package/package.json +33 -12
  147. package/scripts/collect-test-results.ts +102 -77
  148. package/scripts/extract-parameters.ts +112 -70
  149. package/scripts/generate-test-manifest.ts +118 -77
  150. package/templates/.env.example +57 -0
  151. package/templates/BRAND_GUIDE.md +92 -52
  152. package/templates/CLAUDE-SECTION.md +40 -37
  153. package/templates/SPEC.json +186 -38
  154. package/templates/api-dev-state.json +33 -4
  155. package/templates/api-showcase/_components/APICard.tsx +22 -18
  156. package/templates/api-showcase/_components/APIModal.tsx +110 -64
  157. package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
  158. package/templates/api-showcase/_components/APITester.tsx +128 -67
  159. package/templates/api-showcase/page.tsx +4 -4
  160. package/templates/api-test/page.tsx +51 -30
  161. package/templates/api-test/test-structure/route.ts +43 -34
  162. package/templates/component/Component.stories.tsx +41 -39
  163. package/templates/component/Component.test.tsx +96 -78
  164. package/templates/component/Component.tsx +63 -52
  165. package/templates/component/Component.types.ts +10 -6
  166. package/templates/component/Component.visual.spec.ts +170 -0
  167. package/templates/component/index.ts +2 -2
  168. package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
  169. package/templates/dev-tools/page.tsx +4 -3
  170. package/templates/mcp-servers.json +30 -2
  171. package/templates/page/page.e2e.test.ts +56 -48
  172. package/templates/page/page.tsx +3 -3
  173. package/templates/shared/HeroHeader.tsx +16 -15
  174. package/templates/shared/index.ts +1 -1
  175. package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
  176. package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
  177. package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
  178. package/templates/ui-showcase/page.tsx +4 -4
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: Stop
4
+ Purpose: Check if all required phases are complete before allowing stop
5
+
6
+ This hook runs when Claude tries to stop/end the conversation.
7
+ It checks api-dev-state.json to ensure critical workflow phases completed.
8
+
9
+ Gap Fixes Applied:
10
+ - Gap 2: Shows files_created vs files_modified to verify all claimed changes
11
+ - Gap 3: Warns if there are verification_warnings that weren't addressed
12
+ - Gap 4: Requires explicit verification that implementation matches interview
13
+
14
+ Returns:
15
+ - {"decision": "approve"} - Allow stopping
16
+ - {"decision": "block", "reason": "..."} - Prevent stopping with explanation
17
+ """
18
+ import json
19
+ import sys
20
+ import subprocess
21
+ from pathlib import Path
22
+
23
+ # State file is in .claude/ directory (sibling to hooks/)
24
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
25
+
26
+ # Phases that MUST be complete before stopping
27
+ REQUIRED_PHASES = [
28
+ ("research_initial", "Initial research (Context7/WebSearch)"),
29
+ ("interview", "User interview"),
30
+ ("tdd_red", "TDD Red phase (failing tests written)"),
31
+ ("tdd_green", "TDD Green phase (tests passing)"),
32
+ ("verify", "Verification phase (re-checked against docs)"),
33
+ ("documentation", "Documentation updates (manifest/research cached)"),
34
+ ]
35
+
36
+ # Phases that SHOULD be complete (warning but don't block)
37
+ RECOMMENDED_PHASES = [
38
+ ("schema_creation", "Schema creation"),
39
+ ("tdd_refactor", "TDD Refactor phase"),
40
+ ("documentation", "Documentation updates"),
41
+ ]
42
+
43
+
44
+ def get_git_modified_files() -> list[str]:
45
+ """Get list of modified files from git.
46
+
47
+ Gap 2 Fix: Verify which files actually changed.
48
+ """
49
+ try:
50
+ result = subprocess.run(
51
+ ["git", "diff", "--name-only", "HEAD"],
52
+ capture_output=True,
53
+ text=True,
54
+ cwd=STATE_FILE.parent.parent # Project root
55
+ )
56
+ if result.returncode == 0:
57
+ return [f.strip() for f in result.stdout.strip().split("\n") if f.strip()]
58
+ except Exception:
59
+ pass
60
+ return []
61
+
62
+
63
+ def check_verification_warnings(state: dict) -> list[str]:
64
+ """Check for unaddressed verification warnings.
65
+
66
+ Gap 3 Fix: Don't accept "skipped" or warnings without explanation.
67
+ """
68
+ warnings = state.get("verification_warnings", [])
69
+ if warnings:
70
+ return [
71
+ "⚠️ Unaddressed verification warnings:",
72
+ *[f" - {w}" for w in warnings[-5:]], # Show last 5
73
+ "",
74
+ "Please review and address these warnings before completing."
75
+ ]
76
+ return []
77
+
78
+
79
+ def check_interview_implementation_match(state: dict) -> list[str]:
80
+ """Verify implementation matches interview requirements.
81
+
82
+ Gap 4 Fix: Define specific "done" criteria based on interview.
83
+ """
84
+ issues = []
85
+
86
+ interview = state.get("phases", {}).get("interview", {})
87
+ questions = interview.get("questions", [])
88
+
89
+ # Extract key requirements from interview
90
+ all_text = " ".join(str(q) for q in questions)
91
+
92
+ # Check files_created includes expected patterns
93
+ files_created = state.get("files_created", [])
94
+
95
+ # Look for route files if interview mentioned endpoints
96
+ if "endpoint" in all_text.lower() or "/api/" in all_text.lower():
97
+ route_files = [f for f in files_created if "route.ts" in f]
98
+ if not route_files:
99
+ issues.append("⚠️ Interview mentioned endpoints but no route.ts files were created")
100
+
101
+ # Look for test files
102
+ test_files = [f for f in files_created if ".test." in f or "__tests__" in f]
103
+ if not test_files:
104
+ issues.append("⚠️ No test files tracked in files_created")
105
+
106
+ return issues
107
+
108
+
109
+ def main():
110
+ # If no state file, we're not in an API workflow - allow stop
111
+ if not STATE_FILE.exists():
112
+ print(json.dumps({"decision": "approve"}))
113
+ sys.exit(0)
114
+
115
+ # Load state
116
+ try:
117
+ state = json.loads(STATE_FILE.read_text())
118
+ except json.JSONDecodeError:
119
+ # Corrupted state, allow stop
120
+ print(json.dumps({"decision": "approve"}))
121
+ sys.exit(0)
122
+
123
+ phases = state.get("phases", {})
124
+
125
+ # Check if workflow was even started
126
+ research = phases.get("research_initial", {})
127
+ if research.get("status") == "not_started":
128
+ # Workflow not started, allow stop
129
+ print(json.dumps({"decision": "approve"}))
130
+ sys.exit(0)
131
+
132
+ # Collect all issues
133
+ all_issues = []
134
+
135
+ # Check required phases
136
+ incomplete_required = []
137
+ for phase_key, phase_name in REQUIRED_PHASES:
138
+ phase = phases.get(phase_key, {})
139
+ status = phase.get("status", "not_started")
140
+ if status != "complete":
141
+ incomplete_required.append(f" - {phase_name} ({status})")
142
+
143
+ if incomplete_required:
144
+ all_issues.append("❌ REQUIRED phases incomplete:")
145
+ all_issues.extend(incomplete_required)
146
+
147
+ # Check recommended phases
148
+ incomplete_recommended = []
149
+ for phase_key, phase_name in RECOMMENDED_PHASES:
150
+ phase = phases.get(phase_key, {})
151
+ status = phase.get("status", "not_started")
152
+ if status != "complete":
153
+ incomplete_recommended.append(f" - {phase_name} ({status})")
154
+
155
+ # Gap 2: Check git diff vs tracked files
156
+ git_files = get_git_modified_files()
157
+ tracked_files = state.get("files_created", []) + state.get("files_modified", [])
158
+
159
+ if git_files and tracked_files:
160
+ # Find files in git but not tracked
161
+ untracked_changes = []
162
+ for gf in git_files:
163
+ if not any(gf.endswith(tf) or tf in gf for tf in tracked_files):
164
+ if gf.endswith(".ts") and ("/api/" in gf or "/lib/" in gf):
165
+ untracked_changes.append(gf)
166
+
167
+ if untracked_changes:
168
+ all_issues.append("\n⚠️ Gap 2: Files changed but not tracked:")
169
+ all_issues.extend([f" - {f}" for f in untracked_changes[:5]])
170
+
171
+ # Gap 3: Check for unaddressed warnings
172
+ warning_issues = check_verification_warnings(state)
173
+ if warning_issues:
174
+ all_issues.append("\n" + "\n".join(warning_issues))
175
+
176
+ # Gap 4: Check interview-implementation match
177
+ match_issues = check_interview_implementation_match(state)
178
+ if match_issues:
179
+ all_issues.append("\n⚠️ Gap 4: Implementation verification:")
180
+ all_issues.extend([f" {i}" for i in match_issues])
181
+
182
+ # Block if required phases incomplete
183
+ if incomplete_required:
184
+ all_issues.append("\n\nTo continue:")
185
+ all_issues.append(" 1. Complete required phases above")
186
+ all_issues.append(" 2. Use /api-status to see detailed progress")
187
+ all_issues.append(" 3. Run `git diff --name-only` to verify changes")
188
+
189
+ print(json.dumps({
190
+ "decision": "block",
191
+ "reason": "\n".join(all_issues)
192
+ }))
193
+ sys.exit(0)
194
+
195
+ # Build completion message
196
+ message_parts = ["✅ API workflow completing"]
197
+
198
+ if incomplete_recommended:
199
+ message_parts.append("\n⚠️ Optional phases skipped:")
200
+ message_parts.extend(incomplete_recommended)
201
+
202
+ # Show summary of tracked files
203
+ files_created = state.get("files_created", [])
204
+ if files_created:
205
+ message_parts.append(f"\n📁 Files created: {len(files_created)}")
206
+ for f in files_created[:5]:
207
+ message_parts.append(f" - {f}")
208
+ if len(files_created) > 5:
209
+ message_parts.append(f" ... and {len(files_created) - 5} more")
210
+
211
+ # Show any remaining warnings
212
+ if warning_issues or match_issues:
213
+ message_parts.append("\n⚠️ Review suggested:")
214
+ if warning_issues:
215
+ message_parts.extend(warning_issues[:3])
216
+ if match_issues:
217
+ message_parts.extend(match_issues[:3])
218
+
219
+ print(json.dumps({
220
+ "decision": "approve",
221
+ "message": "\n".join(message_parts)
222
+ }))
223
+ sys.exit(0)
224
+
225
+
226
+ if __name__ == "__main__":
227
+ main()
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PreToolUse for Write/Edit
4
+ Purpose: Block writing if deep research not completed WITH USER APPROVAL
5
+
6
+ Phase 5 requires:
7
+ 1. PROPOSE searches based on interview answers
8
+ 2. Show checkbox list to user
9
+ 3. USE AskUserQuestion: "Approve? [Y] / Add more? ____"
10
+ 4. Execute only approved searches
11
+ 5. Loop back if user wants additions
12
+
13
+ Returns:
14
+ - {"permissionDecision": "allow"} - Let the tool run
15
+ - {"permissionDecision": "deny", "reason": "..."} - Block with explanation
16
+ """
17
+ import json
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
22
+
23
+
24
+ def main():
25
+ try:
26
+ input_data = json.load(sys.stdin)
27
+ except json.JSONDecodeError:
28
+ print(json.dumps({"permissionDecision": "allow"}))
29
+ sys.exit(0)
30
+
31
+ tool_input = input_data.get("tool_input", {})
32
+ file_path = tool_input.get("file_path", "")
33
+
34
+ # Only enforce for API route and schema files
35
+ is_api_file = "/api/" in file_path and file_path.endswith(".ts")
36
+ is_schema_file = "/schemas/" in file_path and file_path.endswith(".ts")
37
+
38
+ if not is_api_file and not is_schema_file:
39
+ print(json.dumps({"permissionDecision": "allow"}))
40
+ sys.exit(0)
41
+
42
+ # Skip test files
43
+ if ".test." in file_path or "/__tests__/" in file_path or ".spec." in file_path:
44
+ print(json.dumps({"permissionDecision": "allow"}))
45
+ sys.exit(0)
46
+
47
+ if not STATE_FILE.exists():
48
+ print(json.dumps({"permissionDecision": "allow"}))
49
+ sys.exit(0)
50
+
51
+ try:
52
+ state = json.loads(STATE_FILE.read_text())
53
+ except json.JSONDecodeError:
54
+ print(json.dumps({"permissionDecision": "allow"}))
55
+ sys.exit(0)
56
+
57
+ endpoint = state.get("endpoint", "unknown")
58
+ phases = state.get("phases", {})
59
+ interview = phases.get("interview", {})
60
+ research_deep = phases.get("research_deep", {})
61
+
62
+ # Only enforce after interview is complete
63
+ if interview.get("status") != "complete":
64
+ print(json.dumps({"permissionDecision": "allow"}))
65
+ sys.exit(0)
66
+
67
+ status = research_deep.get("status", "not_started")
68
+
69
+ # If deep research was not needed (no proposed searches), allow
70
+ proposed = research_deep.get("proposed_searches", [])
71
+ if not proposed and status == "not_started":
72
+ print(json.dumps({"permissionDecision": "allow"}))
73
+ sys.exit(0)
74
+
75
+ phase_exit_confirmed = research_deep.get("phase_exit_confirmed", False)
76
+
77
+ if status != "complete" or not phase_exit_confirmed:
78
+ user_question_asked = research_deep.get("user_question_asked", False)
79
+ user_approved = research_deep.get("user_approved", False)
80
+ proposals_shown = research_deep.get("proposals_shown", False)
81
+ approved_searches = research_deep.get("approved_searches", [])
82
+ executed_searches = research_deep.get("executed_searches", [])
83
+ skipped_searches = research_deep.get("skipped_searches", [])
84
+
85
+ # Calculate pending
86
+ pending = [s for s in approved_searches if s not in executed_searches and s not in skipped_searches]
87
+
88
+ missing = []
89
+ if not proposals_shown:
90
+ missing.append("Proposed searches not shown to user")
91
+ if not user_question_asked:
92
+ missing.append("User approval question (AskUserQuestion not used)")
93
+ if not user_approved:
94
+ missing.append("User hasn't approved the search list")
95
+ if pending:
96
+ missing.append(f"Approved searches not executed ({len(pending)} pending)")
97
+ if not phase_exit_confirmed:
98
+ missing.append("Phase exit confirmation (user must explicitly approve to proceed)")
99
+
100
+ print(json.dumps({
101
+ "permissionDecision": "deny",
102
+ "reason": f"""❌ BLOCKED: Deep research (Phase 5) not complete.
103
+
104
+ Status: {status}
105
+ Proposed searches: {len(proposed)}
106
+ User shown proposals: {proposals_shown}
107
+ User question asked: {user_question_asked}
108
+ User approved: {user_approved}
109
+ Approved: {len(approved_searches)}
110
+ Executed: {len(executed_searches)}
111
+ Skipped: {len(skipped_searches)}
112
+ Pending: {len(pending)}
113
+ Phase exit confirmed: {phase_exit_confirmed}
114
+
115
+ MISSING:
116
+ {chr(10).join(f" • {m}" for m in missing)}
117
+
118
+ ═══════════════════════════════════════════════════════════
119
+ ⚠️ GET USER APPROVAL FOR DEEP RESEARCH
120
+ ═══════════════════════════════════════════════════════════
121
+
122
+ REQUIRED STEPS:
123
+
124
+ 1. Based on interview, PROPOSE targeted searches:
125
+ ┌───────────────────────────────────────────────────────┐
126
+ │ PROPOSED DEEP RESEARCH │
127
+ │ │
128
+ │ Based on your interview answers, I want to research: │
129
+ │ │
130
+ │ [x] Error response format (for error handling) │
131
+ │ [x] Rate limiting behavior (caching selected) │
132
+ │ [ ] Webhook support (not selected in interview) │
133
+ │ [x] Authentication edge cases │
134
+ │ │
135
+ │ Approve these searches? [Y] │
136
+ │ Add more: ____ │
137
+ │ Skip and proceed: [n] │
138
+ └───────────────────────────────────────────────────────┘
139
+
140
+ 2. USE AskUserQuestion:
141
+ question: "Approve these deep research searches?"
142
+ options: [
143
+ {{"value": "approve", "label": "Yes, run these searches"}},
144
+ {{"value": "add", "label": "Add more - I also need [topic]"}},
145
+ {{"value": "skip", "label": "Skip deep research, proceed to schema"}}
146
+ ]
147
+
148
+ 3. If user says "add":
149
+ • Ask what additional topics they need
150
+ • Add to proposed_searches
151
+ • LOOP BACK and show updated list
152
+
153
+ 4. If user says "approve":
154
+ • Execute each approved search
155
+ • Record results in executed_searches
156
+
157
+ 5. If user says "skip":
158
+ • Record all as skipped_searches with reason
159
+ • Proceed to schema
160
+
161
+ 6. After all searches complete (or skipped):
162
+ • Set research_deep.user_approved = true
163
+ • Set research_deep.user_question_asked = true
164
+ • Set research_deep.proposals_shown = true
165
+ • Set research_deep.status = "complete"
166
+
167
+ WHY: Research is ADAPTIVE based on interview, not shotgun."""
168
+ }))
169
+ sys.exit(0)
170
+
171
+ # Complete
172
+ executed = research_deep.get("executed_searches", [])
173
+ skipped = research_deep.get("skipped_searches", [])
174
+ print(json.dumps({
175
+ "permissionDecision": "allow",
176
+ "message": f"""✅ Deep research complete.
177
+ Executed: {len(executed)} searches
178
+ Skipped: {len(skipped)} (with reasons)
179
+ User approved the search plan."""
180
+ }))
181
+ sys.exit(0)
182
+
183
+
184
+ if __name__ == "__main__":
185
+ main()
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PreToolUse for Write/Edit
4
+ Purpose: Block writing API code if disambiguation phase not complete WITH USER CONFIRMATION
5
+
6
+ Phase 1 requires:
7
+ 1. Search 3-5 variations of the term
8
+ 2. Present options to user via AskUserQuestion
9
+ 3. User selects which interpretation
10
+ 4. Loop back if still ambiguous
11
+
12
+ Returns:
13
+ - {"permissionDecision": "allow"} - Let the tool run
14
+ - {"permissionDecision": "deny", "reason": "..."} - Block with explanation
15
+ """
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
21
+
22
+ # Minimum search variations required
23
+ MIN_SEARCH_VARIATIONS = 2
24
+
25
+
26
+ def main():
27
+ try:
28
+ input_data = json.load(sys.stdin)
29
+ except json.JSONDecodeError:
30
+ print(json.dumps({"permissionDecision": "allow"}))
31
+ sys.exit(0)
32
+
33
+ tool_input = input_data.get("tool_input", {})
34
+ file_path = tool_input.get("file_path", "")
35
+
36
+ # Only enforce for API route files
37
+ if "/api/" not in file_path:
38
+ print(json.dumps({"permissionDecision": "allow"}))
39
+ sys.exit(0)
40
+
41
+ # Skip test files
42
+ if ".test." in file_path or "/__tests__/" in file_path or ".spec." in file_path:
43
+ print(json.dumps({"permissionDecision": "allow"}))
44
+ sys.exit(0)
45
+
46
+ # Skip documentation/config files
47
+ if file_path.endswith(".md") or file_path.endswith(".json"):
48
+ print(json.dumps({"permissionDecision": "allow"}))
49
+ sys.exit(0)
50
+
51
+ if not STATE_FILE.exists():
52
+ print(json.dumps({
53
+ "permissionDecision": "deny",
54
+ "reason": """❌ API workflow not started.
55
+
56
+ Run /api-create [endpoint-name] to begin the interview-driven workflow.
57
+
58
+ Phase 1 (Disambiguation) is required before any implementation."""
59
+ }))
60
+ sys.exit(0)
61
+
62
+ try:
63
+ state = json.loads(STATE_FILE.read_text())
64
+ except json.JSONDecodeError:
65
+ print(json.dumps({"permissionDecision": "allow"}))
66
+ sys.exit(0)
67
+
68
+ endpoint = state.get("endpoint")
69
+ if not endpoint:
70
+ print(json.dumps({"permissionDecision": "allow"}))
71
+ sys.exit(0)
72
+
73
+ phases = state.get("phases", {})
74
+ disambiguation = phases.get("disambiguation", {})
75
+ status = disambiguation.get("status", "not_started")
76
+
77
+ # Also check phase_exit_confirmed even if status is "complete"
78
+ phase_exit_confirmed = disambiguation.get("phase_exit_confirmed", False)
79
+
80
+ if status != "complete" or not phase_exit_confirmed:
81
+ search_variations = disambiguation.get("search_variations", [])
82
+ user_question_asked = disambiguation.get("user_question_asked", False)
83
+ user_selected = disambiguation.get("user_selected", None)
84
+
85
+ # Check what's missing
86
+ missing = []
87
+ if len(search_variations) < MIN_SEARCH_VARIATIONS:
88
+ missing.append(f"Search variations ({len(search_variations)}/{MIN_SEARCH_VARIATIONS})")
89
+ if not user_question_asked:
90
+ missing.append("User question (AskUserQuestion not used)")
91
+ if not user_selected:
92
+ missing.append("User selection (no choice recorded)")
93
+ if not phase_exit_confirmed:
94
+ missing.append("Phase exit confirmation (user must explicitly confirm to proceed)")
95
+
96
+ print(json.dumps({
97
+ "permissionDecision": "deny",
98
+ "reason": f"""❌ BLOCKED: Disambiguation phase (Phase 1) not complete.
99
+
100
+ Status: {status}
101
+ Search variations: {len(search_variations)}
102
+ User question asked: {user_question_asked}
103
+ User selection: {user_selected or "None"}
104
+ Phase exit confirmed: {phase_exit_confirmed}
105
+
106
+ MISSING:
107
+ {chr(10).join(f" • {m}" for m in missing)}
108
+
109
+ ═══════════════════════════════════════════════════════════
110
+ ⚠️ COMPLETE DISAMBIGUATION WITH USER CONFIRMATION
111
+ ═══════════════════════════════════════════════════════════
112
+
113
+ REQUIRED STEPS:
114
+
115
+ 1. Search 2-3 variations:
116
+ • WebSearch: "{endpoint}"
117
+ • WebSearch: "{endpoint} API"
118
+ • WebSearch: "{endpoint} SDK npm package"
119
+
120
+ 2. USE AskUserQuestion with options:
121
+ ┌───────────────────────────────────────────────────────┐
122
+ │ I found multiple things matching "{endpoint}": │
123
+ │ │
124
+ │ [A] The official REST API │
125
+ │ [B] The npm/SDK wrapper package │
126
+ │ [C] Both (API + SDK) │
127
+ │ [D] Something else: ____ │
128
+ │ │
129
+ │ Which should this endpoint use? │
130
+ └───────────────────────────────────────────────────────┘
131
+
132
+ 3. Record user's choice in state:
133
+ disambiguation.user_selected = "A" (or user's choice)
134
+ disambiguation.user_question_asked = true
135
+ disambiguation.status = "complete"
136
+
137
+ 4. LOOP BACK if user is still unsure - search more variations
138
+
139
+ WHY: Different interpretations = different implementations."""
140
+ }))
141
+ sys.exit(0)
142
+
143
+ # Complete - inject context
144
+ selected = disambiguation.get("user_selected", "")
145
+ print(json.dumps({
146
+ "permissionDecision": "allow",
147
+ "message": f"""✅ Disambiguation complete.
148
+ User selected: {selected}
149
+ Proceeding with this interpretation."""
150
+ }))
151
+ sys.exit(0)
152
+
153
+
154
+ if __name__ == "__main__":
155
+ main()