@hustle-together/api-dev-tools 3.10.1 → 3.11.1
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/.claude/api-dev-state.json +159 -0
- package/.claude/commands/README.md +185 -0
- package/.claude/commands/add-command.md +209 -0
- package/.claude/commands/api-create.md +499 -0
- package/.claude/commands/api-env.md +50 -0
- package/.claude/commands/api-interview.md +331 -0
- package/.claude/commands/api-research.md +331 -0
- package/.claude/commands/api-status.md +259 -0
- package/.claude/commands/api-verify.md +231 -0
- package/.claude/commands/beepboop.md +97 -0
- package/.claude/commands/busycommit.md +112 -0
- package/.claude/commands/commit.md +83 -0
- package/.claude/commands/cycle.md +142 -0
- package/.claude/commands/gap.md +86 -0
- package/.claude/commands/green.md +142 -0
- package/.claude/commands/issue.md +192 -0
- package/.claude/commands/plan.md +168 -0
- package/.claude/commands/pr.md +122 -0
- package/.claude/commands/red.md +142 -0
- package/.claude/commands/refactor.md +142 -0
- package/.claude/commands/spike.md +142 -0
- package/.claude/commands/summarize.md +94 -0
- package/.claude/commands/tdd.md +144 -0
- package/.claude/commands/worktree-add.md +315 -0
- package/.claude/commands/worktree-cleanup.md +281 -0
- package/.claude/hooks/api-workflow-check.py +227 -0
- package/.claude/hooks/enforce-deep-research.py +185 -0
- package/.claude/hooks/enforce-disambiguation.py +155 -0
- package/.claude/hooks/enforce-documentation.py +192 -0
- package/.claude/hooks/enforce-environment.py +253 -0
- package/.claude/hooks/enforce-external-research.py +328 -0
- package/.claude/hooks/enforce-interview.py +421 -0
- package/.claude/hooks/enforce-refactor.py +189 -0
- package/.claude/hooks/enforce-research.py +159 -0
- package/.claude/hooks/enforce-schema.py +186 -0
- package/.claude/hooks/enforce-scope.py +160 -0
- package/.claude/hooks/enforce-tdd-red.py +250 -0
- package/.claude/hooks/enforce-verify.py +186 -0
- package/.claude/hooks/periodic-reground.py +154 -0
- package/.claude/hooks/session-startup.py +151 -0
- package/.claude/hooks/track-tool-use.py +626 -0
- package/.claude/hooks/verify-after-green.py +282 -0
- package/.claude/hooks/verify-implementation.py +225 -0
- package/.claude/research/index.json +6 -0
- package/.claude/settings.json +93 -0
- package/.claude/settings.local.json +11 -0
- package/.claude-plugin/marketplace.json +112 -0
- package/.skills/README.md +291 -0
- package/.skills/_shared/convert-commands.py +192 -0
- package/.skills/_shared/hooks/api-workflow-check.py +227 -0
- package/.skills/_shared/hooks/enforce-deep-research.py +185 -0
- package/.skills/_shared/hooks/enforce-disambiguation.py +155 -0
- package/.skills/_shared/hooks/enforce-documentation.py +192 -0
- package/.skills/_shared/hooks/enforce-environment.py +253 -0
- package/.skills/_shared/hooks/enforce-external-research.py +328 -0
- package/.skills/_shared/hooks/enforce-interview.py +421 -0
- package/.skills/_shared/hooks/enforce-refactor.py +189 -0
- package/.skills/_shared/hooks/enforce-research.py +159 -0
- package/.skills/_shared/hooks/enforce-schema.py +186 -0
- package/.skills/_shared/hooks/enforce-scope.py +160 -0
- package/.skills/_shared/hooks/enforce-tdd-red.py +250 -0
- package/.skills/_shared/hooks/enforce-verify.py +186 -0
- package/.skills/_shared/hooks/periodic-reground.py +154 -0
- package/.skills/_shared/hooks/session-startup.py +151 -0
- package/.skills/_shared/hooks/track-tool-use.py +626 -0
- package/.skills/_shared/hooks/verify-after-green.py +282 -0
- package/.skills/_shared/hooks/verify-implementation.py +225 -0
- package/.skills/_shared/install.sh +114 -0
- package/.skills/_shared/settings.json +93 -0
- package/.skills/add-command/SKILL.md +222 -0
- package/.skills/api-create/SKILL.md +512 -0
- package/.skills/api-env/SKILL.md +63 -0
- package/.skills/api-interview/SKILL.md +344 -0
- package/.skills/api-research/SKILL.md +344 -0
- package/.skills/api-status/SKILL.md +272 -0
- package/.skills/api-verify/SKILL.md +244 -0
- package/.skills/beepboop/SKILL.md +110 -0
- package/.skills/busycommit/SKILL.md +125 -0
- package/.skills/commit/SKILL.md +96 -0
- package/.skills/cycle/SKILL.md +155 -0
- package/.skills/gap/SKILL.md +99 -0
- package/.skills/green/SKILL.md +155 -0
- package/.skills/issue/SKILL.md +205 -0
- package/.skills/plan/SKILL.md +181 -0
- package/.skills/pr/SKILL.md +135 -0
- package/.skills/red/SKILL.md +155 -0
- package/.skills/refactor/SKILL.md +155 -0
- package/.skills/spike/SKILL.md +155 -0
- package/.skills/summarize/SKILL.md +107 -0
- package/.skills/tdd/SKILL.md +157 -0
- package/.skills/update-todos/SKILL.md +228 -0
- package/.skills/worktree-add/SKILL.md +328 -0
- package/.skills/worktree-cleanup/SKILL.md +294 -0
- package/CHANGELOG.md +97 -0
- package/README.md +58 -17
- package/package.json +22 -11
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: Stop
|
|
4
|
+
Purpose: Check if all required phases are complete before allowing stop
|
|
5
|
+
|
|
6
|
+
This hook runs when Claude tries to stop/end the conversation.
|
|
7
|
+
It checks api-dev-state.json to ensure critical workflow phases completed.
|
|
8
|
+
|
|
9
|
+
Gap Fixes Applied:
|
|
10
|
+
- Gap 2: Shows files_created vs files_modified to verify all claimed changes
|
|
11
|
+
- Gap 3: Warns if there are verification_warnings that weren't addressed
|
|
12
|
+
- Gap 4: Requires explicit verification that implementation matches interview
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
- {"decision": "approve"} - Allow stopping
|
|
16
|
+
- {"decision": "block", "reason": "..."} - Prevent stopping with explanation
|
|
17
|
+
"""
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
import subprocess
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
# State file is in .claude/ directory (sibling to hooks/)
|
|
24
|
+
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
25
|
+
|
|
26
|
+
# Phases that MUST be complete before stopping
|
|
27
|
+
REQUIRED_PHASES = [
|
|
28
|
+
("research_initial", "Initial research (Context7/WebSearch)"),
|
|
29
|
+
("interview", "User interview"),
|
|
30
|
+
("tdd_red", "TDD Red phase (failing tests written)"),
|
|
31
|
+
("tdd_green", "TDD Green phase (tests passing)"),
|
|
32
|
+
("verify", "Verification phase (re-checked against docs)"),
|
|
33
|
+
("documentation", "Documentation updates (manifest/research cached)"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Phases that SHOULD be complete (warning but don't block)
|
|
37
|
+
RECOMMENDED_PHASES = [
|
|
38
|
+
("schema_creation", "Schema creation"),
|
|
39
|
+
("tdd_refactor", "TDD Refactor phase"),
|
|
40
|
+
("documentation", "Documentation updates"),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_git_modified_files() -> list[str]:
|
|
45
|
+
"""Get list of modified files from git.
|
|
46
|
+
|
|
47
|
+
Gap 2 Fix: Verify which files actually changed.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
["git", "diff", "--name-only", "HEAD"],
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
cwd=STATE_FILE.parent.parent # Project root
|
|
55
|
+
)
|
|
56
|
+
if result.returncode == 0:
|
|
57
|
+
return [f.strip() for f in result.stdout.strip().split("\n") if f.strip()]
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_verification_warnings(state: dict) -> list[str]:
|
|
64
|
+
"""Check for unaddressed verification warnings.
|
|
65
|
+
|
|
66
|
+
Gap 3 Fix: Don't accept "skipped" or warnings without explanation.
|
|
67
|
+
"""
|
|
68
|
+
warnings = state.get("verification_warnings", [])
|
|
69
|
+
if warnings:
|
|
70
|
+
return [
|
|
71
|
+
"⚠️ Unaddressed verification warnings:",
|
|
72
|
+
*[f" - {w}" for w in warnings[-5:]], # Show last 5
|
|
73
|
+
"",
|
|
74
|
+
"Please review and address these warnings before completing."
|
|
75
|
+
]
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def check_interview_implementation_match(state: dict) -> list[str]:
|
|
80
|
+
"""Verify implementation matches interview requirements.
|
|
81
|
+
|
|
82
|
+
Gap 4 Fix: Define specific "done" criteria based on interview.
|
|
83
|
+
"""
|
|
84
|
+
issues = []
|
|
85
|
+
|
|
86
|
+
interview = state.get("phases", {}).get("interview", {})
|
|
87
|
+
questions = interview.get("questions", [])
|
|
88
|
+
|
|
89
|
+
# Extract key requirements from interview
|
|
90
|
+
all_text = " ".join(str(q) for q in questions)
|
|
91
|
+
|
|
92
|
+
# Check files_created includes expected patterns
|
|
93
|
+
files_created = state.get("files_created", [])
|
|
94
|
+
|
|
95
|
+
# Look for route files if interview mentioned endpoints
|
|
96
|
+
if "endpoint" in all_text.lower() or "/api/" in all_text.lower():
|
|
97
|
+
route_files = [f for f in files_created if "route.ts" in f]
|
|
98
|
+
if not route_files:
|
|
99
|
+
issues.append("⚠️ Interview mentioned endpoints but no route.ts files were created")
|
|
100
|
+
|
|
101
|
+
# Look for test files
|
|
102
|
+
test_files = [f for f in files_created if ".test." in f or "__tests__" in f]
|
|
103
|
+
if not test_files:
|
|
104
|
+
issues.append("⚠️ No test files tracked in files_created")
|
|
105
|
+
|
|
106
|
+
return issues
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main():
|
|
110
|
+
# If no state file, we're not in an API workflow - allow stop
|
|
111
|
+
if not STATE_FILE.exists():
|
|
112
|
+
print(json.dumps({"decision": "approve"}))
|
|
113
|
+
sys.exit(0)
|
|
114
|
+
|
|
115
|
+
# Load state
|
|
116
|
+
try:
|
|
117
|
+
state = json.loads(STATE_FILE.read_text())
|
|
118
|
+
except json.JSONDecodeError:
|
|
119
|
+
# Corrupted state, allow stop
|
|
120
|
+
print(json.dumps({"decision": "approve"}))
|
|
121
|
+
sys.exit(0)
|
|
122
|
+
|
|
123
|
+
phases = state.get("phases", {})
|
|
124
|
+
|
|
125
|
+
# Check if workflow was even started
|
|
126
|
+
research = phases.get("research_initial", {})
|
|
127
|
+
if research.get("status") == "not_started":
|
|
128
|
+
# Workflow not started, allow stop
|
|
129
|
+
print(json.dumps({"decision": "approve"}))
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
|
|
132
|
+
# Collect all issues
|
|
133
|
+
all_issues = []
|
|
134
|
+
|
|
135
|
+
# Check required phases
|
|
136
|
+
incomplete_required = []
|
|
137
|
+
for phase_key, phase_name in REQUIRED_PHASES:
|
|
138
|
+
phase = phases.get(phase_key, {})
|
|
139
|
+
status = phase.get("status", "not_started")
|
|
140
|
+
if status != "complete":
|
|
141
|
+
incomplete_required.append(f" - {phase_name} ({status})")
|
|
142
|
+
|
|
143
|
+
if incomplete_required:
|
|
144
|
+
all_issues.append("❌ REQUIRED phases incomplete:")
|
|
145
|
+
all_issues.extend(incomplete_required)
|
|
146
|
+
|
|
147
|
+
# Check recommended phases
|
|
148
|
+
incomplete_recommended = []
|
|
149
|
+
for phase_key, phase_name in RECOMMENDED_PHASES:
|
|
150
|
+
phase = phases.get(phase_key, {})
|
|
151
|
+
status = phase.get("status", "not_started")
|
|
152
|
+
if status != "complete":
|
|
153
|
+
incomplete_recommended.append(f" - {phase_name} ({status})")
|
|
154
|
+
|
|
155
|
+
# Gap 2: Check git diff vs tracked files
|
|
156
|
+
git_files = get_git_modified_files()
|
|
157
|
+
tracked_files = state.get("files_created", []) + state.get("files_modified", [])
|
|
158
|
+
|
|
159
|
+
if git_files and tracked_files:
|
|
160
|
+
# Find files in git but not tracked
|
|
161
|
+
untracked_changes = []
|
|
162
|
+
for gf in git_files:
|
|
163
|
+
if not any(gf.endswith(tf) or tf in gf for tf in tracked_files):
|
|
164
|
+
if gf.endswith(".ts") and ("/api/" in gf or "/lib/" in gf):
|
|
165
|
+
untracked_changes.append(gf)
|
|
166
|
+
|
|
167
|
+
if untracked_changes:
|
|
168
|
+
all_issues.append("\n⚠️ Gap 2: Files changed but not tracked:")
|
|
169
|
+
all_issues.extend([f" - {f}" for f in untracked_changes[:5]])
|
|
170
|
+
|
|
171
|
+
# Gap 3: Check for unaddressed warnings
|
|
172
|
+
warning_issues = check_verification_warnings(state)
|
|
173
|
+
if warning_issues:
|
|
174
|
+
all_issues.append("\n" + "\n".join(warning_issues))
|
|
175
|
+
|
|
176
|
+
# Gap 4: Check interview-implementation match
|
|
177
|
+
match_issues = check_interview_implementation_match(state)
|
|
178
|
+
if match_issues:
|
|
179
|
+
all_issues.append("\n⚠️ Gap 4: Implementation verification:")
|
|
180
|
+
all_issues.extend([f" {i}" for i in match_issues])
|
|
181
|
+
|
|
182
|
+
# Block if required phases incomplete
|
|
183
|
+
if incomplete_required:
|
|
184
|
+
all_issues.append("\n\nTo continue:")
|
|
185
|
+
all_issues.append(" 1. Complete required phases above")
|
|
186
|
+
all_issues.append(" 2. Use /api-status to see detailed progress")
|
|
187
|
+
all_issues.append(" 3. Run `git diff --name-only` to verify changes")
|
|
188
|
+
|
|
189
|
+
print(json.dumps({
|
|
190
|
+
"decision": "block",
|
|
191
|
+
"reason": "\n".join(all_issues)
|
|
192
|
+
}))
|
|
193
|
+
sys.exit(0)
|
|
194
|
+
|
|
195
|
+
# Build completion message
|
|
196
|
+
message_parts = ["✅ API workflow completing"]
|
|
197
|
+
|
|
198
|
+
if incomplete_recommended:
|
|
199
|
+
message_parts.append("\n⚠️ Optional phases skipped:")
|
|
200
|
+
message_parts.extend(incomplete_recommended)
|
|
201
|
+
|
|
202
|
+
# Show summary of tracked files
|
|
203
|
+
files_created = state.get("files_created", [])
|
|
204
|
+
if files_created:
|
|
205
|
+
message_parts.append(f"\n📁 Files created: {len(files_created)}")
|
|
206
|
+
for f in files_created[:5]:
|
|
207
|
+
message_parts.append(f" - {f}")
|
|
208
|
+
if len(files_created) > 5:
|
|
209
|
+
message_parts.append(f" ... and {len(files_created) - 5} more")
|
|
210
|
+
|
|
211
|
+
# Show any remaining warnings
|
|
212
|
+
if warning_issues or match_issues:
|
|
213
|
+
message_parts.append("\n⚠️ Review suggested:")
|
|
214
|
+
if warning_issues:
|
|
215
|
+
message_parts.extend(warning_issues[:3])
|
|
216
|
+
if match_issues:
|
|
217
|
+
message_parts.extend(match_issues[:3])
|
|
218
|
+
|
|
219
|
+
print(json.dumps({
|
|
220
|
+
"decision": "approve",
|
|
221
|
+
"message": "\n".join(message_parts)
|
|
222
|
+
}))
|
|
223
|
+
sys.exit(0)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
main()
|
|
@@ -0,0 +1,185 @@
|
|
|
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 5 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
|
+
phase_exit_confirmed = research_deep.get("phase_exit_confirmed", False)
|
|
76
|
+
|
|
77
|
+
if status != "complete" or not phase_exit_confirmed:
|
|
78
|
+
user_question_asked = research_deep.get("user_question_asked", False)
|
|
79
|
+
user_approved = research_deep.get("user_approved", False)
|
|
80
|
+
proposals_shown = research_deep.get("proposals_shown", False)
|
|
81
|
+
approved_searches = research_deep.get("approved_searches", [])
|
|
82
|
+
executed_searches = research_deep.get("executed_searches", [])
|
|
83
|
+
skipped_searches = research_deep.get("skipped_searches", [])
|
|
84
|
+
|
|
85
|
+
# Calculate pending
|
|
86
|
+
pending = [s for s in approved_searches if s not in executed_searches and s not in skipped_searches]
|
|
87
|
+
|
|
88
|
+
missing = []
|
|
89
|
+
if not proposals_shown:
|
|
90
|
+
missing.append("Proposed searches not shown to user")
|
|
91
|
+
if not user_question_asked:
|
|
92
|
+
missing.append("User approval question (AskUserQuestion not used)")
|
|
93
|
+
if not user_approved:
|
|
94
|
+
missing.append("User hasn't approved the search list")
|
|
95
|
+
if pending:
|
|
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)")
|
|
99
|
+
|
|
100
|
+
print(json.dumps({
|
|
101
|
+
"permissionDecision": "deny",
|
|
102
|
+
"reason": f"""❌ BLOCKED: Deep research (Phase 5) not complete.
|
|
103
|
+
|
|
104
|
+
Status: {status}
|
|
105
|
+
Proposed searches: {len(proposed)}
|
|
106
|
+
User shown proposals: {proposals_shown}
|
|
107
|
+
User question asked: {user_question_asked}
|
|
108
|
+
User approved: {user_approved}
|
|
109
|
+
Approved: {len(approved_searches)}
|
|
110
|
+
Executed: {len(executed_searches)}
|
|
111
|
+
Skipped: {len(skipped_searches)}
|
|
112
|
+
Pending: {len(pending)}
|
|
113
|
+
Phase exit confirmed: {phase_exit_confirmed}
|
|
114
|
+
|
|
115
|
+
MISSING:
|
|
116
|
+
{chr(10).join(f" • {m}" for m in missing)}
|
|
117
|
+
|
|
118
|
+
═══════════════════════════════════════════════════════════
|
|
119
|
+
⚠️ GET USER APPROVAL FOR DEEP RESEARCH
|
|
120
|
+
═══════════════════════════════════════════════════════════
|
|
121
|
+
|
|
122
|
+
REQUIRED STEPS:
|
|
123
|
+
|
|
124
|
+
1. Based on interview, PROPOSE targeted searches:
|
|
125
|
+
┌───────────────────────────────────────────────────────┐
|
|
126
|
+
│ PROPOSED DEEP RESEARCH │
|
|
127
|
+
│ │
|
|
128
|
+
│ Based on your interview answers, I want to research: │
|
|
129
|
+
│ │
|
|
130
|
+
│ [x] Error response format (for error handling) │
|
|
131
|
+
│ [x] Rate limiting behavior (caching selected) │
|
|
132
|
+
│ [ ] Webhook support (not selected in interview) │
|
|
133
|
+
│ [x] Authentication edge cases │
|
|
134
|
+
│ │
|
|
135
|
+
│ Approve these searches? [Y] │
|
|
136
|
+
│ Add more: ____ │
|
|
137
|
+
│ Skip and proceed: [n] │
|
|
138
|
+
└───────────────────────────────────────────────────────┘
|
|
139
|
+
|
|
140
|
+
2. USE AskUserQuestion:
|
|
141
|
+
question: "Approve these deep research searches?"
|
|
142
|
+
options: [
|
|
143
|
+
{{"value": "approve", "label": "Yes, run these searches"}},
|
|
144
|
+
{{"value": "add", "label": "Add more - I also need [topic]"}},
|
|
145
|
+
{{"value": "skip", "label": "Skip deep research, proceed to schema"}}
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
3. If user says "add":
|
|
149
|
+
• Ask what additional topics they need
|
|
150
|
+
• Add to proposed_searches
|
|
151
|
+
• LOOP BACK and show updated list
|
|
152
|
+
|
|
153
|
+
4. If user says "approve":
|
|
154
|
+
• Execute each approved search
|
|
155
|
+
• Record results in executed_searches
|
|
156
|
+
|
|
157
|
+
5. If user says "skip":
|
|
158
|
+
• Record all as skipped_searches with reason
|
|
159
|
+
• Proceed to schema
|
|
160
|
+
|
|
161
|
+
6. After all searches complete (or skipped):
|
|
162
|
+
• Set research_deep.user_approved = true
|
|
163
|
+
• Set research_deep.user_question_asked = true
|
|
164
|
+
• Set research_deep.proposals_shown = true
|
|
165
|
+
• Set research_deep.status = "complete"
|
|
166
|
+
|
|
167
|
+
WHY: Research is ADAPTIVE based on interview, not shotgun."""
|
|
168
|
+
}))
|
|
169
|
+
sys.exit(0)
|
|
170
|
+
|
|
171
|
+
# Complete
|
|
172
|
+
executed = research_deep.get("executed_searches", [])
|
|
173
|
+
skipped = research_deep.get("skipped_searches", [])
|
|
174
|
+
print(json.dumps({
|
|
175
|
+
"permissionDecision": "allow",
|
|
176
|
+
"message": f"""✅ Deep research complete.
|
|
177
|
+
Executed: {len(executed)} searches
|
|
178
|
+
Skipped: {len(skipped)} (with reasons)
|
|
179
|
+
User approved the search plan."""
|
|
180
|
+
}))
|
|
181
|
+
sys.exit(0)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
main()
|
|
@@ -0,0 +1,155 @@
|
|
|
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 1 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 1 (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
|
+
# 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:
|
|
81
|
+
search_variations = disambiguation.get("search_variations", [])
|
|
82
|
+
user_question_asked = disambiguation.get("user_question_asked", False)
|
|
83
|
+
user_selected = disambiguation.get("user_selected", None)
|
|
84
|
+
|
|
85
|
+
# Check what's missing
|
|
86
|
+
missing = []
|
|
87
|
+
if len(search_variations) < MIN_SEARCH_VARIATIONS:
|
|
88
|
+
missing.append(f"Search variations ({len(search_variations)}/{MIN_SEARCH_VARIATIONS})")
|
|
89
|
+
if not user_question_asked:
|
|
90
|
+
missing.append("User question (AskUserQuestion not used)")
|
|
91
|
+
if not user_selected:
|
|
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)")
|
|
95
|
+
|
|
96
|
+
print(json.dumps({
|
|
97
|
+
"permissionDecision": "deny",
|
|
98
|
+
"reason": f"""❌ BLOCKED: Disambiguation phase (Phase 1) not complete.
|
|
99
|
+
|
|
100
|
+
Status: {status}
|
|
101
|
+
Search variations: {len(search_variations)}
|
|
102
|
+
User question asked: {user_question_asked}
|
|
103
|
+
User selection: {user_selected or "None"}
|
|
104
|
+
Phase exit confirmed: {phase_exit_confirmed}
|
|
105
|
+
|
|
106
|
+
MISSING:
|
|
107
|
+
{chr(10).join(f" • {m}" for m in missing)}
|
|
108
|
+
|
|
109
|
+
═══════════════════════════════════════════════════════════
|
|
110
|
+
⚠️ COMPLETE DISAMBIGUATION WITH USER CONFIRMATION
|
|
111
|
+
═══════════════════════════════════════════════════════════
|
|
112
|
+
|
|
113
|
+
REQUIRED STEPS:
|
|
114
|
+
|
|
115
|
+
1. Search 2-3 variations:
|
|
116
|
+
• WebSearch: "{endpoint}"
|
|
117
|
+
• WebSearch: "{endpoint} API"
|
|
118
|
+
• WebSearch: "{endpoint} SDK npm package"
|
|
119
|
+
|
|
120
|
+
2. USE AskUserQuestion with options:
|
|
121
|
+
┌───────────────────────────────────────────────────────┐
|
|
122
|
+
│ I found multiple things matching "{endpoint}": │
|
|
123
|
+
│ │
|
|
124
|
+
│ [A] The official REST API │
|
|
125
|
+
│ [B] The npm/SDK wrapper package │
|
|
126
|
+
│ [C] Both (API + SDK) │
|
|
127
|
+
│ [D] Something else: ____ │
|
|
128
|
+
│ │
|
|
129
|
+
│ Which should this endpoint use? │
|
|
130
|
+
└───────────────────────────────────────────────────────┘
|
|
131
|
+
|
|
132
|
+
3. Record user's choice in state:
|
|
133
|
+
disambiguation.user_selected = "A" (or user's choice)
|
|
134
|
+
disambiguation.user_question_asked = true
|
|
135
|
+
disambiguation.status = "complete"
|
|
136
|
+
|
|
137
|
+
4. LOOP BACK if user is still unsure - search more variations
|
|
138
|
+
|
|
139
|
+
WHY: Different interpretations = different implementations."""
|
|
140
|
+
}))
|
|
141
|
+
sys.exit(0)
|
|
142
|
+
|
|
143
|
+
# Complete - inject context
|
|
144
|
+
selected = disambiguation.get("user_selected", "")
|
|
145
|
+
print(json.dumps({
|
|
146
|
+
"permissionDecision": "allow",
|
|
147
|
+
"message": f"""✅ Disambiguation complete.
|
|
148
|
+
User selected: {selected}
|
|
149
|
+
Proceeding with this interpretation."""
|
|
150
|
+
}))
|
|
151
|
+
sys.exit(0)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
main()
|