@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
@@ -29,6 +29,8 @@ REQUIRED_PHASES = [
29
29
  ("interview", "User interview"),
30
30
  ("tdd_red", "TDD Red phase (failing tests written)"),
31
31
  ("tdd_green", "TDD Green phase (tests passing)"),
32
+ ("verify", "Verification phase (re-checked against docs)"),
33
+ ("documentation", "Documentation updates (manifest/research cached)"),
32
34
  ]
33
35
 
34
36
  # Phases that SHOULD be complete (warning but don't block)
@@ -0,0 +1,180 @@
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 4 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
+ if status != "complete":
76
+ user_question_asked = research_deep.get("user_question_asked", False)
77
+ user_approved = research_deep.get("user_approved", False)
78
+ proposals_shown = research_deep.get("proposals_shown", False)
79
+ approved_searches = research_deep.get("approved_searches", [])
80
+ executed_searches = research_deep.get("executed_searches", [])
81
+ skipped_searches = research_deep.get("skipped_searches", [])
82
+
83
+ # Calculate pending
84
+ pending = [s for s in approved_searches if s not in executed_searches and s not in skipped_searches]
85
+
86
+ missing = []
87
+ if not proposals_shown:
88
+ missing.append("Proposed searches not shown to user")
89
+ if not user_question_asked:
90
+ missing.append("User approval question (AskUserQuestion not used)")
91
+ if not user_approved:
92
+ missing.append("User hasn't approved the search list")
93
+ if pending:
94
+ missing.append(f"Approved searches not executed ({len(pending)} pending)")
95
+
96
+ print(json.dumps({
97
+ "permissionDecision": "deny",
98
+ "reason": f"""❌ BLOCKED: Deep research (Phase 4) not complete.
99
+
100
+ Status: {status}
101
+ Proposed searches: {len(proposed)}
102
+ User shown proposals: {proposals_shown}
103
+ User question asked: {user_question_asked}
104
+ User approved: {user_approved}
105
+ Approved: {len(approved_searches)}
106
+ Executed: {len(executed_searches)}
107
+ Skipped: {len(skipped_searches)}
108
+ Pending: {len(pending)}
109
+
110
+ MISSING:
111
+ {chr(10).join(f" • {m}" for m in missing)}
112
+
113
+ ═══════════════════════════════════════════════════════════
114
+ ⚠️ GET USER APPROVAL FOR DEEP RESEARCH
115
+ ═══════════════════════════════════════════════════════════
116
+
117
+ REQUIRED STEPS:
118
+
119
+ 1. Based on interview, PROPOSE targeted searches:
120
+ ┌───────────────────────────────────────────────────────┐
121
+ │ PROPOSED DEEP RESEARCH │
122
+ │ │
123
+ │ Based on your interview answers, I want to research: │
124
+ │ │
125
+ │ [x] Error response format (for error handling) │
126
+ │ [x] Rate limiting behavior (caching selected) │
127
+ │ [ ] Webhook support (not selected in interview) │
128
+ │ [x] Authentication edge cases │
129
+ │ │
130
+ │ Approve these searches? [Y] │
131
+ │ Add more: ____ │
132
+ │ Skip and proceed: [n] │
133
+ └───────────────────────────────────────────────────────┘
134
+
135
+ 2. USE AskUserQuestion:
136
+ question: "Approve these deep research searches?"
137
+ options: [
138
+ {{"value": "approve", "label": "Yes, run these searches"}},
139
+ {{"value": "add", "label": "Add more - I also need [topic]"}},
140
+ {{"value": "skip", "label": "Skip deep research, proceed to schema"}}
141
+ ]
142
+
143
+ 3. If user says "add":
144
+ • Ask what additional topics they need
145
+ • Add to proposed_searches
146
+ • LOOP BACK and show updated list
147
+
148
+ 4. If user says "approve":
149
+ • Execute each approved search
150
+ • Record results in executed_searches
151
+
152
+ 5. If user says "skip":
153
+ • Record all as skipped_searches with reason
154
+ • Proceed to schema
155
+
156
+ 6. After all searches complete (or skipped):
157
+ • Set research_deep.user_approved = true
158
+ • Set research_deep.user_question_asked = true
159
+ • Set research_deep.proposals_shown = true
160
+ • Set research_deep.status = "complete"
161
+
162
+ WHY: Research is ADAPTIVE based on interview, not shotgun."""
163
+ }))
164
+ sys.exit(0)
165
+
166
+ # Complete
167
+ executed = research_deep.get("executed_searches", [])
168
+ skipped = research_deep.get("skipped_searches", [])
169
+ print(json.dumps({
170
+ "permissionDecision": "allow",
171
+ "message": f"""✅ Deep research complete.
172
+ Executed: {len(executed)} searches
173
+ Skipped: {len(skipped)} (with reasons)
174
+ User approved the search plan."""
175
+ }))
176
+ sys.exit(0)
177
+
178
+
179
+ if __name__ == "__main__":
180
+ main()
@@ -0,0 +1,149 @@
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 0 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 0 (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
+ if status != "complete":
78
+ search_variations = disambiguation.get("search_variations", [])
79
+ user_question_asked = disambiguation.get("user_question_asked", False)
80
+ user_selected = disambiguation.get("user_selected", None)
81
+
82
+ # Check what's missing
83
+ missing = []
84
+ if len(search_variations) < MIN_SEARCH_VARIATIONS:
85
+ missing.append(f"Search variations ({len(search_variations)}/{MIN_SEARCH_VARIATIONS})")
86
+ if not user_question_asked:
87
+ missing.append("User question (AskUserQuestion not used)")
88
+ if not user_selected:
89
+ missing.append("User selection (no choice recorded)")
90
+
91
+ print(json.dumps({
92
+ "permissionDecision": "deny",
93
+ "reason": f"""❌ BLOCKED: Disambiguation phase (Phase 0) not complete.
94
+
95
+ Status: {status}
96
+ Search variations: {len(search_variations)}
97
+ User question asked: {user_question_asked}
98
+ User selection: {user_selected or "None"}
99
+
100
+ MISSING:
101
+ {chr(10).join(f" • {m}" for m in missing)}
102
+
103
+ ═══════════════════════════════════════════════════════════
104
+ ⚠️ COMPLETE DISAMBIGUATION WITH USER CONFIRMATION
105
+ ═══════════════════════════════════════════════════════════
106
+
107
+ REQUIRED STEPS:
108
+
109
+ 1. Search 2-3 variations:
110
+ • WebSearch: "{endpoint}"
111
+ • WebSearch: "{endpoint} API"
112
+ • WebSearch: "{endpoint} SDK npm package"
113
+
114
+ 2. USE AskUserQuestion with options:
115
+ ┌───────────────────────────────────────────────────────┐
116
+ │ I found multiple things matching "{endpoint}": │
117
+ │ │
118
+ │ [A] The official REST API │
119
+ │ [B] The npm/SDK wrapper package │
120
+ │ [C] Both (API + SDK) │
121
+ │ [D] Something else: ____ │
122
+ │ │
123
+ │ Which should this endpoint use? │
124
+ └───────────────────────────────────────────────────────┘
125
+
126
+ 3. Record user's choice in state:
127
+ disambiguation.user_selected = "A" (or user's choice)
128
+ disambiguation.user_question_asked = true
129
+ disambiguation.status = "complete"
130
+
131
+ 4. LOOP BACK if user is still unsure - search more variations
132
+
133
+ WHY: Different interpretations = different implementations."""
134
+ }))
135
+ sys.exit(0)
136
+
137
+ # Complete - inject context
138
+ selected = disambiguation.get("user_selected", "")
139
+ print(json.dumps({
140
+ "permissionDecision": "allow",
141
+ "message": f"""✅ Disambiguation complete.
142
+ User selected: {selected}
143
+ Proceeding with this interpretation."""
144
+ }))
145
+ sys.exit(0)
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PreToolUse for Write/Edit (and Stop)
4
+ Purpose: Block completion until documentation confirmed WITH USER REVIEW
5
+
6
+ Phase 11 (Documentation) requires:
7
+ 1. Update api-tests-manifest.json
8
+ 2. Cache research to .claude/research/
9
+ 3. Update OpenAPI spec if applicable
10
+ 4. SHOW documentation checklist to user
11
+ 5. USE AskUserQuestion: "Documentation complete? [Y/n]"
12
+ 6. Only allow completion when user confirms
13
+
14
+ Returns:
15
+ - {"permissionDecision": "allow"} - Let the tool run
16
+ - {"permissionDecision": "deny", "reason": "..."} - Block with explanation
17
+ """
18
+ import json
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
23
+
24
+
25
+ def main():
26
+ try:
27
+ input_data = json.load(sys.stdin)
28
+ except json.JSONDecodeError:
29
+ print(json.dumps({"permissionDecision": "allow"}))
30
+ sys.exit(0)
31
+
32
+ tool_input = input_data.get("tool_input", {})
33
+ file_path = tool_input.get("file_path", "")
34
+
35
+ # Only enforce for documentation files or when trying to mark workflow complete
36
+ is_manifest = "api-tests-manifest.json" in file_path
37
+ is_openapi = "openapi" in file_path.lower() and file_path.endswith((".json", ".yaml", ".yml"))
38
+ is_readme = file_path.endswith("README.md") and "/api/" in file_path
39
+
40
+ # Also check for state file updates (marking complete)
41
+ is_state_update = "api-dev-state.json" in file_path
42
+
43
+ if not is_manifest and not is_openapi and not is_readme and not is_state_update:
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
+ tdd_refactor = phases.get("tdd_refactor", {})
60
+ documentation = phases.get("documentation", {})
61
+
62
+ # Only enforce after refactor is complete
63
+ if tdd_refactor.get("status") != "complete":
64
+ # Allow documentation updates during development
65
+ print(json.dumps({"permissionDecision": "allow"}))
66
+ sys.exit(0)
67
+
68
+ status = documentation.get("status", "not_started")
69
+
70
+ if status != "complete":
71
+ user_question_asked = documentation.get("user_question_asked", False)
72
+ user_confirmed = documentation.get("user_confirmed", False)
73
+ checklist_shown = documentation.get("checklist_shown", False)
74
+ manifest_updated = documentation.get("manifest_updated", False)
75
+ research_cached = documentation.get("research_cached", False)
76
+ openapi_updated = documentation.get("openapi_updated", False)
77
+
78
+ missing = []
79
+ if not manifest_updated:
80
+ missing.append("api-tests-manifest.json not updated")
81
+ if not research_cached:
82
+ missing.append("Research not cached to .claude/research/")
83
+ if not checklist_shown:
84
+ missing.append("Documentation checklist not shown to user")
85
+ if not user_question_asked:
86
+ missing.append("User confirmation question (AskUserQuestion not used)")
87
+ if not user_confirmed:
88
+ missing.append("User hasn't confirmed documentation complete")
89
+
90
+ print(json.dumps({
91
+ "permissionDecision": "deny",
92
+ "reason": f"""❌ BLOCKED: Documentation (Phase 11) not complete.
93
+
94
+ Status: {status}
95
+ Manifest updated: {manifest_updated}
96
+ Research cached: {research_cached}
97
+ OpenAPI updated: {openapi_updated}
98
+ Checklist shown: {checklist_shown}
99
+ User question asked: {user_question_asked}
100
+ User confirmed: {user_confirmed}
101
+
102
+ MISSING:
103
+ {chr(10).join(f" • {m}" for m in missing)}
104
+
105
+ ═══════════════════════════════════════════════════════════
106
+ ⚠️ GET USER CONFIRMATION FOR DOCUMENTATION
107
+ ═══════════════════════════════════════════════════════════
108
+
109
+ REQUIRED STEPS:
110
+
111
+ 1. Update api-tests-manifest.json with:
112
+ • Endpoint path, method, description
113
+ • Request/response schemas
114
+ • Test coverage info
115
+ • Code examples
116
+ • Testing notes
117
+
118
+ 2. Cache research to .claude/research/{endpoint}/:
119
+ • sources.json - URLs and summaries
120
+ • interview.json - User decisions
121
+ • schema.json - Final Zod schemas
122
+
123
+ 3. Update OpenAPI spec (if applicable):
124
+ • Add endpoint definition
125
+ • Document parameters
126
+ • Document responses
127
+
128
+ 4. SHOW documentation checklist to user:
129
+ ┌───────────────────────────────────────────────────────┐
130
+ │ DOCUMENTATION CHECKLIST │
131
+ │ │
132
+ │ ✓ api-tests-manifest.json │
133
+ │ • Added {endpoint} entry │
134
+ │ • 8 test scenarios documented │
135
+ │ • Code examples included │
136
+ │ │
137
+ │ ✓ Research Cache │
138
+ │ • .claude/research/{endpoint}/sources.json │
139
+ │ • .claude/research/{endpoint}/interview.json │
140
+ │ │
141
+ │ {'✓' if openapi_updated else '⏭'} OpenAPI Spec │
142
+ │ • {'Updated' if openapi_updated else 'Skipped (internal API)'} │
143
+ │ │
144
+ │ All documentation complete? [Y] │
145
+ │ Need to add something? [n] ____ │
146
+ └───────────────────────────────────────────────────────┘
147
+
148
+ 5. USE AskUserQuestion:
149
+ question: "Documentation checklist complete?"
150
+ options: [
151
+ {{"value": "confirm", "label": "Yes, all documentation is done"}},
152
+ {{"value": "add", "label": "No, I need to add [what]"}},
153
+ {{"value": "skip", "label": "Skip docs for now (not recommended)"}}
154
+ ]
155
+
156
+ 6. If user says "add":
157
+ • Ask what documentation is missing
158
+ • Update the relevant files
159
+ • LOOP BACK and show updated checklist
160
+
161
+ 7. If user says "confirm":
162
+ • Set documentation.user_confirmed = true
163
+ • Set documentation.user_question_asked = true
164
+ • Set documentation.checklist_shown = true
165
+ • Set documentation.manifest_updated = true
166
+ • Set documentation.research_cached = true
167
+ • Set documentation.status = "complete"
168
+ • Mark entire workflow as complete!
169
+
170
+ WHY: Documentation ensures next developer (or future Claude) has context."""
171
+ }))
172
+ sys.exit(0)
173
+
174
+ # Documentation complete
175
+ print(json.dumps({
176
+ "permissionDecision": "allow",
177
+ "message": f"""✅ Documentation complete for {endpoint}.
178
+ Manifest updated: {manifest_updated}
179
+ Research cached: {research_cached}
180
+ OpenAPI updated: {openapi_updated}
181
+ User confirmed documentation is complete."""
182
+ }))
183
+ sys.exit(0)
184
+
185
+
186
+ if __name__ == "__main__":
187
+ main()