@hustle-together/api-dev-tools 3.3.0 → 3.5.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 (45) hide show
  1. package/README.md +712 -377
  2. package/commands/api-create.md +68 -23
  3. package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +1 -1
  4. package/demo/hustle-together/blog/interview-driven-api-development.html +1 -1
  5. package/demo/hustle-together/blog/tdd-for-ai.html +1 -1
  6. package/demo/hustle-together/index.html +2 -2
  7. package/demo/workflow-demo-v3.5-backup.html +5008 -0
  8. package/demo/workflow-demo.html +5137 -3805
  9. package/hooks/enforce-deep-research.py +6 -1
  10. package/hooks/enforce-disambiguation.py +7 -1
  11. package/hooks/enforce-documentation.py +6 -1
  12. package/hooks/enforce-environment.py +5 -1
  13. package/hooks/enforce-interview.py +5 -1
  14. package/hooks/enforce-refactor.py +3 -1
  15. package/hooks/enforce-schema.py +0 -0
  16. package/hooks/enforce-scope.py +5 -1
  17. package/hooks/enforce-tdd-red.py +5 -1
  18. package/hooks/enforce-verify.py +0 -0
  19. package/hooks/track-tool-use.py +167 -0
  20. package/hooks/verify-implementation.py +0 -0
  21. package/package.json +1 -1
  22. package/templates/api-dev-state.json +24 -0
  23. package/demo/audio/audio-sync.js +0 -295
  24. package/demo/audio/generate-all-narrations.js +0 -581
  25. package/demo/audio/generate-narration.js +0 -486
  26. package/demo/audio/generate-voice-previews.js +0 -140
  27. package/demo/audio/narration-adam-timing.json +0 -4675
  28. package/demo/audio/narration-adam.mp3 +0 -0
  29. package/demo/audio/narration-creature-timing.json +0 -4675
  30. package/demo/audio/narration-creature.mp3 +0 -0
  31. package/demo/audio/narration-gaming-timing.json +0 -4675
  32. package/demo/audio/narration-gaming.mp3 +0 -0
  33. package/demo/audio/narration-hope-timing.json +0 -4675
  34. package/demo/audio/narration-hope.mp3 +0 -0
  35. package/demo/audio/narration-mark-timing.json +0 -4675
  36. package/demo/audio/narration-mark.mp3 +0 -0
  37. package/demo/audio/narration-timing.json +0 -3614
  38. package/demo/audio/narration-timing.sample.json +0 -48
  39. package/demo/audio/narration.mp3 +0 -0
  40. package/demo/audio/previews/manifest.json +0 -30
  41. package/demo/audio/previews/preview-creature.mp3 +0 -0
  42. package/demo/audio/previews/preview-gaming.mp3 +0 -0
  43. package/demo/audio/previews/preview-hope.mp3 +0 -0
  44. package/demo/audio/previews/preview-mark.mp3 +0 -0
  45. package/demo/audio/voices-manifest.json +0 -50
@@ -72,7 +72,9 @@ def main():
72
72
  print(json.dumps({"permissionDecision": "allow"}))
73
73
  sys.exit(0)
74
74
 
75
- if status != "complete":
75
+ phase_exit_confirmed = research_deep.get("phase_exit_confirmed", False)
76
+
77
+ if status != "complete" or not phase_exit_confirmed:
76
78
  user_question_asked = research_deep.get("user_question_asked", False)
77
79
  user_approved = research_deep.get("user_approved", False)
78
80
  proposals_shown = research_deep.get("proposals_shown", False)
@@ -92,6 +94,8 @@ def main():
92
94
  missing.append("User hasn't approved the search list")
93
95
  if pending:
94
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)")
95
99
 
96
100
  print(json.dumps({
97
101
  "permissionDecision": "deny",
@@ -106,6 +110,7 @@ Approved: {len(approved_searches)}
106
110
  Executed: {len(executed_searches)}
107
111
  Skipped: {len(skipped_searches)}
108
112
  Pending: {len(pending)}
113
+ Phase exit confirmed: {phase_exit_confirmed}
109
114
 
110
115
  MISSING:
111
116
  {chr(10).join(f" • {m}" for m in missing)}
@@ -74,7 +74,10 @@ Phase 0 (Disambiguation) is required before any implementation."""
74
74
  disambiguation = phases.get("disambiguation", {})
75
75
  status = disambiguation.get("status", "not_started")
76
76
 
77
- if status != "complete":
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:
78
81
  search_variations = disambiguation.get("search_variations", [])
79
82
  user_question_asked = disambiguation.get("user_question_asked", False)
80
83
  user_selected = disambiguation.get("user_selected", None)
@@ -87,6 +90,8 @@ Phase 0 (Disambiguation) is required before any implementation."""
87
90
  missing.append("User question (AskUserQuestion not used)")
88
91
  if not user_selected:
89
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)")
90
95
 
91
96
  print(json.dumps({
92
97
  "permissionDecision": "deny",
@@ -96,6 +101,7 @@ Status: {status}
96
101
  Search variations: {len(search_variations)}
97
102
  User question asked: {user_question_asked}
98
103
  User selection: {user_selected or "None"}
104
+ Phase exit confirmed: {phase_exit_confirmed}
99
105
 
100
106
  MISSING:
101
107
  {chr(10).join(f" • {m}" for m in missing)}
@@ -67,7 +67,9 @@ def main():
67
67
 
68
68
  status = documentation.get("status", "not_started")
69
69
 
70
- if status != "complete":
70
+ phase_exit_confirmed = documentation.get("phase_exit_confirmed", False)
71
+
72
+ if status != "complete" or not phase_exit_confirmed:
71
73
  user_question_asked = documentation.get("user_question_asked", False)
72
74
  user_confirmed = documentation.get("user_confirmed", False)
73
75
  checklist_shown = documentation.get("checklist_shown", False)
@@ -86,6 +88,8 @@ def main():
86
88
  missing.append("User confirmation question (AskUserQuestion not used)")
87
89
  if not user_confirmed:
88
90
  missing.append("User hasn't confirmed documentation complete")
91
+ if not phase_exit_confirmed:
92
+ missing.append("Phase exit confirmation (user must explicitly approve to complete)")
89
93
 
90
94
  print(json.dumps({
91
95
  "permissionDecision": "deny",
@@ -98,6 +102,7 @@ OpenAPI updated: {openapi_updated}
98
102
  Checklist shown: {checklist_shown}
99
103
  User question asked: {user_question_asked}
100
104
  User confirmed: {user_confirmed}
105
+ Phase exit confirmed: {phase_exit_confirmed}
101
106
 
102
107
  MISSING:
103
108
  {chr(10).join(f" • {m}" for m in missing)}
@@ -131,9 +131,10 @@ def main():
131
131
  user_question_asked = environment_check.get("user_question_asked", False)
132
132
  user_ready = environment_check.get("user_ready", False)
133
133
  env_shown = environment_check.get("env_shown", False)
134
+ phase_exit_confirmed = environment_check.get("phase_exit_confirmed", False)
134
135
 
135
136
  # Check if environment check is complete
136
- if env_status != "complete":
137
+ if env_status != "complete" or not phase_exit_confirmed:
137
138
  # Infer required keys if not already set
138
139
  if not keys_required:
139
140
  interview = phases.get("interview", {})
@@ -157,6 +158,8 @@ def main():
157
158
  missing_steps.append("User readiness question (AskUserQuestion not used)")
158
159
  if not user_ready:
159
160
  missing_steps.append("User hasn't confirmed readiness for TDD")
161
+ if not phase_exit_confirmed:
162
+ missing_steps.append("Phase exit confirmation (user must explicitly approve to proceed)")
160
163
 
161
164
  print(json.dumps({
162
165
  "permissionDecision": "deny",
@@ -169,6 +172,7 @@ Missing: {len(missing)}
169
172
  User shown env: {env_shown}
170
173
  User question asked: {user_question_asked}
171
174
  User ready: {user_ready}
175
+ Phase exit confirmed: {phase_exit_confirmed}
172
176
 
173
177
  MISSING:
174
178
  {chr(10).join(f" • {m}" for m in missing_steps)}
@@ -262,15 +262,18 @@ Reset the interview and ask with options based on research."""
262
262
  # Check 6: FINAL USER CONFIRMATION - must confirm interview is complete
263
263
  user_question_asked_final = interview.get("user_question_asked", False)
264
264
  user_completed = interview.get("user_completed", False)
265
+ phase_exit_confirmed = interview.get("phase_exit_confirmed", False)
265
266
  decisions = interview.get("decisions", {})
266
267
 
267
- if not user_completed or not user_question_asked_final:
268
+ if not user_completed or not user_question_asked_final or not phase_exit_confirmed:
268
269
  decision_summary = _build_decision_summary(decisions)
269
270
  missing = []
270
271
  if not user_question_asked_final:
271
272
  missing.append("Final confirmation question (AskUserQuestion not used)")
272
273
  if not user_completed:
273
274
  missing.append("User hasn't confirmed interview complete")
275
+ if not phase_exit_confirmed:
276
+ missing.append("Phase exit confirmation (user must explicitly approve to proceed)")
274
277
 
275
278
  print(json.dumps({
276
279
  "permissionDecision": "deny",
@@ -279,6 +282,7 @@ Reset the interview and ask with options based on research."""
279
282
  Questions asked: {len(questions)}
280
283
  Structured questions: {actual_structured}
281
284
  User final confirmation: {user_completed}
285
+ Phase exit confirmed: {phase_exit_confirmed}
282
286
 
283
287
  MISSING:
284
288
  {chr(10).join(f" • {m}" for m in missing)}
@@ -119,8 +119,9 @@ def main():
119
119
  gaps_found = verify.get("gaps_found", 0)
120
120
  gaps_fixed = verify.get("gaps_fixed", 0)
121
121
  intentional_omissions = verify.get("intentional_omissions", [])
122
+ phase_exit_confirmed = verify.get("phase_exit_confirmed", False)
122
123
 
123
- if verify_status != "complete":
124
+ if verify_status != "complete" or not phase_exit_confirmed:
124
125
  print(json.dumps({
125
126
  "permissionDecision": "deny",
126
127
  "reason": f"""❌ BLOCKED: Verify phase (Phase 9) not complete.
@@ -129,6 +130,7 @@ Current status: {verify_status}
129
130
  Gaps found: {gaps_found}
130
131
  Gaps fixed: {gaps_fixed}
131
132
  Intentional omissions: {len(intentional_omissions)}
133
+ Phase exit confirmed: {phase_exit_confirmed}
132
134
 
133
135
  ═══════════════════════════════════════════════════════════
134
136
  ⚠️ VERIFY BEFORE REFACTORING
File without changes
@@ -71,8 +71,9 @@ def main():
71
71
  status = scope.get("status", "not_started")
72
72
  user_confirmed = scope.get("user_confirmed", False)
73
73
  user_question_asked = scope.get("user_question_asked", False)
74
+ phase_exit_confirmed = scope.get("phase_exit_confirmed", False)
74
75
 
75
- if status != "complete" or not user_confirmed:
76
+ if status != "complete" or not user_confirmed or not phase_exit_confirmed:
76
77
  endpoint_path = scope.get("endpoint_path", f"/api/v2/{endpoint}")
77
78
  modifications = scope.get("modifications", [])
78
79
 
@@ -81,6 +82,8 @@ def main():
81
82
  missing.append("User question (AskUserQuestion not used)")
82
83
  if not user_confirmed:
83
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)")
84
87
 
85
88
  print(json.dumps({
86
89
  "permissionDecision": "deny",
@@ -89,6 +92,7 @@ def main():
89
92
  Status: {status}
90
93
  User question asked: {user_question_asked}
91
94
  User confirmed: {user_confirmed}
95
+ Phase exit confirmed: {phase_exit_confirmed}
92
96
  Proposed path: {endpoint_path}
93
97
  Modifications: {len(modifications)}
94
98
 
@@ -123,9 +123,10 @@ Example test structure:
123
123
  user_approved = tdd_red.get("user_approved", False)
124
124
  matrix_shown = tdd_red.get("matrix_shown", False)
125
125
  test_scenarios = tdd_red.get("test_scenarios", [])
126
+ phase_exit_confirmed = tdd_red.get("phase_exit_confirmed", False)
126
127
 
127
128
  # Check if TDD Red phase is complete
128
- if tdd_red_status != "complete":
129
+ if tdd_red_status != "complete" or not phase_exit_confirmed:
129
130
  test_exists, expected_path = find_test_file(file_path)
130
131
 
131
132
  # Check what's missing for user checkpoint
@@ -138,6 +139,8 @@ Example test structure:
138
139
  missing.append("User approval question (AskUserQuestion not used)")
139
140
  if not user_approved:
140
141
  missing.append("User hasn't approved the test plan")
142
+ if not phase_exit_confirmed:
143
+ missing.append("Phase exit confirmation (user must explicitly approve to proceed)")
141
144
 
142
145
  print(json.dumps({
143
146
  "permissionDecision": "deny",
@@ -149,6 +152,7 @@ Test file exists: {test_exists}
149
152
  Matrix shown: {matrix_shown}
150
153
  User question asked: {user_question_asked}
151
154
  User approved: {user_approved}
155
+ Phase exit confirmed: {phase_exit_confirmed}
152
156
  Scenarios: {len(test_scenarios)}
153
157
 
154
158
  MISSING:
File without changes
@@ -156,6 +156,36 @@ def main():
156
156
 
157
157
  interview["last_activity"] = datetime.now().isoformat()
158
158
 
159
+ # ========================================
160
+ # CRITICAL: Set user_question_asked flags
161
+ # This is what the enforcement hooks check!
162
+ # ========================================
163
+ interview["user_question_asked"] = True
164
+
165
+ # Also update the CURRENT phase based on workflow state
166
+ # Determine which phase we're in and set its user_question_asked flag
167
+ current_phase = _determine_current_phase(phases)
168
+ if current_phase and current_phase in phases:
169
+ phases[current_phase]["user_question_asked"] = True
170
+ # If user responded, also track that
171
+ if user_response:
172
+ phases[current_phase]["last_user_response"] = user_response[:200]
173
+ phases[current_phase]["last_question_timestamp"] = datetime.now().isoformat()
174
+
175
+ # ========================================
176
+ # CRITICAL: Detect phase exit confirmations
177
+ # This prevents Claude from self-answering
178
+ # ========================================
179
+ question_text = tool_input.get("question", "").lower()
180
+ question_type = _detect_question_type(question_text, options)
181
+ phases[current_phase]["last_question_type"] = question_type
182
+
183
+ # If this is an exit confirmation question AND user responded affirmatively
184
+ if question_type == "exit_confirmation":
185
+ # Check if user's response indicates approval/confirmation
186
+ if user_response and _is_affirmative_response(user_response, options):
187
+ phases[current_phase]["phase_exit_confirmed"] = True
188
+
159
189
  # Log for visibility
160
190
  if has_options:
161
191
  interview["last_structured_question"] = {
@@ -294,6 +324,143 @@ def main():
294
324
  sys.exit(0)
295
325
 
296
326
 
327
+ def _detect_question_type(question_text: str, options: list) -> str:
328
+ """
329
+ Detect the type of question being asked.
330
+ Returns: 'exit_confirmation', 'data_collection', 'clarification', or 'unknown'
331
+ """
332
+ question_lower = question_text.lower()
333
+
334
+ # Exit confirmation patterns - questions asking to proceed/continue/move to next phase
335
+ exit_patterns = [
336
+ "proceed",
337
+ "continue",
338
+ "ready to",
339
+ "move to",
340
+ "is this correct",
341
+ "all correct",
342
+ "looks correct",
343
+ "approve",
344
+ "approved",
345
+ "confirm",
346
+ "complete",
347
+ "shall i",
348
+ "should i proceed",
349
+ "does this match",
350
+ "ready for",
351
+ "start tdd",
352
+ "start tests",
353
+ "begin",
354
+ "next phase",
355
+ "move on",
356
+ "go ahead"
357
+ ]
358
+
359
+ # Check options for exit-like labels
360
+ option_labels = [opt.get("label", "").lower() for opt in options] if options else []
361
+ exit_option_patterns = [
362
+ "yes", "proceed", "continue", "approve", "confirm",
363
+ "ready", "looks good", "correct", "done", "complete"
364
+ ]
365
+
366
+ # If question matches exit patterns
367
+ for pattern in exit_patterns:
368
+ if pattern in question_lower:
369
+ return "exit_confirmation"
370
+
371
+ # If options suggest it's an exit confirmation
372
+ for opt_label in option_labels:
373
+ for pattern in exit_option_patterns:
374
+ if pattern in opt_label:
375
+ return "exit_confirmation"
376
+
377
+ # Data collection - asking for choices about implementation
378
+ data_patterns = [
379
+ "which", "what", "how should", "prefer", "want",
380
+ "format", "handling", "strategy", "method"
381
+ ]
382
+ for pattern in data_patterns:
383
+ if pattern in question_lower:
384
+ return "data_collection"
385
+
386
+ # Clarification - asking for more info
387
+ clarify_patterns = [
388
+ "clarify", "explain", "more detail", "what do you mean"
389
+ ]
390
+ for pattern in clarify_patterns:
391
+ if pattern in question_lower:
392
+ return "clarification"
393
+
394
+ return "unknown"
395
+
396
+
397
+ def _is_affirmative_response(response: str, options: list) -> bool:
398
+ """
399
+ Check if the user's response indicates approval/confirmation.
400
+ """
401
+ response_lower = response.lower().strip()
402
+
403
+ # Direct affirmative words
404
+ affirmative_words = [
405
+ "yes", "y", "proceed", "continue", "approve", "confirm",
406
+ "correct", "ready", "go", "ok", "okay", "looks good",
407
+ "sounds good", "perfect", "great", "fine", "done",
408
+ "all good", "looks correct", "is correct", "all correct"
409
+ ]
410
+
411
+ for word in affirmative_words:
412
+ if word in response_lower:
413
+ return True
414
+
415
+ # Check if response matches an affirmative option
416
+ if options:
417
+ for opt in options:
418
+ opt_label = opt.get("label", "").lower()
419
+ opt_value = opt.get("value", "").lower()
420
+
421
+ # If response matches an option that sounds affirmative
422
+ if opt_label in response_lower or response_lower in opt_label:
423
+ for aff in affirmative_words:
424
+ if aff in opt_label:
425
+ return True
426
+
427
+ # Check for negative responses (to avoid false positives)
428
+ negative_words = ["no", "change", "modify", "add more", "not yet", "wait"]
429
+ for word in negative_words:
430
+ if word in response_lower:
431
+ return False
432
+
433
+ return False
434
+
435
+
436
+ def _determine_current_phase(phases: dict) -> str:
437
+ """Determine which phase is currently active based on status."""
438
+ # Phase order - return first incomplete phase
439
+ phase_order = [
440
+ "disambiguation",
441
+ "scope",
442
+ "research_initial",
443
+ "interview",
444
+ "research_deep",
445
+ "schema_creation",
446
+ "environment_check",
447
+ "tdd_red",
448
+ "tdd_green",
449
+ "verify",
450
+ "tdd_refactor",
451
+ "documentation"
452
+ ]
453
+
454
+ for phase_name in phase_order:
455
+ phase = phases.get(phase_name, {})
456
+ status = phase.get("status", "not_started")
457
+ if status != "complete":
458
+ return phase_name
459
+
460
+ # All complete, return documentation
461
+ return "documentation"
462
+
463
+
297
464
  def create_initial_state():
298
465
  """Create initial state structure (v3.0.0)"""
299
466
  return {
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hustle-together/api-dev-tools",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "Interview-driven, research-first API development workflow with continuous verification loops for Claude Code",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -15,6 +15,8 @@
15
15
  "search_variations": [],
16
16
  "user_question_asked": false,
17
17
  "user_selected": null,
18
+ "phase_exit_confirmed": false,
19
+ "last_question_type": null,
18
20
  "description": "Pre-research disambiguation to clarify ambiguous requests"
19
21
  },
20
22
  "scope": {
@@ -22,6 +24,8 @@
22
24
  "confirmed": false,
23
25
  "user_question_asked": false,
24
26
  "user_confirmed": false,
27
+ "phase_exit_confirmed": false,
28
+ "last_question_type": null,
25
29
  "description": "Initial scope understanding and confirmation"
26
30
  },
27
31
  "research_initial": {
@@ -30,6 +34,8 @@
30
34
  "summary_shown": false,
31
35
  "user_question_asked": false,
32
36
  "user_approved": false,
37
+ "phase_exit_confirmed": false,
38
+ "last_question_type": null,
33
39
  "description": "Context7/WebSearch research for live documentation"
34
40
  },
35
41
  "interview": {
@@ -40,6 +46,8 @@
40
46
  "decisions": {},
41
47
  "user_question_asked": false,
42
48
  "user_completed": false,
49
+ "phase_exit_confirmed": false,
50
+ "last_question_type": null,
43
51
  "description": "Structured interview about requirements (generated FROM research)"
44
52
  },
45
53
  "research_deep": {
@@ -52,6 +60,8 @@
52
60
  "proposals_shown": false,
53
61
  "user_question_asked": false,
54
62
  "user_approved": false,
63
+ "phase_exit_confirmed": false,
64
+ "last_question_type": null,
55
65
  "description": "Deep dive based on interview answers (adaptive, not shotgun)"
56
66
  },
57
67
  "schema_creation": {
@@ -61,6 +71,8 @@
61
71
  "schema_shown": false,
62
72
  "user_question_asked": false,
63
73
  "user_confirmed": false,
74
+ "phase_exit_confirmed": false,
75
+ "last_question_type": null,
64
76
  "description": "Zod schema creation from research"
65
77
  },
66
78
  "environment_check": {
@@ -71,6 +83,8 @@
71
83
  "env_shown": false,
72
84
  "user_question_asked": false,
73
85
  "user_ready": false,
86
+ "phase_exit_confirmed": false,
87
+ "last_question_type": null,
74
88
  "description": "API key and environment verification"
75
89
  },
76
90
  "tdd_red": {
@@ -81,12 +95,16 @@
81
95
  "matrix_shown": false,
82
96
  "user_question_asked": false,
83
97
  "user_approved": false,
98
+ "phase_exit_confirmed": false,
99
+ "last_question_type": null,
84
100
  "description": "Write failing tests first"
85
101
  },
86
102
  "tdd_green": {
87
103
  "status": "not_started",
88
104
  "implementation_file": null,
89
105
  "all_tests_passing": false,
106
+ "phase_exit_confirmed": false,
107
+ "last_question_type": null,
90
108
  "description": "Minimal implementation to pass tests"
91
109
  },
92
110
  "verify": {
@@ -100,10 +118,14 @@
100
118
  "user_question_asked": false,
101
119
  "user_decided": false,
102
120
  "user_decision": null,
121
+ "phase_exit_confirmed": false,
122
+ "last_question_type": null,
103
123
  "description": "Re-research after Green to verify implementation matches docs"
104
124
  },
105
125
  "tdd_refactor": {
106
126
  "status": "not_started",
127
+ "phase_exit_confirmed": false,
128
+ "last_question_type": null,
107
129
  "description": "Code cleanup while keeping tests green"
108
130
  },
109
131
  "documentation": {
@@ -115,6 +137,8 @@
115
137
  "checklist_shown": false,
116
138
  "user_question_asked": false,
117
139
  "user_confirmed": false,
140
+ "phase_exit_confirmed": false,
141
+ "last_question_type": null,
118
142
  "description": "Update manifests, OpenAPI, cache research"
119
143
  }
120
144
  },