@hustle-together/api-dev-tools 3.0.0 → 3.2.0

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 (38) hide show
  1. package/README.md +71 -0
  2. package/bin/cli.js +184 -14
  3. package/demo/audio/generate-all-narrations.js +124 -59
  4. package/demo/audio/generate-narration.js +120 -56
  5. package/demo/audio/narration-adam-timing.json +3086 -2077
  6. package/demo/audio/narration-adam.mp3 +0 -0
  7. package/demo/audio/narration-creature-timing.json +3094 -2085
  8. package/demo/audio/narration-creature.mp3 +0 -0
  9. package/demo/audio/narration-gaming-timing.json +3091 -2082
  10. package/demo/audio/narration-gaming.mp3 +0 -0
  11. package/demo/audio/narration-hope-timing.json +3072 -2063
  12. package/demo/audio/narration-hope.mp3 +0 -0
  13. package/demo/audio/narration-mark-timing.json +3090 -2081
  14. package/demo/audio/narration-mark.mp3 +0 -0
  15. package/demo/audio/voices-manifest.json +16 -16
  16. package/demo/workflow-demo.html +1528 -411
  17. package/hooks/api-workflow-check.py +2 -0
  18. package/hooks/enforce-deep-research.py +180 -0
  19. package/hooks/enforce-disambiguation.py +149 -0
  20. package/hooks/enforce-documentation.py +187 -0
  21. package/hooks/enforce-environment.py +249 -0
  22. package/hooks/enforce-interview.py +64 -1
  23. package/hooks/enforce-refactor.py +187 -0
  24. package/hooks/enforce-research.py +93 -46
  25. package/hooks/enforce-schema.py +186 -0
  26. package/hooks/enforce-scope.py +156 -0
  27. package/hooks/enforce-tdd-red.py +246 -0
  28. package/hooks/enforce-verify.py +186 -0
  29. package/hooks/verify-after-green.py +136 -6
  30. package/package.json +2 -1
  31. package/scripts/collect-test-results.ts +404 -0
  32. package/scripts/extract-parameters.ts +483 -0
  33. package/scripts/generate-test-manifest.ts +520 -0
  34. package/templates/CLAUDE-SECTION.md +84 -0
  35. package/templates/api-dev-state.json +45 -5
  36. package/templates/api-test/page.tsx +315 -0
  37. package/templates/api-test/test-structure/route.ts +269 -0
  38. package/templates/settings.json +36 -0
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  Hook: PreToolUse for Write/Edit
4
- Purpose: Block writing API code if research phase not complete
4
+ Purpose: Block writing API code if research phase not complete WITH USER CHECKPOINT
5
5
 
6
- This hook runs BEFORE Claude can write or edit files in /api/ directories.
7
- It checks the api-dev-state.json file to ensure research was completed first.
6
+ Phase 2 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
8
11
 
9
12
  Returns:
10
13
  - {"permissionDecision": "allow"} - Let the tool run
@@ -14,97 +17,141 @@ import json
14
17
  import sys
15
18
  from pathlib import Path
16
19
 
17
- # State file is in .claude/ directory (sibling to hooks/)
18
20
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
19
21
 
22
+ # Minimum sources required
23
+ MIN_SOURCES = 2
24
+
20
25
 
21
26
  def main():
22
- # Read hook input from stdin (Claude Code passes tool info as JSON)
23
27
  try:
24
28
  input_data = json.load(sys.stdin)
25
29
  except json.JSONDecodeError:
26
- # If we can't parse input, allow (fail open for safety)
27
30
  print(json.dumps({"permissionDecision": "allow"}))
28
31
  sys.exit(0)
29
32
 
30
- tool_name = input_data.get("tool_name", "")
31
33
  tool_input = input_data.get("tool_input", {})
32
-
33
- # Get the file path being written/edited
34
34
  file_path = tool_input.get("file_path", "")
35
35
 
36
36
  # Only enforce for API route files
37
- # Check for both /api/ and /api-test/ patterns
38
37
  if "/api/" not in file_path and "/api-test/" not in file_path:
39
- # Not an API file, allow without checking
40
38
  print(json.dumps({"permissionDecision": "allow"}))
41
39
  sys.exit(0)
42
40
 
43
- # Also skip for test files - tests should be written before research completes
44
- # (TDD Red phase)
41
+ # Skip test files - TDD Red allows tests before research complete
45
42
  if ".test." in file_path or "/__tests__/" in file_path:
46
43
  print(json.dumps({"permissionDecision": "allow"}))
47
44
  sys.exit(0)
48
45
 
49
- # Skip for documentation/config files
50
46
  if file_path.endswith(".md") or file_path.endswith(".json"):
51
47
  print(json.dumps({"permissionDecision": "allow"}))
52
48
  sys.exit(0)
53
49
 
54
- # Check if state file exists
55
50
  if not STATE_FILE.exists():
56
51
  print(json.dumps({
57
52
  "permissionDecision": "deny",
58
53
  "reason": """❌ API development state not initialized.
59
54
 
60
- Before writing API implementation code, you must:
61
- 1. Run /api-create [endpoint-name] to start the workflow
62
- OR
63
- 2. Run /api-research [library-name] to research dependencies
64
-
65
- This ensures you're working with current documentation, not outdated training data."""
55
+ Run /api-create [endpoint-name] to start the workflow."""
66
56
  }))
67
57
  sys.exit(0)
68
58
 
69
- # Load and check state
70
59
  try:
71
60
  state = json.loads(STATE_FILE.read_text())
72
61
  except json.JSONDecodeError:
73
- # Corrupted state file, allow but warn
74
62
  print(json.dumps({"permissionDecision": "allow"}))
75
63
  sys.exit(0)
76
64
 
77
- # Check research phase status
65
+ endpoint = state.get("endpoint", "unknown")
78
66
  phases = state.get("phases", {})
79
67
  research = phases.get("research_initial", {})
80
- research_status = research.get("status", "not_started")
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")
81
85
 
82
- if research_status != "complete":
83
- sources_count = len(research.get("sources", []))
84
86
  print(json.dumps({
85
87
  "permissionDecision": "deny",
86
- "reason": f"""❌ Cannot write API implementation code yet.
87
-
88
- RESEARCH PHASE INCOMPLETE
89
- Current status: {research_status}
90
- Sources consulted: {sources_count}
91
-
92
- REQUIRED ACTIONS:
93
- 1. Complete research phase first
94
- 2. Run: /api-research [library-name]
95
- 3. Ensure Context7 or WebSearch has been used
96
-
97
- WHY THIS MATTERS:
98
- - Implementation must match CURRENT API documentation
99
- - Training data may be outdated
100
- - All parameters must be discovered before coding
101
-
102
- Once research is complete, you can proceed with implementation."""
88
+ "reason": f"""❌ BLOCKED: Initial research (Phase 2) 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."""
103
144
  }))
104
145
  sys.exit(0)
105
146
 
106
- # Research complete, allow writing
107
- print(json.dumps({"permissionDecision": "allow"}))
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
+ }))
108
155
  sys.exit(0)
109
156
 
110
157
 
@@ -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 5 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 5) 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 3
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,156 @@
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 1 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
+
75
+ if status != "complete" or not user_confirmed:
76
+ endpoint_path = scope.get("endpoint_path", f"/api/v2/{endpoint}")
77
+ modifications = scope.get("modifications", [])
78
+
79
+ missing = []
80
+ if not user_question_asked:
81
+ missing.append("User question (AskUserQuestion not used)")
82
+ if not user_confirmed:
83
+ missing.append("User confirmation (user hasn't said 'yes')")
84
+
85
+ print(json.dumps({
86
+ "permissionDecision": "deny",
87
+ "reason": f"""❌ BLOCKED: Scope confirmation (Phase 1) not complete.
88
+
89
+ Status: {status}
90
+ User question asked: {user_question_asked}
91
+ User confirmed: {user_confirmed}
92
+ Proposed path: {endpoint_path}
93
+ Modifications: {len(modifications)}
94
+
95
+ MISSING:
96
+ {chr(10).join(f" • {m}" for m in missing)}
97
+
98
+ ═══════════════════════════════════════════════════════════
99
+ ⚠️ GET USER CONFIRMATION OF SCOPE
100
+ ═══════════════════════════════════════════════════════════
101
+
102
+ REQUIRED STEPS:
103
+
104
+ 1. Present your understanding:
105
+ ┌───────────────────────────────────────────────────────┐
106
+ │ SCOPE CONFIRMATION │
107
+ │ │
108
+ │ I understand you want: {endpoint_path} │
109
+ │ Purpose: [describe inferred purpose] │
110
+ │ External API: [service name if any] │
111
+ │ │
112
+ │ Is this correct? [Y/n] │
113
+ │ Any modifications needed? ____ │
114
+ └───────────────────────────────────────────────────────┘
115
+
116
+ 2. USE AskUserQuestion:
117
+ question: "Is this scope correct? Any modifications?"
118
+ options: [
119
+ {{"value": "yes", "label": "Yes, proceed"}},
120
+ {{"value": "modify", "label": "I have modifications"}},
121
+ {{"value": "no", "label": "No, let me clarify"}}
122
+ ]
123
+
124
+ 3. If user says "modify" or "no":
125
+ • Ask for their modifications
126
+ • Record them in scope.modifications
127
+ • LOOP BACK and confirm again
128
+
129
+ 4. If user says "yes":
130
+ • Set scope.user_confirmed = true
131
+ • Set scope.user_question_asked = true
132
+ • Set scope.status = "complete"
133
+
134
+ WHY: Prevents building the wrong thing."""
135
+ }))
136
+ sys.exit(0)
137
+
138
+ # Scope confirmed - inject context
139
+ endpoint_path = scope.get("endpoint_path", f"/api/v2/{endpoint}")
140
+ modifications = scope.get("modifications", [])
141
+
142
+ context = [f"✅ Scope confirmed: {endpoint_path}"]
143
+ if modifications:
144
+ context.append("User modifications:")
145
+ for mod in modifications[:3]:
146
+ context.append(f" • {mod}")
147
+
148
+ print(json.dumps({
149
+ "permissionDecision": "allow",
150
+ "message": "\n".join(context)
151
+ }))
152
+ sys.exit(0)
153
+
154
+
155
+ if __name__ == "__main__":
156
+ main()