@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.
- package/.claude/adr-requests/.gitkeep +10 -0
- package/.claude/agents/adr-researcher.md +109 -0
- package/.claude/agents/visual-analyzer.md +183 -0
- package/.claude/api-dev-state.json +7 -463
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/workflow-logs/None.json +49 -0
- package/.claude/workflow-logs/session-20251230-143727.json +106 -0
- package/.skills/adr-deep-research/SKILL.md +351 -0
- package/.skills/api-create/SKILL.md +116 -17
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +786 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- package/.skills/parallel-spawn/SKILL.md +212 -0
- package/.skills/ralph-continue/SKILL.md +151 -0
- package/.skills/ralph-loop/SKILL.md +341 -0
- package/.skills/ralph-status/SKILL.md +87 -0
- package/.skills/refactor/SKILL.md +59 -0
- package/.skills/shadcn/SKILL.md +522 -0
- package/.skills/test-all/SKILL.md +210 -0
- package/.skills/test-builds/SKILL.md +208 -0
- package/.skills/test-debug/SKILL.md +212 -0
- package/.skills/test-e2e/SKILL.md +168 -0
- package/.skills/test-review/SKILL.md +707 -0
- package/.skills/test-unit/SKILL.md +143 -0
- package/.skills/test-visual/SKILL.md +301 -0
- package/.skills/token-report/SKILL.md +132 -0
- package/CHANGELOG.md +575 -0
- package/README.md +426 -56
- package/bin/cli.js +1538 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-build.md +259 -0
- package/commands/hustle-combine.md +81 -2
- package/commands/hustle-ui-create-page.md +84 -2
- package/commands/hustle-ui-create.md +82 -2
- package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
- package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
- package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
- package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
- package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
- package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
- package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
- package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
- package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
- package/hooks/api-workflow-check.py +34 -0
- package/hooks/auto-answer.py +305 -0
- package/hooks/check-update.py +132 -0
- package/hooks/completion-promise-detector.py +293 -0
- package/hooks/context-capacity-warning.py +171 -0
- package/hooks/docs-update-check.py +120 -0
- package/hooks/enforce-dry-run.py +134 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/hooks/generate-adr-options.py +282 -0
- package/hooks/hook_utils.py +609 -0
- package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/hooks/ntfy-on-question.py +240 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +267 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/parallel-orchestrator.py +451 -0
- package/hooks/periodic-reground.py +270 -67
- package/hooks/project-document-prompt.py +302 -0
- package/hooks/remote-question-proxy.py +284 -0
- package/hooks/remote-question-server.py +1224 -0
- package/hooks/run-code-review.py +176 -29
- package/hooks/run-visual-qa.py +338 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/hooks/update-adr-decision.py +236 -0
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-testing-checklist.py +195 -0
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +367 -58
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/docs/page.tsx +230 -0
- package/templates/eslint-plugin-zod-schema/index.js +446 -0
- package/templates/eslint-plugin-zod-schema/package.json +26 -0
- package/templates/github-workflows/security.yml +274 -0
- package/templates/hustle-build-defaults.json +136 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +155 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- 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()
|