@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.
- package/README.md +45 -2
- package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +957 -0
- package/demo/hustle-together/blog/interview-driven-api-development.html +1132 -0
- package/demo/hustle-together/blog/tdd-for-ai.html +982 -0
- package/demo/hustle-together/index.html +1279 -0
- package/demo/workflow-demo.html +881 -157
- package/hooks/enforce-interview.py +52 -2
- package/hooks/track-tool-use.py +51 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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:
|
package/hooks/track-tool-use.py
CHANGED
|
@@ -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