@hustle-together/api-dev-tools 3.12.3 → 4.5.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.
Files changed (159) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +7 -463
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/workflow-logs/None.json +49 -0
  9. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  10. package/.skills/adr-deep-research/SKILL.md +351 -0
  11. package/.skills/api-create/SKILL.md +116 -17
  12. package/.skills/api-research/SKILL.md +130 -0
  13. package/.skills/docs-sync/SKILL.md +260 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +786 -0
  17. package/.skills/hustle-build-review/SKILL.md +518 -0
  18. package/.skills/parallel-spawn/SKILL.md +212 -0
  19. package/.skills/ralph-continue/SKILL.md +151 -0
  20. package/.skills/ralph-loop/SKILL.md +341 -0
  21. package/.skills/ralph-status/SKILL.md +87 -0
  22. package/.skills/refactor/SKILL.md +59 -0
  23. package/.skills/shadcn/SKILL.md +522 -0
  24. package/.skills/test-all/SKILL.md +210 -0
  25. package/.skills/test-builds/SKILL.md +208 -0
  26. package/.skills/test-debug/SKILL.md +212 -0
  27. package/.skills/test-e2e/SKILL.md +168 -0
  28. package/.skills/test-review/SKILL.md +707 -0
  29. package/.skills/test-unit/SKILL.md +143 -0
  30. package/.skills/test-visual/SKILL.md +301 -0
  31. package/.skills/token-report/SKILL.md +132 -0
  32. package/CHANGELOG.md +575 -0
  33. package/README.md +426 -56
  34. package/bin/cli.js +1538 -88
  35. package/commands/hustle-api-create.md +22 -0
  36. package/commands/hustle-build.md +259 -0
  37. package/commands/hustle-combine.md +81 -2
  38. package/commands/hustle-ui-create-page.md +84 -2
  39. package/commands/hustle-ui-create.md +82 -2
  40. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  95. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  96. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  97. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  98. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  99. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  100. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  101. package/hooks/api-workflow-check.py +34 -0
  102. package/hooks/auto-answer.py +305 -0
  103. package/hooks/check-update.py +132 -0
  104. package/hooks/completion-promise-detector.py +293 -0
  105. package/hooks/context-capacity-warning.py +171 -0
  106. package/hooks/docs-update-check.py +120 -0
  107. package/hooks/enforce-dry-run.py +134 -0
  108. package/hooks/enforce-external-research.py +25 -0
  109. package/hooks/enforce-interview.py +20 -0
  110. package/hooks/generate-adr-options.py +282 -0
  111. package/hooks/hook_utils.py +609 -0
  112. package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  113. package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  114. package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  115. package/hooks/ntfy-on-question.py +240 -0
  116. package/hooks/orchestrator-completion.py +313 -0
  117. package/hooks/orchestrator-handoff.py +267 -0
  118. package/hooks/orchestrator-session-startup.py +146 -0
  119. package/hooks/parallel-orchestrator.py +451 -0
  120. package/hooks/periodic-reground.py +270 -67
  121. package/hooks/project-document-prompt.py +302 -0
  122. package/hooks/remote-question-proxy.py +284 -0
  123. package/hooks/remote-question-server.py +1224 -0
  124. package/hooks/run-code-review.py +176 -29
  125. package/hooks/run-visual-qa.py +338 -0
  126. package/hooks/session-logger.py +27 -1
  127. package/hooks/session-startup.py +113 -0
  128. package/hooks/update-adr-decision.py +236 -0
  129. package/hooks/update-api-showcase.py +13 -1
  130. package/hooks/update-testing-checklist.py +195 -0
  131. package/hooks/update-ui-showcase.py +13 -1
  132. package/package.json +7 -3
  133. package/scripts/extract-schema-docs.cjs +322 -0
  134. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  135. package/templates/CLAUDE-SECTION.md +89 -64
  136. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  137. package/templates/api-dev-state.json +33 -1
  138. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  139. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  140. package/templates/api-showcase/_components/APITester.tsx +367 -58
  141. package/templates/brand-page/page.tsx +645 -0
  142. package/templates/component/Component.visual.spec.ts +30 -24
  143. package/templates/docs/page.tsx +230 -0
  144. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  145. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  146. package/templates/github-workflows/security.yml +274 -0
  147. package/templates/hustle-build-defaults.json +136 -0
  148. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  149. package/templates/page/page.e2e.test.ts +30 -26
  150. package/templates/performance-budgets.json +63 -5
  151. package/templates/playwright-report/page.tsx +258 -0
  152. package/templates/registry.json +279 -3
  153. package/templates/review-dashboard/page.tsx +510 -0
  154. package/templates/settings.json +155 -7
  155. package/templates/test-results/page.tsx +237 -0
  156. package/templates/typedoc.json +19 -0
  157. package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
  158. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  159. package/templates/ui-showcase/page.tsx +1 -1
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Enforce Dry-Run Mode Hook
4
+
5
+ This hook blocks Write and Edit operations when --dry-run mode is active.
6
+ It allows the workflow to run completely (research, interviews, schema generation)
7
+ but prevents any files from being written.
8
+
9
+ Hook Type: PreToolUse (matcher: Write, Edit)
10
+
11
+ Use Cases:
12
+ - Preview what a workflow will create before committing
13
+ - Test autonomous mode without modifying files
14
+ - Validate workflow logic without side effects
15
+
16
+ v4.5.0: Initial implementation
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ # Import shared utilities
25
+ try:
26
+ from hook_utils import (
27
+ check_dry_run_mode,
28
+ log_workflow_event,
29
+ load_state
30
+ )
31
+ UTILS_AVAILABLE = True
32
+ except ImportError:
33
+ UTILS_AVAILABLE = False
34
+
35
+
36
+ def get_file_path_from_env():
37
+ """Extract file path from tool input environment variable."""
38
+ tool_input = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
39
+ try:
40
+ data = json.loads(tool_input)
41
+ return data.get("file_path", "unknown")
42
+ except json.JSONDecodeError:
43
+ return "unknown"
44
+
45
+
46
+ def main():
47
+ """Main hook entry point."""
48
+ tool_name = os.environ.get("CLAUDE_TOOL_NAME", "")
49
+
50
+ # Only enforce for Write and Edit tools
51
+ if tool_name not in ["Write", "Edit"]:
52
+ print(json.dumps({"continue": True}))
53
+ return
54
+
55
+ # Check if dry-run mode is active
56
+ dry_run_active = False
57
+
58
+ if UTILS_AVAILABLE:
59
+ try:
60
+ dry_run_active = check_dry_run_mode()
61
+ except Exception:
62
+ pass
63
+ else:
64
+ # Fallback: check state file directly
65
+ try:
66
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
67
+ state_file = Path(project_dir) / ".claude" / "api-dev-state.json"
68
+ if state_file.exists():
69
+ state = json.loads(state_file.read_text())
70
+ dry_run_active = state.get("dry_run_mode", False) or state.get("flags", {}).get("dry_run", False)
71
+ except Exception:
72
+ pass
73
+
74
+ if not dry_run_active:
75
+ # Normal mode - allow the operation
76
+ print(json.dumps({"continue": True}))
77
+ return
78
+
79
+ # Dry-run mode active - block the write
80
+ file_path = get_file_path_from_env()
81
+
82
+ # Log the blocked operation
83
+ if UTILS_AVAILABLE:
84
+ try:
85
+ log_workflow_event("dry_run_block", {
86
+ "tool": tool_name,
87
+ "file_path": file_path,
88
+ "action": "blocked"
89
+ })
90
+ except Exception:
91
+ pass
92
+
93
+ # Return blocking result with informative message
94
+ result = {
95
+ "continue": False,
96
+ "reason": f"""## 🔒 Dry-Run Mode Active
97
+
98
+ **Tool:** {tool_name}
99
+ **Would write to:** `{file_path}`
100
+
101
+ In dry-run mode, no files are modified. The workflow continues to show
102
+ what WOULD be created, but Write and Edit operations are blocked.
103
+
104
+ ### To Execute For Real:
105
+ 1. Run the same command without `--dry-run`
106
+ 2. Or disable dry-run: Update state with `dry_run_mode: false`
107
+
108
+ ### Preview Summary:
109
+ This operation would {_get_operation_description(tool_name, file_path)}
110
+
111
+ ---
112
+ _Dry-run preview - no files were modified_
113
+ """
114
+ }
115
+ print(json.dumps(result))
116
+
117
+
118
+ def _get_operation_description(tool_name, file_path):
119
+ """Generate a human-readable description of the blocked operation."""
120
+ path = Path(file_path)
121
+
122
+ if tool_name == "Write":
123
+ if not path.exists() if file_path != "unknown" else True:
124
+ return f"create a new file at `{file_path}`"
125
+ return f"overwrite the file at `{file_path}`"
126
+
127
+ if tool_name == "Edit":
128
+ return f"modify the file at `{file_path}`"
129
+
130
+ return f"perform a {tool_name} operation on `{file_path}`"
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
@@ -16,6 +16,9 @@ The hook triggers on:
16
16
  - ANY questions about tools, services, or platforms
17
17
  - ANY request for implementation, editing, or changes
18
18
 
19
+ v3.12.13 Fix:
20
+ - Skip enforcement when running in source repository (developing the package)
21
+
19
22
  Returns:
20
23
  - Prints context to stdout (injected into conversation)
21
24
  - Exit 0 to allow the prompt to proceed
@@ -26,6 +29,28 @@ import re
26
29
  from pathlib import Path
27
30
  from datetime import datetime
28
31
 
32
+ # Import shared utilities
33
+ try:
34
+ from hook_utils import is_source_repository
35
+ except ImportError:
36
+ # Fallback if import fails
37
+ def is_source_repository():
38
+ try:
39
+ package_json = Path.cwd() / "package.json"
40
+ if package_json.exists():
41
+ data = json.loads(package_json.read_text())
42
+ if data.get("name") == "@hustle-together/api-dev-tools":
43
+ return True
44
+ if (Path.cwd() / "templates").is_dir():
45
+ return True
46
+ except Exception:
47
+ pass
48
+ return False
49
+
50
+ # Skip enforcement in source repository
51
+ if is_source_repository():
52
+ sys.exit(0)
53
+
29
54
  # State file is in .claude/ directory (sibling to hooks/)
30
55
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
31
56
 
@@ -27,6 +27,13 @@ import json
27
27
  import sys
28
28
  from pathlib import Path
29
29
 
30
+ # Import shared utilities for logging (v4.5.0)
31
+ try:
32
+ from hook_utils import log_workflow_event
33
+ UTILS_AVAILABLE = True
34
+ except ImportError:
35
+ UTILS_AVAILABLE = False
36
+
30
37
  # State file is in .claude/ directory (sibling to hooks/)
31
38
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
32
39
 
@@ -333,6 +340,19 @@ WHY: User must approve their decisions before they drive implementation."""
333
340
  # Build a reminder of what the user decided
334
341
  decision_summary = _build_decision_summary(decisions)
335
342
 
343
+ # Log the interview decision being applied (v4.5.0)
344
+ if UTILS_AVAILABLE:
345
+ try:
346
+ log_workflow_event("interview_decision", {
347
+ "action": "applying_decisions",
348
+ "file_path": file_path,
349
+ "decision_count": len(decisions),
350
+ "decisions": {k: v.get("value", v.get("response", ""))[:100]
351
+ for k, v in decisions.items()}
352
+ })
353
+ except Exception:
354
+ pass
355
+
336
356
  # Allow but inject context about user decisions
337
357
  print(json.dumps({
338
358
  "permissionDecision": "allow",
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ADR Options Generator Hook (v2.0 - Deep Research)
4
+
5
+ Automatically creates ADR research REQUESTS when research discovers
6
+ multiple options for significant decisions (database, auth, caching, etc.).
7
+
8
+ Hook Type: PostToolUse (matcher: WebSearch, WebFetch, mcp__context7)
9
+
10
+ Flow:
11
+ 1. Research phase discovers options (e.g., "Supabase vs Firebase vs Postgres")
12
+ 2. Hook detects multiple options for significant decision category
13
+ 3. Creates RESEARCH REQUEST file (not placeholder ADR)
14
+ 4. Injects context telling AI to run /adr-deep-research
15
+ 5. Deep research skill spawns parallel agents to research each option
16
+ 6. Real ADR with substantive pros/cons is created
17
+ 7. Interview phase presents these informed options to user
18
+ """
19
+
20
+ import json
21
+ import os
22
+ import re
23
+ from datetime import datetime
24
+ from pathlib import Path
25
+
26
+
27
+ def load_config():
28
+ """Load ADR configuration from hustle-build-defaults.json"""
29
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
30
+
31
+ # Check project-specific config
32
+ config_file = Path(project_dir) / ".claude" / "hustle-build-defaults.json"
33
+ if config_file.exists():
34
+ try:
35
+ config = json.loads(config_file.read_text())
36
+ return config.get("adr", {})
37
+ except Exception:
38
+ pass
39
+
40
+ # Fall back to template
41
+ template_file = Path(project_dir) / "templates" / "hustle-build-defaults.json"
42
+ if template_file.exists():
43
+ try:
44
+ config = json.loads(template_file.read_text())
45
+ return config.get("adr", {})
46
+ except Exception:
47
+ pass
48
+
49
+ # Default config
50
+ return {
51
+ "enabled": True,
52
+ "significant_decisions": {
53
+ "database": ["supabase", "firebase", "postgres", "mysql", "mongodb", "sqlite", "planetscale", "neon"],
54
+ "auth": ["api key", "oauth", "jwt", "session", "cookie", "basic auth", "api-key", "bearer"],
55
+ "cache": ["redis", "memcached", "in-memory", "cdn", "edge", "vercel kv"],
56
+ "hosting": ["vercel", "netlify", "aws", "cloudflare", "railway", "render", "fly.io"],
57
+ "state": ["redux", "zustand", "jotai", "context", "mobx", "recoil", "valtio"],
58
+ "styling": ["tailwind", "css modules", "styled-components", "emotion", "vanilla-extract"],
59
+ },
60
+ "min_options_for_adr": 2
61
+ }
62
+
63
+
64
+ def load_state():
65
+ """Load current workflow state"""
66
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
67
+ state_file = Path(project_dir) / ".claude" / "api-dev-state.json"
68
+
69
+ if state_file.exists():
70
+ try:
71
+ return json.loads(state_file.read_text())
72
+ except Exception:
73
+ pass
74
+ return {}
75
+
76
+
77
+ def get_next_adr_number():
78
+ """Get the next ADR number from existing ADRs"""
79
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
80
+ adrs_dir = Path(project_dir) / ".claude" / "adrs"
81
+
82
+ if not adrs_dir.exists():
83
+ return 1
84
+
85
+ existing = list(adrs_dir.glob("*.md"))
86
+ if not existing:
87
+ return 1
88
+
89
+ numbers = []
90
+ for f in existing:
91
+ match = re.match(r"(\d+)-", f.name)
92
+ if match:
93
+ numbers.append(int(match.group(1)))
94
+
95
+ return max(numbers, default=0) + 1
96
+
97
+
98
+ def detect_decision_points(content, config):
99
+ """
100
+ Check if research content contains multiple options for significant decisions.
101
+ Returns list of (category, matched_options) tuples.
102
+ """
103
+ if not config.get("enabled", True):
104
+ return []
105
+
106
+ significant = config.get("significant_decisions", {})
107
+ min_options = config.get("min_options_for_adr", 2)
108
+
109
+ content_lower = content.lower()
110
+ detected = []
111
+
112
+ for category, keywords in significant.items():
113
+ matches = [k for k in keywords if k.lower() in content_lower]
114
+ if len(matches) >= min_options:
115
+ detected.append((category, matches))
116
+
117
+ return detected
118
+
119
+
120
+ def create_adr_research_request(category, options, context, endpoint):
121
+ """Create a research REQUEST file for deep ADR research.
122
+
123
+ Instead of creating a placeholder ADR with empty pros/cons,
124
+ we create a request file that triggers /adr-deep-research skill.
125
+ """
126
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
127
+ requests_dir = Path(project_dir) / ".claude" / "adr-requests"
128
+ requests_dir.mkdir(parents=True, exist_ok=True)
129
+
130
+ # Check if request already exists for this category
131
+ pending_file = requests_dir / f"pending-{category}.json"
132
+ if pending_file.exists():
133
+ return None # Already pending
134
+
135
+ # Check if ADR already exists for this category
136
+ adrs_dir = Path(project_dir) / ".claude" / "adrs"
137
+ if adrs_dir.exists():
138
+ existing = list(adrs_dir.glob(f"*-{category}-choice.md"))
139
+ if existing:
140
+ return None # ADR already created
141
+
142
+ # Create research request
143
+ request = {
144
+ "category": category,
145
+ "options": options,
146
+ "context": context[:1000] if context else "",
147
+ "endpoint": endpoint,
148
+ "status": "pending",
149
+ "created_at": datetime.now().isoformat(),
150
+ "adr_number": get_next_adr_number()
151
+ }
152
+
153
+ pending_file.write_text(json.dumps(request, indent=2))
154
+
155
+ return request
156
+
157
+
158
+ def update_registry(adr_number, category, options, endpoint, filename):
159
+ """Add ADR to registry"""
160
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
161
+ registry_file = Path(project_dir) / ".claude" / "registry.json"
162
+
163
+ if registry_file.exists():
164
+ try:
165
+ registry = json.loads(registry_file.read_text())
166
+ except Exception:
167
+ registry = {}
168
+ else:
169
+ registry = {}
170
+
171
+ if "adrs" not in registry:
172
+ registry["adrs"] = {}
173
+
174
+ adr_key = f"{adr_number:04d}-{category}-choice"
175
+ registry["adrs"][adr_key] = {
176
+ "number": adr_number,
177
+ "title": f"{category.title()} Choice",
178
+ "status": "proposed",
179
+ "date": datetime.now().strftime("%Y-%m-%d"),
180
+ "phase": "initial_research",
181
+ "endpoint": endpoint,
182
+ "category": category,
183
+ "decision": None,
184
+ "options_considered": options,
185
+ "file": f".claude/adrs/{filename}"
186
+ }
187
+
188
+ registry_file.parent.mkdir(parents=True, exist_ok=True)
189
+ registry_file.write_text(json.dumps(registry, indent=2))
190
+
191
+
192
+ def main():
193
+ # Get tool output from environment
194
+ tool_output = os.environ.get("CLAUDE_TOOL_OUTPUT", "")
195
+ tool_name = os.environ.get("CLAUDE_TOOL_NAME", "")
196
+
197
+ # Only process research tools
198
+ if tool_name not in ["WebSearch", "WebFetch", "mcp__context7__get-library-docs"]:
199
+ print(json.dumps({"continue": True}))
200
+ return
201
+
202
+ # Load config
203
+ config = load_config()
204
+ if not config.get("enabled", True):
205
+ print(json.dumps({"continue": True}))
206
+ return
207
+
208
+ # Get current workflow context
209
+ state = load_state()
210
+ current_phase = state.get("current_phase", "")
211
+ endpoint = state.get("current_endpoint", state.get("workflow_id", "unknown"))
212
+
213
+ # Only generate ADRs during research phases
214
+ if current_phase not in ["initial_research", "deep_research", ""]:
215
+ print(json.dumps({"continue": True}))
216
+ return
217
+
218
+ # Detect decision points in research content
219
+ decision_points = detect_decision_points(tool_output, config)
220
+
221
+ if not decision_points:
222
+ print(json.dumps({"continue": True}))
223
+ return
224
+
225
+ # Create research requests for each decision point
226
+ created_requests = []
227
+ for category, options in decision_points:
228
+ request = create_adr_research_request(
229
+ category=category,
230
+ options=options,
231
+ context=tool_output[:1000],
232
+ endpoint=endpoint
233
+ )
234
+
235
+ if request:
236
+ created_requests.append({
237
+ "category": category,
238
+ "options": options,
239
+ "adr_number": request["adr_number"]
240
+ })
241
+
242
+ if created_requests:
243
+ # Build list of research commands to run
244
+ research_commands = "\n".join([
245
+ f"- `/adr-deep-research {r['category']}` - Research {', '.join(r['options'])}"
246
+ for r in created_requests
247
+ ])
248
+
249
+ # Build summary of what was detected
250
+ detection_summary = "\n".join([
251
+ f"- **{r['category'].title()}**: {', '.join(r['options'])}"
252
+ for r in created_requests
253
+ ])
254
+
255
+ result = {
256
+ "continue": True,
257
+ "additionalContext": f"""## ADR Research Needed
258
+
259
+ Research discovered significant decision points that require deeper investigation:
260
+
261
+ {detection_summary}
262
+
263
+ **Next Step:** Run deep research to get real pros/cons before the interview:
264
+
265
+ {research_commands}
266
+
267
+ This will:
268
+ 1. Spawn parallel research agents (one per option)
269
+ 2. Fetch official documentation for each technology
270
+ 3. Extract real pros, cons, pricing, and best-use cases
271
+ 4. Create a substantive ADR with informed recommendations
272
+
273
+ The ADR will then be referenced during the interview phase.
274
+ """
275
+ }
276
+ print(json.dumps(result))
277
+ else:
278
+ print(json.dumps({"continue": True}))
279
+
280
+
281
+ if __name__ == "__main__":
282
+ main()