@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,159 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PreToolUse for Write/Edit
4
+ Purpose: Block writing API code if research phase not complete WITH USER CHECKPOINT
5
+
6
+ Phase 3 requires:
7
+ 1. Execute 2-3 initial searches (Context7, WebSearch)
8
+ 2. Present summary TABLE to user
9
+ 3. USE AskUserQuestion: "Proceed? [Y] / Search more? [n]"
10
+ 4. Loop back if user wants more research
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 sources required
23
+ MIN_SOURCES = 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 and "/api-test/" not in file_path:
38
+ print(json.dumps({"permissionDecision": "allow"}))
39
+ sys.exit(0)
40
+
41
+ # Skip test files - TDD Red allows tests before research complete
42
+ if ".test." in file_path or "/__tests__/" in file_path:
43
+ print(json.dumps({"permissionDecision": "allow"}))
44
+ sys.exit(0)
45
+
46
+ if file_path.endswith(".md") or file_path.endswith(".json"):
47
+ print(json.dumps({"permissionDecision": "allow"}))
48
+ sys.exit(0)
49
+
50
+ if not STATE_FILE.exists():
51
+ print(json.dumps({
52
+ "permissionDecision": "deny",
53
+ "reason": """❌ API development state not initialized.
54
+
55
+ Run /api-create [endpoint-name] to start the workflow."""
56
+ }))
57
+ sys.exit(0)
58
+
59
+ try:
60
+ state = json.loads(STATE_FILE.read_text())
61
+ except json.JSONDecodeError:
62
+ print(json.dumps({"permissionDecision": "allow"}))
63
+ sys.exit(0)
64
+
65
+ endpoint = state.get("endpoint", "unknown")
66
+ phases = state.get("phases", {})
67
+ research = phases.get("research_initial", {})
68
+ status = research.get("status", "not_started")
69
+
70
+ if status != "complete":
71
+ sources = research.get("sources", [])
72
+ user_question_asked = research.get("user_question_asked", False)
73
+ user_approved = research.get("user_approved", False)
74
+ summary_shown = research.get("summary_shown", False)
75
+
76
+ missing = []
77
+ if len(sources) < MIN_SOURCES:
78
+ missing.append(f"Sources ({len(sources)}/{MIN_SOURCES} minimum)")
79
+ if not summary_shown:
80
+ missing.append("Research summary table not shown to user")
81
+ if not user_question_asked:
82
+ missing.append("User checkpoint (AskUserQuestion not used)")
83
+ if not user_approved:
84
+ missing.append("User approval to proceed")
85
+
86
+ print(json.dumps({
87
+ "permissionDecision": "deny",
88
+ "reason": f"""❌ BLOCKED: Initial research (Phase 3) not complete.
89
+
90
+ Status: {status}
91
+ Sources consulted: {len(sources)}
92
+ Summary shown: {summary_shown}
93
+ User question asked: {user_question_asked}
94
+ User approved: {user_approved}
95
+
96
+ MISSING:
97
+ {chr(10).join(f" • {m}" for m in missing)}
98
+
99
+ ═══════════════════════════════════════════════════════════
100
+ ⚠️ COMPLETE RESEARCH WITH USER CHECKPOINT
101
+ ═══════════════════════════════════════════════════════════
102
+
103
+ REQUIRED STEPS:
104
+
105
+ 1. Execute 2-3 initial searches:
106
+ • Context7: "{endpoint}"
107
+ • WebSearch: "{endpoint} official documentation"
108
+ • WebSearch: "site:[official-domain] {endpoint} API reference"
109
+
110
+ 2. Present summary TABLE to user:
111
+ ┌───────────────────────────────────────────────────────┐
112
+ │ RESEARCH SUMMARY │
113
+ │ │
114
+ │ │ Source │ Found │
115
+ │ ├────────────────┼────────────────────────────────────│
116
+ │ │ Official docs │ ✓ [URL] │
117
+ │ │ API Reference │ ✓ REST v2 │
118
+ │ │ Auth method │ ✓ Bearer token │
119
+ │ │ NPM package │ ? Not found │
120
+ │ │
121
+ │ Proceed? [Y] / Search more? [n] ____ │
122
+ └───────────────────────────────────────────────────────┘
123
+
124
+ 3. USE AskUserQuestion:
125
+ question: "Research summary above. Proceed or search more?"
126
+ options: [
127
+ {{"value": "proceed", "label": "Proceed to interview"}},
128
+ {{"value": "more", "label": "Search more - I need [topic]"}},
129
+ {{"value": "specific", "label": "Search for something specific"}}
130
+ ]
131
+
132
+ 4. If user says "more" or "specific":
133
+ • Ask what they want to research
134
+ • Execute additional searches
135
+ • LOOP BACK and show updated summary
136
+
137
+ 5. If user says "proceed":
138
+ • Set research_initial.user_approved = true
139
+ • Set research_initial.user_question_asked = true
140
+ • Set research_initial.summary_shown = true
141
+ • Set research_initial.status = "complete"
142
+
143
+ WHY: Implementation must match CURRENT API documentation."""
144
+ }))
145
+ sys.exit(0)
146
+
147
+ # Research complete - inject context
148
+ sources = research.get("sources", [])
149
+ print(json.dumps({
150
+ "permissionDecision": "allow",
151
+ "message": f"""✅ Initial research complete.
152
+ Sources: {len(sources)}
153
+ User approved proceeding to interview."""
154
+ }))
155
+ sys.exit(0)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PreToolUse for Write/Edit
4
+ Purpose: Block writing implementation if schema not reviewed WITH USER CONFIRMATION
5
+
6
+ Phase 6 requires:
7
+ 1. Create Zod schemas based on interview + research
8
+ 2. SHOW schema to user in formatted display
9
+ 3. USE AskUserQuestion: "Schema matches interview? [Y/n]"
10
+ 4. Loop back if user wants changes
11
+ 5. Only proceed when user confirms
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
+ # Skip documentation/config files
48
+ if file_path.endswith(".md") or file_path.endswith(".json"):
49
+ print(json.dumps({"permissionDecision": "allow"}))
50
+ sys.exit(0)
51
+
52
+ if not STATE_FILE.exists():
53
+ print(json.dumps({"permissionDecision": "allow"}))
54
+ sys.exit(0)
55
+
56
+ try:
57
+ state = json.loads(STATE_FILE.read_text())
58
+ except json.JSONDecodeError:
59
+ print(json.dumps({"permissionDecision": "allow"}))
60
+ sys.exit(0)
61
+
62
+ endpoint = state.get("endpoint", "unknown")
63
+ phases = state.get("phases", {})
64
+ interview = phases.get("interview", {})
65
+ research_deep = phases.get("research_deep", {})
66
+ schema_creation = phases.get("schema_creation", {})
67
+
68
+ # Only enforce after interview is complete
69
+ if interview.get("status") != "complete":
70
+ print(json.dumps({"permissionDecision": "allow"}))
71
+ sys.exit(0)
72
+
73
+ # Only enforce after deep research is complete (or not needed)
74
+ deep_status = research_deep.get("status", "not_started")
75
+ proposed = research_deep.get("proposed_searches", [])
76
+ if proposed and deep_status != "complete":
77
+ # Let enforce-deep-research.py handle this
78
+ print(json.dumps({"permissionDecision": "allow"}))
79
+ sys.exit(0)
80
+
81
+ status = schema_creation.get("status", "not_started")
82
+
83
+ if status != "complete":
84
+ user_question_asked = schema_creation.get("user_question_asked", False)
85
+ user_confirmed = schema_creation.get("user_confirmed", False)
86
+ schema_shown = schema_creation.get("schema_shown", False)
87
+ schema_file = schema_creation.get("schema_file", None)
88
+ fields_count = schema_creation.get("fields_count", 0)
89
+
90
+ missing = []
91
+ if not schema_shown:
92
+ missing.append("Schema not shown to user")
93
+ if not user_question_asked:
94
+ missing.append("User review question (AskUserQuestion not used)")
95
+ if not user_confirmed:
96
+ missing.append("User hasn't confirmed schema matches interview")
97
+
98
+ print(json.dumps({
99
+ "permissionDecision": "deny",
100
+ "reason": f"""❌ BLOCKED: Schema creation (Phase 6) not complete.
101
+
102
+ Status: {status}
103
+ Schema shown: {schema_shown}
104
+ User question asked: {user_question_asked}
105
+ User confirmed: {user_confirmed}
106
+ Schema file: {schema_file or "Not created yet"}
107
+ Fields: {fields_count}
108
+
109
+ MISSING:
110
+ {chr(10).join(f" • {m}" for m in missing)}
111
+
112
+ ═══════════════════════════════════════════════════════════
113
+ ⚠️ GET USER CONFIRMATION FOR SCHEMA
114
+ ═══════════════════════════════════════════════════════════
115
+
116
+ REQUIRED STEPS:
117
+
118
+ 1. Create Zod schemas based on:
119
+ • Interview answers (error handling, caching, etc.)
120
+ • Research findings (API parameters, response format)
121
+
122
+ 2. SHOW formatted schema to user:
123
+ ┌───────────────────────────────────────────────────────┐
124
+ │ SCHEMA REVIEW │
125
+ │ │
126
+ │ Request Schema: │
127
+ │ domain: z.string() ← From interview: domain │
128
+ │ mode: z.enum(["full", "logo"]) ← Your choice: full │
129
+ │ includeColors: z.boolean().default(true) │
130
+ │ │
131
+ │ Response Schema: │
132
+ │ success: z.boolean() │
133
+ │ data: BrandDataSchema │
134
+ │ cached: z.boolean() ← From interview: 24h │
135
+ │ error: ErrorSchema.optional() │
136
+ │ │
137
+ │ Based on YOUR interview answers: │
138
+ │ ✓ Error handling: Return objects │
139
+ │ ✓ Caching: 24h (long) │
140
+ │ ✓ Mode: Full brand kit │
141
+ │ │
142
+ │ Does this match your requirements? [Y/n] │
143
+ └───────────────────────────────────────────────────────┘
144
+
145
+ 3. USE AskUserQuestion:
146
+ question: "Does this schema match your interview answers?"
147
+ options: [
148
+ {{"value": "confirm", "label": "Yes, schema looks correct"}},
149
+ {{"value": "modify", "label": "No, I need changes - [describe]"}},
150
+ {{"value": "restart", "label": "Let's redo the interview"}}
151
+ ]
152
+
153
+ 4. If user says "modify":
154
+ • Ask what changes they need
155
+ • Update schema accordingly
156
+ • LOOP BACK and show updated schema
157
+
158
+ 5. If user says "restart":
159
+ • Reset interview phase
160
+ • Go back to Phase 4
161
+
162
+ 6. If user says "confirm":
163
+ • Set schema_creation.user_confirmed = true
164
+ • Set schema_creation.user_question_asked = true
165
+ • Set schema_creation.schema_shown = true
166
+ • Set schema_creation.status = "complete"
167
+
168
+ WHY: Schema is the CONTRACT. User must approve before implementation."""
169
+ }))
170
+ sys.exit(0)
171
+
172
+ # Schema complete
173
+ schema_file = schema_creation.get("schema_file", "")
174
+ fields_count = schema_creation.get("fields_count", 0)
175
+ print(json.dumps({
176
+ "permissionDecision": "allow",
177
+ "message": f"""✅ Schema creation complete.
178
+ Schema file: {schema_file}
179
+ Fields: {fields_count}
180
+ User confirmed schema matches interview requirements."""
181
+ }))
182
+ sys.exit(0)
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PreToolUse for Write/Edit
4
+ Purpose: Block writing API code if scope not confirmed BY USER
5
+
6
+ Phase 2 requires:
7
+ 1. Present scope understanding to user
8
+ 2. USE AskUserQuestion: "Is this correct? [Y/n]"
9
+ 3. Record any modifications user requests
10
+ 4. Loop back if user wants changes
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
+
23
+ def main():
24
+ try:
25
+ input_data = json.load(sys.stdin)
26
+ except json.JSONDecodeError:
27
+ print(json.dumps({"permissionDecision": "allow"}))
28
+ sys.exit(0)
29
+
30
+ tool_input = input_data.get("tool_input", {})
31
+ file_path = tool_input.get("file_path", "")
32
+
33
+ # Only enforce for API route files
34
+ if "/api/" not in file_path:
35
+ print(json.dumps({"permissionDecision": "allow"}))
36
+ sys.exit(0)
37
+
38
+ # Skip test files
39
+ if ".test." in file_path or "/__tests__/" in file_path or ".spec." in file_path:
40
+ print(json.dumps({"permissionDecision": "allow"}))
41
+ sys.exit(0)
42
+
43
+ if file_path.endswith(".md") or file_path.endswith(".json"):
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")
58
+ if not endpoint:
59
+ print(json.dumps({"permissionDecision": "allow"}))
60
+ sys.exit(0)
61
+
62
+ phases = state.get("phases", {})
63
+ disambiguation = phases.get("disambiguation", {})
64
+ scope = phases.get("scope", {})
65
+
66
+ # Check disambiguation is complete first
67
+ if disambiguation.get("status") != "complete":
68
+ print(json.dumps({"permissionDecision": "allow"}))
69
+ sys.exit(0)
70
+
71
+ status = scope.get("status", "not_started")
72
+ user_confirmed = scope.get("user_confirmed", False)
73
+ user_question_asked = scope.get("user_question_asked", False)
74
+ phase_exit_confirmed = scope.get("phase_exit_confirmed", False)
75
+
76
+ if status != "complete" or not user_confirmed or not phase_exit_confirmed:
77
+ endpoint_path = scope.get("endpoint_path", f"/api/v2/{endpoint}")
78
+ modifications = scope.get("modifications", [])
79
+
80
+ missing = []
81
+ if not user_question_asked:
82
+ missing.append("User question (AskUserQuestion not used)")
83
+ if not user_confirmed:
84
+ missing.append("User confirmation (user hasn't said 'yes')")
85
+ if not phase_exit_confirmed:
86
+ missing.append("Phase exit confirmation (user must explicitly approve to proceed)")
87
+
88
+ print(json.dumps({
89
+ "permissionDecision": "deny",
90
+ "reason": f"""❌ BLOCKED: Scope confirmation (Phase 2) not complete.
91
+
92
+ Status: {status}
93
+ User question asked: {user_question_asked}
94
+ User confirmed: {user_confirmed}
95
+ Phase exit confirmed: {phase_exit_confirmed}
96
+ Proposed path: {endpoint_path}
97
+ Modifications: {len(modifications)}
98
+
99
+ MISSING:
100
+ {chr(10).join(f" • {m}" for m in missing)}
101
+
102
+ ═══════════════════════════════════════════════════════════
103
+ ⚠️ GET USER CONFIRMATION OF SCOPE
104
+ ═══════════════════════════════════════════════════════════
105
+
106
+ REQUIRED STEPS:
107
+
108
+ 1. Present your understanding:
109
+ ┌───────────────────────────────────────────────────────┐
110
+ │ SCOPE CONFIRMATION │
111
+ │ │
112
+ │ I understand you want: {endpoint_path} │
113
+ │ Purpose: [describe inferred purpose] │
114
+ │ External API: [service name if any] │
115
+ │ │
116
+ │ Is this correct? [Y/n] │
117
+ │ Any modifications needed? ____ │
118
+ └───────────────────────────────────────────────────────┘
119
+
120
+ 2. USE AskUserQuestion:
121
+ question: "Is this scope correct? Any modifications?"
122
+ options: [
123
+ {{"value": "yes", "label": "Yes, proceed"}},
124
+ {{"value": "modify", "label": "I have modifications"}},
125
+ {{"value": "no", "label": "No, let me clarify"}}
126
+ ]
127
+
128
+ 3. If user says "modify" or "no":
129
+ • Ask for their modifications
130
+ • Record them in scope.modifications
131
+ • LOOP BACK and confirm again
132
+
133
+ 4. If user says "yes":
134
+ • Set scope.user_confirmed = true
135
+ • Set scope.user_question_asked = true
136
+ • Set scope.status = "complete"
137
+
138
+ WHY: Prevents building the wrong thing."""
139
+ }))
140
+ sys.exit(0)
141
+
142
+ # Scope confirmed - inject context
143
+ endpoint_path = scope.get("endpoint_path", f"/api/v2/{endpoint}")
144
+ modifications = scope.get("modifications", [])
145
+
146
+ context = [f"✅ Scope confirmed: {endpoint_path}"]
147
+ if modifications:
148
+ context.append("User modifications:")
149
+ for mod in modifications[:3]:
150
+ context.append(f" • {mod}")
151
+
152
+ print(json.dumps({
153
+ "permissionDecision": "allow",
154
+ "message": "\n".join(context)
155
+ }))
156
+ sys.exit(0)
157
+
158
+
159
+ if __name__ == "__main__":
160
+ main()