@hustle-together/api-dev-tools 1.8.0 → 2.0.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.
@@ -259,11 +259,61 @@ Reset the interview and ask with options based on research."""
259
259
  }))
260
260
  sys.exit(0)
261
261
 
262
- # All checks passed
263
- print(json.dumps({"permissionDecision": "allow"}))
262
+ # All checks passed - inject interview decisions as context reminder
263
+ decisions = interview.get("decisions", {})
264
+
265
+ if decisions:
266
+ # Build a reminder of what the user decided
267
+ decision_summary = _build_decision_summary(decisions)
268
+
269
+ # Allow but inject context about user decisions
270
+ print(json.dumps({
271
+ "permissionDecision": "allow",
272
+ "message": f"""✅ Interview complete. REMEMBER THE USER'S DECISIONS:
273
+
274
+ {decision_summary}
275
+
276
+ Your implementation MUST align with these choices.
277
+ The state file tracks these for consistency verification."""
278
+ }))
279
+ else:
280
+ print(json.dumps({"permissionDecision": "allow"}))
281
+
264
282
  sys.exit(0)
265
283
 
266
284
 
285
+ def _build_decision_summary(decisions: dict) -> str:
286
+ """Build a human-readable summary of user decisions from the interview."""
287
+ if not decisions:
288
+ return "No key decisions recorded."
289
+
290
+ lines = []
291
+ decision_labels = {
292
+ "provider": "AI Provider",
293
+ "purpose": "Primary Purpose",
294
+ "response_format": "Response Format",
295
+ "required_params": "Required Parameters",
296
+ "optional_params": "Optional Parameters",
297
+ "error_handling": "Error Handling",
298
+ "api_key_handling": "API Key Handling",
299
+ "external_services": "External Services",
300
+ }
301
+
302
+ for key, data in decisions.items():
303
+ label = decision_labels.get(key, key.replace("_", " ").title())
304
+ response = data.get("response", "")
305
+ value = data.get("value", "")
306
+
307
+ if value:
308
+ lines.append(f"• {label}: {value}")
309
+ elif response:
310
+ # Truncate long responses
311
+ short_response = response[:80] + "..." if len(response) > 80 else response
312
+ lines.append(f"• {label}: {short_response}")
313
+
314
+ return "\n".join(lines) if lines else "No key decisions recorded."
315
+
316
+
267
317
  def _build_research_based_example(research_queries: list) -> str:
268
318
  """Build an example question based on actual research queries."""
269
319
  if not research_queries:
@@ -61,7 +61,8 @@ def main():
61
61
  "status": "not_started",
62
62
  "questions": [],
63
63
  "user_question_count": 0,
64
- "structured_question_count": 0
64
+ "structured_question_count": 0,
65
+ "decisions": {} # Track key decisions for consistency checking
65
66
  })
66
67
 
67
68
  # Track the question
@@ -78,16 +79,62 @@ def main():
78
79
  structured_count = interview.get("structured_question_count", 0) + 1
79
80
  interview["structured_question_count"] = structured_count
80
81
 
82
+ # IMPORTANT: Capture the user's response from tool_output
83
+ # PostToolUse runs AFTER the tool completes, so we have the response
84
+ user_response = None
85
+ selected_value = None
86
+
87
+ # tool_output contains the user's response
88
+ if isinstance(tool_output, str):
89
+ user_response = tool_output
90
+ elif isinstance(tool_output, dict):
91
+ user_response = tool_output.get("response", tool_output.get("result", str(tool_output)))
92
+
93
+ # Try to match response to an option value
94
+ if has_options and user_response:
95
+ response_lower = user_response.lower().strip()
96
+ for opt in options:
97
+ opt_value = opt.get("value", "").lower()
98
+ opt_label = opt.get("label", "").lower()
99
+ # Check if response matches value or label
100
+ if opt_value in response_lower or response_lower in opt_label or opt_label in response_lower:
101
+ selected_value = opt.get("value")
102
+ break
103
+
81
104
  question_entry = {
82
105
  "question": tool_input.get("question", ""),
83
106
  "timestamp": datetime.now().isoformat(),
84
107
  "tool_used": True, # Proves AskUserQuestion was actually called
85
108
  "has_options": has_options,
86
109
  "options_count": len(options),
87
- "options": [opt.get("label", opt.get("value", "")) for opt in options[:5]] if options else []
110
+ "options": [opt.get("label", opt.get("value", "")) for opt in options[:5]] if options else [],
111
+ "user_response": user_response[:500] if user_response else None, # Capture actual response
112
+ "selected_value": selected_value # Matched option value if applicable
88
113
  }
89
114
  questions.append(question_entry)
90
115
 
116
+ # Track key decisions in a summary dict for easy reference during implementation
117
+ decisions = interview.setdefault("decisions", {})
118
+ question_text = tool_input.get("question", "").lower()
119
+
120
+ # Categorize common decision types
121
+ if "provider" in question_text or "ai provider" in question_text:
122
+ decisions["provider"] = {"response": user_response, "value": selected_value}
123
+ elif "purpose" in question_text or "primary purpose" in question_text:
124
+ decisions["purpose"] = {"response": user_response, "value": selected_value}
125
+ elif "format" in question_text or "response format" in question_text:
126
+ decisions["response_format"] = {"response": user_response, "value": selected_value}
127
+ elif "parameter" in question_text and "required" in question_text:
128
+ decisions["required_params"] = {"response": user_response, "value": selected_value}
129
+ elif "parameter" in question_text and "optional" in question_text:
130
+ decisions["optional_params"] = {"response": user_response, "value": selected_value}
131
+ elif "error" in question_text:
132
+ decisions["error_handling"] = {"response": user_response, "value": selected_value}
133
+ elif "api key" in question_text or "key" in question_text:
134
+ decisions["api_key_handling"] = {"response": user_response, "value": selected_value}
135
+ elif "service" in question_text or "external" in question_text:
136
+ decisions["external_services"] = {"response": user_response, "value": selected_value}
137
+
91
138
  # Update interview status
92
139
  if interview.get("status") == "not_started":
93
140
  interview["status"] = "in_progress"
@@ -100,6 +147,8 @@ def main():
100
147
  interview["last_structured_question"] = {
101
148
  "question": tool_input.get("question", "")[:100],
102
149
  "options_count": len(options),
150
+ "user_response": user_response[:100] if user_response else None,
151
+ "selected_value": selected_value,
103
152
  "timestamp": datetime.now().isoformat()
104
153
  }
105
154
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hustle-together/api-dev-tools",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "description": "Interview-driven API development workflow for Claude Code - Automates research, testing, and documentation",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {