agentic-loop 1.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/.claude/commands/explain.md +114 -0
- package/.claude/commands/idea.md +398 -0
- package/.claude/commands/my-dna.md +122 -0
- package/.claude/commands/prd.md +286 -0
- package/.claude/commands/review.md +167 -0
- package/.claude/commands/sign.md +32 -0
- package/.claude/commands/styleguide.md +450 -0
- package/.claude/commands/tour.md +301 -0
- package/.claude/commands/vibe-check.md +116 -0
- package/.claude/commands/vibe-help.md +47 -0
- package/.claude/commands/vibe-list.md +203 -0
- package/.pre-commit-hooks.yaml +102 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/bin/agentic-loop.sh +24 -0
- package/bin/postinstall.sh +29 -0
- package/bin/ralph.sh +171 -0
- package/bin/vibe-check.js +19 -0
- package/dist/checks/check-any-types.d.ts +6 -0
- package/dist/checks/check-any-types.d.ts.map +1 -0
- package/dist/checks/check-any-types.js +73 -0
- package/dist/checks/check-any-types.js.map +1 -0
- package/dist/checks/check-commented-code.d.ts +6 -0
- package/dist/checks/check-commented-code.d.ts.map +1 -0
- package/dist/checks/check-commented-code.js +81 -0
- package/dist/checks/check-commented-code.js.map +1 -0
- package/dist/checks/check-console-error.d.ts +6 -0
- package/dist/checks/check-console-error.d.ts.map +1 -0
- package/dist/checks/check-console-error.js +41 -0
- package/dist/checks/check-console-error.js.map +1 -0
- package/dist/checks/check-debug-statements.d.ts +6 -0
- package/dist/checks/check-debug-statements.d.ts.map +1 -0
- package/dist/checks/check-debug-statements.js +120 -0
- package/dist/checks/check-debug-statements.js.map +1 -0
- package/dist/checks/check-deep-nesting.d.ts +6 -0
- package/dist/checks/check-deep-nesting.d.ts.map +1 -0
- package/dist/checks/check-deep-nesting.js +116 -0
- package/dist/checks/check-deep-nesting.js.map +1 -0
- package/dist/checks/check-docker-platform.d.ts +6 -0
- package/dist/checks/check-docker-platform.d.ts.map +1 -0
- package/dist/checks/check-docker-platform.js +42 -0
- package/dist/checks/check-docker-platform.js.map +1 -0
- package/dist/checks/check-dry-violations.d.ts +6 -0
- package/dist/checks/check-dry-violations.d.ts.map +1 -0
- package/dist/checks/check-dry-violations.js +124 -0
- package/dist/checks/check-dry-violations.js.map +1 -0
- package/dist/checks/check-empty-catch.d.ts +6 -0
- package/dist/checks/check-empty-catch.d.ts.map +1 -0
- package/dist/checks/check-empty-catch.js +111 -0
- package/dist/checks/check-empty-catch.js.map +1 -0
- package/dist/checks/check-function-length.d.ts +6 -0
- package/dist/checks/check-function-length.d.ts.map +1 -0
- package/dist/checks/check-function-length.js +152 -0
- package/dist/checks/check-function-length.js.map +1 -0
- package/dist/checks/check-hardcoded-ai-models.d.ts +10 -0
- package/dist/checks/check-hardcoded-ai-models.d.ts.map +1 -0
- package/dist/checks/check-hardcoded-ai-models.js +102 -0
- package/dist/checks/check-hardcoded-ai-models.js.map +1 -0
- package/dist/checks/check-hardcoded-urls.d.ts +6 -0
- package/dist/checks/check-hardcoded-urls.d.ts.map +1 -0
- package/dist/checks/check-hardcoded-urls.js +124 -0
- package/dist/checks/check-hardcoded-urls.js.map +1 -0
- package/dist/checks/check-magic-numbers.d.ts +6 -0
- package/dist/checks/check-magic-numbers.d.ts.map +1 -0
- package/dist/checks/check-magic-numbers.js +116 -0
- package/dist/checks/check-magic-numbers.js.map +1 -0
- package/dist/checks/check-secrets.d.ts +6 -0
- package/dist/checks/check-secrets.d.ts.map +1 -0
- package/dist/checks/check-secrets.js +138 -0
- package/dist/checks/check-secrets.js.map +1 -0
- package/dist/checks/check-snake-case-ts.d.ts +6 -0
- package/dist/checks/check-snake-case-ts.d.ts.map +1 -0
- package/dist/checks/check-snake-case-ts.js +78 -0
- package/dist/checks/check-snake-case-ts.js.map +1 -0
- package/dist/checks/check-todo-fixme.d.ts +6 -0
- package/dist/checks/check-todo-fixme.d.ts.map +1 -0
- package/dist/checks/check-todo-fixme.js +41 -0
- package/dist/checks/check-todo-fixme.js.map +1 -0
- package/dist/checks/check-unsafe-html.d.ts +6 -0
- package/dist/checks/check-unsafe-html.d.ts.map +1 -0
- package/dist/checks/check-unsafe-html.js +101 -0
- package/dist/checks/check-unsafe-html.js.map +1 -0
- package/dist/checks/index.d.ts +30 -0
- package/dist/checks/index.d.ts.map +1 -0
- package/dist/checks/index.js +57 -0
- package/dist/checks/index.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +208 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/file-reader.d.ts +24 -0
- package/dist/utils/file-reader.d.ts.map +1 -0
- package/dist/utils/file-reader.js +146 -0
- package/dist/utils/file-reader.js.map +1 -0
- package/dist/utils/patterns.d.ts +27 -0
- package/dist/utils/patterns.d.ts.map +1 -0
- package/dist/utils/patterns.js +84 -0
- package/dist/utils/patterns.js.map +1 -0
- package/dist/utils/reporters.d.ts +21 -0
- package/dist/utils/reporters.d.ts.map +1 -0
- package/dist/utils/reporters.js +115 -0
- package/dist/utils/reporters.js.map +1 -0
- package/dist/utils/types.d.ts +71 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +5 -0
- package/dist/utils/types.js.map +1 -0
- package/package.json +83 -0
- package/ralph/api.sh +216 -0
- package/ralph/backup.sh +838 -0
- package/ralph/browser-verify/README.md +135 -0
- package/ralph/browser-verify/verify.ts +450 -0
- package/ralph/checks/check-fastapi-responses.py +155 -0
- package/ralph/hooks/hooks-config.json +72 -0
- package/ralph/hooks/inject-context.sh +44 -0
- package/ralph/hooks/install.sh +207 -0
- package/ralph/hooks/log-tools.sh +45 -0
- package/ralph/hooks/protect-prd.sh +27 -0
- package/ralph/hooks/save-learnings.sh +36 -0
- package/ralph/hooks/warn-debug.sh +54 -0
- package/ralph/hooks/warn-empty-catch.sh +63 -0
- package/ralph/hooks/warn-secrets.sh +89 -0
- package/ralph/hooks/warn-urls.sh +77 -0
- package/ralph/init.sh +515 -0
- package/ralph/loop.sh +730 -0
- package/ralph/playwright.sh +238 -0
- package/ralph/prd.sh +295 -0
- package/ralph/setup/feature-tour.sh +155 -0
- package/ralph/setup/quick-setup.sh +239 -0
- package/ralph/setup/tutorial.sh +159 -0
- package/ralph/setup/ui.sh +136 -0
- package/ralph/setup.sh +401 -0
- package/ralph/signs.sh +150 -0
- package/ralph/utils.sh +682 -0
- package/ralph/verify/browser.sh +324 -0
- package/ralph/verify/lint.sh +363 -0
- package/ralph/verify/review.sh +152 -0
- package/ralph/verify/tests.sh +81 -0
- package/ralph/verify.sh +268 -0
- package/templates/PROMPT.md +235 -0
- package/templates/config/fullstack.json +86 -0
- package/templates/config/go.json +81 -0
- package/templates/config/minimal.json +76 -0
- package/templates/config/node.json +81 -0
- package/templates/config/python.json +81 -0
- package/templates/config/rust.json +81 -0
- package/templates/examples/CLAUDE-django.md +174 -0
- package/templates/examples/CLAUDE-fastapi.md +270 -0
- package/templates/examples/CLAUDE-fastmcp.md +352 -0
- package/templates/examples/CLAUDE-fullstack.md +256 -0
- package/templates/examples/CLAUDE-node.md +246 -0
- package/templates/examples/CLAUDE-react.md +138 -0
- package/templates/optional/cursorrules.template +147 -0
- package/templates/optional/eslint.config.js +34 -0
- package/templates/optional/lint-staged.config.js +34 -0
- package/templates/optional/ruff.toml +125 -0
- package/templates/optional/vibe-check.yml +116 -0
- package/templates/optional/vscode-settings.json +127 -0
- package/templates/signs.json +46 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Check that FastAPI endpoints have Pydantic response models defined.
|
|
4
|
+
Ensures Swagger/OpenAPI docs show proper response schemas.
|
|
5
|
+
|
|
6
|
+
Usage: python check-fastapi-responses.py [directory]
|
|
7
|
+
|
|
8
|
+
Exit codes:
|
|
9
|
+
0 - All endpoints have response models
|
|
10
|
+
1 - Some endpoints missing response models
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import NamedTuple
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EndpointIssue(NamedTuple):
|
|
20
|
+
file: str
|
|
21
|
+
line: int
|
|
22
|
+
method: str
|
|
23
|
+
path: str
|
|
24
|
+
issue: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_file(filepath: Path) -> list[EndpointIssue]:
|
|
28
|
+
"""Check a single Python file for FastAPI endpoints without response models."""
|
|
29
|
+
issues = []
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
content = filepath.read_text()
|
|
33
|
+
tree = ast.parse(content)
|
|
34
|
+
except (SyntaxError, UnicodeDecodeError):
|
|
35
|
+
return issues
|
|
36
|
+
|
|
37
|
+
# HTTP methods that should have response models
|
|
38
|
+
http_methods = {'get', 'post', 'put', 'patch', 'delete'}
|
|
39
|
+
|
|
40
|
+
for node in ast.walk(tree):
|
|
41
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# Check decorators for FastAPI route decorators
|
|
45
|
+
for decorator in node.decorator_list:
|
|
46
|
+
# Handle @router.get("/path") or @app.get("/path")
|
|
47
|
+
if isinstance(decorator, ast.Call):
|
|
48
|
+
if isinstance(decorator.func, ast.Attribute):
|
|
49
|
+
method = decorator.func.attr
|
|
50
|
+
if method not in http_methods:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# Get the path from first argument
|
|
54
|
+
path = "unknown"
|
|
55
|
+
if decorator.args and isinstance(decorator.args[0], ast.Constant):
|
|
56
|
+
path = decorator.args[0].value
|
|
57
|
+
|
|
58
|
+
# Check for response_model in keyword arguments
|
|
59
|
+
has_response_model = any(
|
|
60
|
+
kw.arg == 'response_model'
|
|
61
|
+
for kw in decorator.keywords
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Check for return type annotation
|
|
65
|
+
has_return_annotation = node.returns is not None
|
|
66
|
+
|
|
67
|
+
# Check if return annotation is None or missing
|
|
68
|
+
is_none_return = (
|
|
69
|
+
isinstance(node.returns, ast.Constant) and
|
|
70
|
+
node.returns.value is None
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if not has_response_model and (not has_return_annotation or is_none_return):
|
|
74
|
+
# Skip certain common patterns that don't need response models
|
|
75
|
+
if method == 'delete' and path.endswith('}'):
|
|
76
|
+
continue # DELETE /items/{id} often returns nothing
|
|
77
|
+
|
|
78
|
+
issues.append(EndpointIssue(
|
|
79
|
+
file=str(filepath),
|
|
80
|
+
line=node.lineno,
|
|
81
|
+
method=method.upper(),
|
|
82
|
+
path=path,
|
|
83
|
+
issue="Missing response_model or return type annotation"
|
|
84
|
+
))
|
|
85
|
+
|
|
86
|
+
return issues
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check_directory(directory: Path) -> list[EndpointIssue]:
|
|
90
|
+
"""Check all Python files in directory for FastAPI response model issues."""
|
|
91
|
+
all_issues = []
|
|
92
|
+
|
|
93
|
+
# Common patterns for API files
|
|
94
|
+
patterns = [
|
|
95
|
+
'**/router*.py',
|
|
96
|
+
'**/routes*.py',
|
|
97
|
+
'**/api*.py',
|
|
98
|
+
'**/endpoints*.py',
|
|
99
|
+
'**/views*.py',
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
checked_files = set()
|
|
103
|
+
|
|
104
|
+
for pattern in patterns:
|
|
105
|
+
for filepath in directory.glob(pattern):
|
|
106
|
+
if filepath in checked_files:
|
|
107
|
+
continue
|
|
108
|
+
checked_files.add(filepath)
|
|
109
|
+
all_issues.extend(check_file(filepath))
|
|
110
|
+
|
|
111
|
+
# Also check any file with FastAPI imports
|
|
112
|
+
for filepath in directory.rglob('*.py'):
|
|
113
|
+
if filepath in checked_files:
|
|
114
|
+
continue
|
|
115
|
+
try:
|
|
116
|
+
content = filepath.read_text()
|
|
117
|
+
if 'from fastapi' in content or 'import fastapi' in content:
|
|
118
|
+
if 'APIRouter' in content or '@app.' in content or '@router.' in content:
|
|
119
|
+
all_issues.extend(check_file(filepath))
|
|
120
|
+
checked_files.add(filepath)
|
|
121
|
+
except (UnicodeDecodeError, PermissionError):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
return all_issues
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main():
|
|
128
|
+
directory = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('.')
|
|
129
|
+
|
|
130
|
+
if not directory.exists():
|
|
131
|
+
print(f"Error: Directory not found: {directory}")
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
issues = check_directory(directory)
|
|
135
|
+
|
|
136
|
+
if not issues:
|
|
137
|
+
print("✓ All FastAPI endpoints have response models defined")
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
|
|
140
|
+
print(f"Found {len(issues)} endpoint(s) without response models:\n")
|
|
141
|
+
|
|
142
|
+
for issue in sorted(issues, key=lambda x: (x.file, x.line)):
|
|
143
|
+
print(f" {issue.file}:{issue.line}")
|
|
144
|
+
print(f" {issue.method} {issue.path}")
|
|
145
|
+
print(f" → {issue.issue}\n")
|
|
146
|
+
|
|
147
|
+
print("Fix: Add response_model parameter or return type annotation:")
|
|
148
|
+
print(' @router.get("/items", response_model=list[ItemSchema])')
|
|
149
|
+
print(" async def get_items() -> list[ItemSchema]:")
|
|
150
|
+
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == '__main__':
|
|
155
|
+
main()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Copy this 'hooks' section into your ~/.claude/settings.json or .claude/settings.json",
|
|
3
|
+
"_instructions": "Replace VIBE_PATH with the path to your agentic-loop installation",
|
|
4
|
+
"hooks": {
|
|
5
|
+
"PreToolUse": [
|
|
6
|
+
{
|
|
7
|
+
"matcher": "Edit|Write",
|
|
8
|
+
"hooks": [
|
|
9
|
+
{
|
|
10
|
+
"type": "command",
|
|
11
|
+
"command": "VIBE_PATH/ralph/hooks/protect-prd.sh",
|
|
12
|
+
"timeout": 5
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"PostToolUse": [
|
|
18
|
+
{
|
|
19
|
+
"matcher": "Edit|Write",
|
|
20
|
+
"hooks": [
|
|
21
|
+
{
|
|
22
|
+
"type": "command",
|
|
23
|
+
"command": "VIBE_PATH/ralph/hooks/warn-debug.sh",
|
|
24
|
+
"timeout": 5
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "command",
|
|
28
|
+
"command": "VIBE_PATH/ralph/hooks/warn-secrets.sh",
|
|
29
|
+
"timeout": 5
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "VIBE_PATH/ralph/hooks/warn-urls.sh",
|
|
34
|
+
"timeout": 5
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"matcher": "*",
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "VIBE_PATH/ralph/hooks/log-tools.sh",
|
|
44
|
+
"timeout": 3
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"SessionStart": [
|
|
50
|
+
{
|
|
51
|
+
"hooks": [
|
|
52
|
+
{
|
|
53
|
+
"type": "command",
|
|
54
|
+
"command": "VIBE_PATH/ralph/hooks/inject-context.sh",
|
|
55
|
+
"timeout": 5
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"Stop": [
|
|
61
|
+
{
|
|
62
|
+
"hooks": [
|
|
63
|
+
{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "VIBE_PATH/ralph/hooks/save-learnings.sh",
|
|
66
|
+
"timeout": 10
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# inject-context.sh - Inject signs and recent progress at session start
|
|
3
|
+
# Hook: SessionStart
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
INPUT=$(cat)
|
|
8
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "."')
|
|
9
|
+
RALPH_DIR="$CWD/.ralph"
|
|
10
|
+
|
|
11
|
+
CONTEXT=""
|
|
12
|
+
|
|
13
|
+
# Inject signs (learned patterns)
|
|
14
|
+
if [[ -f "$RALPH_DIR/signs.json" ]]; then
|
|
15
|
+
SIGNS=$(jq -r '.signs[]? | "- [\(.category)] \(.pattern)"' "$RALPH_DIR/signs.json" 2>/dev/null | head -20)
|
|
16
|
+
if [[ -n "$SIGNS" ]]; then
|
|
17
|
+
CONTEXT="## Learned Patterns (Signs)\nApply these lessons:\n$SIGNS\n\n"
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Inject recent progress
|
|
22
|
+
if [[ -f "$RALPH_DIR/progress.txt" ]]; then
|
|
23
|
+
PROGRESS=$(tail -10 "$RALPH_DIR/progress.txt" 2>/dev/null)
|
|
24
|
+
if [[ -n "$PROGRESS" ]]; then
|
|
25
|
+
CONTEXT="${CONTEXT}## Recent Progress\n$PROGRESS\n\n"
|
|
26
|
+
fi
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Inject last failure context if exists
|
|
30
|
+
if [[ -f "$RALPH_DIR/last_failure.txt" ]]; then
|
|
31
|
+
FAILURE=$(cat "$RALPH_DIR/last_failure.txt" | head -50)
|
|
32
|
+
CONTEXT="${CONTEXT}## Previous Failure (FIX THIS)\n$FAILURE\n"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [[ -n "$CONTEXT" ]]; then
|
|
36
|
+
jq -n --arg ctx "$CONTEXT" '{
|
|
37
|
+
"continue": true,
|
|
38
|
+
"hookSpecificOutput": {
|
|
39
|
+
"additionalContext": $ctx
|
|
40
|
+
}
|
|
41
|
+
}'
|
|
42
|
+
else
|
|
43
|
+
echo '{"continue": true}'
|
|
44
|
+
fi
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install.sh - Install Ralph hooks into Claude Code settings
|
|
3
|
+
#
|
|
4
|
+
# Usage: ./install.sh [--global] [--force]
|
|
5
|
+
# --global: Install to ~/.claude/settings.json (applies to all projects)
|
|
6
|
+
# --force: Reinstall even if hooks already configured
|
|
7
|
+
# Default: Install to .claude/settings.json (project-level)
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
|
|
13
|
+
# Parse args
|
|
14
|
+
SETTINGS_FILE=".claude/settings.json"
|
|
15
|
+
FORCE=false
|
|
16
|
+
|
|
17
|
+
for arg in "$@"; do
|
|
18
|
+
case $arg in
|
|
19
|
+
--global)
|
|
20
|
+
SETTINGS_FILE="$HOME/.claude/settings.json"
|
|
21
|
+
;;
|
|
22
|
+
--force)
|
|
23
|
+
FORCE=true
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
# Colors
|
|
29
|
+
RED='\033[0;31m'
|
|
30
|
+
GREEN='\033[0;32m'
|
|
31
|
+
YELLOW='\033[1;33m'
|
|
32
|
+
NC='\033[0m'
|
|
33
|
+
|
|
34
|
+
# Auto-install jq if missing
|
|
35
|
+
install_jq() {
|
|
36
|
+
echo -e "${YELLOW}jq not found, installing...${NC}"
|
|
37
|
+
|
|
38
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
39
|
+
# macOS
|
|
40
|
+
if command -v brew &>/dev/null; then
|
|
41
|
+
brew install jq
|
|
42
|
+
else
|
|
43
|
+
echo -e "${RED}Error: Homebrew not found. Install jq manually:${NC}"
|
|
44
|
+
echo " brew install jq"
|
|
45
|
+
echo " Or: https://jqlang.github.io/jq/download/"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
elif command -v apt-get &>/dev/null; then
|
|
49
|
+
# Debian/Ubuntu
|
|
50
|
+
sudo apt-get update -qq && sudo apt-get install -y jq
|
|
51
|
+
elif command -v dnf &>/dev/null; then
|
|
52
|
+
# Fedora/RHEL 8+
|
|
53
|
+
sudo dnf install -y jq
|
|
54
|
+
elif command -v yum &>/dev/null; then
|
|
55
|
+
# CentOS/RHEL 7
|
|
56
|
+
sudo yum install -y jq
|
|
57
|
+
elif command -v pacman &>/dev/null; then
|
|
58
|
+
# Arch
|
|
59
|
+
sudo pacman -S --noconfirm jq
|
|
60
|
+
elif command -v apk &>/dev/null; then
|
|
61
|
+
# Alpine
|
|
62
|
+
sudo apk add jq
|
|
63
|
+
else
|
|
64
|
+
echo -e "${RED}Error: Could not detect package manager.${NC}"
|
|
65
|
+
echo "Install jq manually: https://jqlang.github.io/jq/download/"
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
echo -e "${GREEN}✓ jq installed${NC}"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Check for jq, install if missing
|
|
73
|
+
if ! command -v jq &>/dev/null; then
|
|
74
|
+
install_jq
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
echo "Installing Ralph hooks..."
|
|
78
|
+
echo " Hooks path: $SCRIPT_DIR"
|
|
79
|
+
echo " Settings: $SETTINGS_FILE"
|
|
80
|
+
echo ""
|
|
81
|
+
|
|
82
|
+
# Ensure settings directory exists
|
|
83
|
+
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
|
84
|
+
|
|
85
|
+
# Create settings file if it doesn't exist
|
|
86
|
+
if [[ ! -f "$SETTINGS_FILE" ]]; then
|
|
87
|
+
echo '{}' > "$SETTINGS_FILE"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Check if hooks already configured and valid
|
|
91
|
+
if [[ "$FORCE" != "true" ]] && jq -e '.hooks' "$SETTINGS_FILE" > /dev/null 2>&1; then
|
|
92
|
+
session_hook=$(jq -r '.hooks.SessionStart[0].hooks[0].command // empty' "$SETTINGS_FILE" 2>/dev/null)
|
|
93
|
+
stop_hook=$(jq -r '.hooks.Stop[0].hooks[0].command // empty' "$SETTINGS_FILE" 2>/dev/null)
|
|
94
|
+
|
|
95
|
+
# All hooks must exist AND point to current script directory
|
|
96
|
+
if [[ -n "$session_hook" && -x "$session_hook" && -n "$stop_hook" && -x "$stop_hook" ]]; then
|
|
97
|
+
if [[ "$session_hook" == "$SCRIPT_DIR/"* && "$stop_hook" == "$SCRIPT_DIR/"* ]]; then
|
|
98
|
+
echo -e "${YELLOW}Hooks already configured and valid.${NC}"
|
|
99
|
+
echo "Use --force to reinstall."
|
|
100
|
+
exit 0
|
|
101
|
+
else
|
|
102
|
+
echo -e "${YELLOW}Hooks point to different location, updating...${NC}"
|
|
103
|
+
echo ""
|
|
104
|
+
fi
|
|
105
|
+
else
|
|
106
|
+
echo -e "${YELLOW}Existing hooks are invalid, reinstalling...${NC}"
|
|
107
|
+
echo ""
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# Build hooks config with actual path
|
|
112
|
+
HOOKS_CONFIG=$(cat <<EOF
|
|
113
|
+
{
|
|
114
|
+
"PreToolUse": [
|
|
115
|
+
{
|
|
116
|
+
"matcher": "Edit|Write",
|
|
117
|
+
"hooks": [
|
|
118
|
+
{
|
|
119
|
+
"type": "command",
|
|
120
|
+
"command": "$SCRIPT_DIR/protect-prd.sh",
|
|
121
|
+
"timeout": 5
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
"PostToolUse": [
|
|
127
|
+
{
|
|
128
|
+
"matcher": "Edit|Write",
|
|
129
|
+
"hooks": [
|
|
130
|
+
{
|
|
131
|
+
"type": "command",
|
|
132
|
+
"command": "$SCRIPT_DIR/warn-debug.sh",
|
|
133
|
+
"timeout": 5
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"type": "command",
|
|
137
|
+
"command": "$SCRIPT_DIR/warn-secrets.sh",
|
|
138
|
+
"timeout": 5
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"type": "command",
|
|
142
|
+
"command": "$SCRIPT_DIR/warn-urls.sh",
|
|
143
|
+
"timeout": 5
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"type": "command",
|
|
147
|
+
"command": "$SCRIPT_DIR/warn-empty-catch.sh",
|
|
148
|
+
"timeout": 5
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"matcher": "*",
|
|
154
|
+
"hooks": [
|
|
155
|
+
{
|
|
156
|
+
"type": "command",
|
|
157
|
+
"command": "$SCRIPT_DIR/log-tools.sh",
|
|
158
|
+
"timeout": 3
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
"SessionStart": [
|
|
164
|
+
{
|
|
165
|
+
"hooks": [
|
|
166
|
+
{
|
|
167
|
+
"type": "command",
|
|
168
|
+
"command": "$SCRIPT_DIR/inject-context.sh",
|
|
169
|
+
"timeout": 5
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
"Stop": [
|
|
175
|
+
{
|
|
176
|
+
"hooks": [
|
|
177
|
+
{
|
|
178
|
+
"type": "command",
|
|
179
|
+
"command": "$SCRIPT_DIR/save-learnings.sh",
|
|
180
|
+
"timeout": 10
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
EOF
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Merge hooks into settings
|
|
190
|
+
CURRENT_SETTINGS=$(cat "$SETTINGS_FILE")
|
|
191
|
+
MERGED=$(echo "$CURRENT_SETTINGS" | jq --argjson hooks "$HOOKS_CONFIG" '.hooks = $hooks')
|
|
192
|
+
|
|
193
|
+
echo "$MERGED" > "$SETTINGS_FILE"
|
|
194
|
+
|
|
195
|
+
echo -e "${GREEN}✓ Hooks installed successfully!${NC}"
|
|
196
|
+
echo ""
|
|
197
|
+
echo "Hooks enabled:"
|
|
198
|
+
echo " • protect-prd.sh - Blocks edits to prd.json"
|
|
199
|
+
echo " • warn-debug.sh - Warns about console.log/debugger"
|
|
200
|
+
echo " • warn-secrets.sh - Warns about hardcoded secrets/API keys"
|
|
201
|
+
echo " • warn-urls.sh - Warns about hardcoded localhost URLs"
|
|
202
|
+
echo " • warn-empty-catch.sh - Warns about empty catch blocks"
|
|
203
|
+
echo " • inject-context.sh - Loads signs & progress at session start"
|
|
204
|
+
echo " • save-learnings.sh - Extracts learnings at session end"
|
|
205
|
+
echo " • log-tools.sh - Logs tool usage to .ralph/tool-log.txt"
|
|
206
|
+
echo ""
|
|
207
|
+
echo -e "${YELLOW}Note:${NC} Restart Claude Code for hooks to take effect."
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# log-tools.sh - Log tool usage for debugging and analysis
|
|
3
|
+
# Hook: PostToolUse matcher: "*"
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
INPUT=$(cat)
|
|
8
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "."')
|
|
9
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
|
|
10
|
+
RALPH_DIR="$CWD/.ralph"
|
|
11
|
+
|
|
12
|
+
# Only log if .ralph exists (we're in a Ralph session)
|
|
13
|
+
if [[ -d "$RALPH_DIR" ]]; then
|
|
14
|
+
TIMESTAMP=$(date '+%H:%M:%S')
|
|
15
|
+
|
|
16
|
+
# Extract relevant info based on tool
|
|
17
|
+
case "$TOOL_NAME" in
|
|
18
|
+
Read)
|
|
19
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' | sed "s|$CWD/||")
|
|
20
|
+
echo "[$TIMESTAMP] READ: $FILE" >> "$RALPH_DIR/tool-log.txt"
|
|
21
|
+
;;
|
|
22
|
+
Write)
|
|
23
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' | sed "s|$CWD/||")
|
|
24
|
+
echo "[$TIMESTAMP] WRITE: $FILE" >> "$RALPH_DIR/tool-log.txt"
|
|
25
|
+
;;
|
|
26
|
+
Edit)
|
|
27
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' | sed "s|$CWD/||")
|
|
28
|
+
echo "[$TIMESTAMP] EDIT: $FILE" >> "$RALPH_DIR/tool-log.txt"
|
|
29
|
+
;;
|
|
30
|
+
Bash)
|
|
31
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""' | head -c 80)
|
|
32
|
+
echo "[$TIMESTAMP] BASH: $CMD" >> "$RALPH_DIR/tool-log.txt"
|
|
33
|
+
;;
|
|
34
|
+
Grep|Glob)
|
|
35
|
+
PATTERN=$(echo "$INPUT" | jq -r '.tool_input.pattern // ""')
|
|
36
|
+
echo "[$TIMESTAMP] $TOOL_NAME: $PATTERN" >> "$RALPH_DIR/tool-log.txt"
|
|
37
|
+
;;
|
|
38
|
+
Task)
|
|
39
|
+
DESC=$(echo "$INPUT" | jq -r '.tool_input.description // ""')
|
|
40
|
+
echo "[$TIMESTAMP] TASK: $DESC" >> "$RALPH_DIR/tool-log.txt"
|
|
41
|
+
;;
|
|
42
|
+
esac
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo '{"continue": true}'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# protect-prd.sh - Protect prd.json from accidental edits
|
|
3
|
+
# Hook: PreToolUse matcher: "Edit|Write"
|
|
4
|
+
#
|
|
5
|
+
# Allows: /prd, /idea commands (they create .prd-edit-allowed marker)
|
|
6
|
+
# Blocks: Accidental edits during normal coding
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
|
|
12
|
+
|
|
13
|
+
# Check if editing prd.json
|
|
14
|
+
if [[ "$FILE_PATH" == *"prd.json"* ]]; then
|
|
15
|
+
# Allow if /prd or /idea set the bypass marker
|
|
16
|
+
if [[ -f ".ralph/.prd-edit-allowed" ]]; then
|
|
17
|
+
rm -f ".ralph/.prd-edit-allowed" # One-time use
|
|
18
|
+
echo '{"continue": true}'
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
echo "BLOCKED: prd.json is managed by Ralph. Use /prd or /idea to add stories." >&2
|
|
23
|
+
exit 2 # Exit code 2 = blocking error
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Allow all other edits
|
|
27
|
+
echo '{"continue": true}'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# save-learnings.sh - Extract potential learnings from session transcript
|
|
3
|
+
# Hook: Stop
|
|
4
|
+
#
|
|
5
|
+
# This hook analyzes the transcript for patterns that might be worth saving as signs.
|
|
6
|
+
# It logs suggestions to .ralph/suggested-signs.txt for human review.
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "."')
|
|
12
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""')
|
|
13
|
+
RALPH_DIR="$CWD/.ralph"
|
|
14
|
+
|
|
15
|
+
# Ensure .ralph directory exists
|
|
16
|
+
mkdir -p "$RALPH_DIR"
|
|
17
|
+
|
|
18
|
+
# Log session end
|
|
19
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Session ended" >> "$RALPH_DIR/progress.txt"
|
|
20
|
+
|
|
21
|
+
# If transcript exists, analyze for patterns
|
|
22
|
+
if [[ -f "$TRANSCRIPT_PATH" ]]; then
|
|
23
|
+
# Look for retry patterns (same error appearing multiple times)
|
|
24
|
+
# Look for "I learned" or "the issue was" phrases
|
|
25
|
+
# This is a simple heuristic - can be enhanced
|
|
26
|
+
|
|
27
|
+
PATTERNS=$(grep -E "(the issue was|I learned|the problem was|fixed by|solution was)" "$TRANSCRIPT_PATH" 2>/dev/null | tail -5 || true)
|
|
28
|
+
|
|
29
|
+
if [[ -n "$PATTERNS" ]]; then
|
|
30
|
+
echo "" >> "$RALPH_DIR/suggested-signs.txt"
|
|
31
|
+
echo "=== Session $(date '+%Y-%m-%d %H:%M:%S') ===" >> "$RALPH_DIR/suggested-signs.txt"
|
|
32
|
+
echo "$PATTERNS" >> "$RALPH_DIR/suggested-signs.txt"
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
echo '{"continue": true}'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# warn-debug.sh - Warn about debug statements in written code
|
|
3
|
+
# Hook: PostToolUse matcher: "Edit|Write"
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
INPUT=$(cat)
|
|
8
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
9
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
|
|
10
|
+
|
|
11
|
+
# Only check code files
|
|
12
|
+
case "$FILE_PATH" in
|
|
13
|
+
*.ts|*.tsx|*.js|*.jsx|*.py|*.go|*.rs)
|
|
14
|
+
;;
|
|
15
|
+
*)
|
|
16
|
+
echo '{"continue": true}'
|
|
17
|
+
exit 0
|
|
18
|
+
;;
|
|
19
|
+
esac
|
|
20
|
+
|
|
21
|
+
# Get the content that was written
|
|
22
|
+
NEW_CONTENT=""
|
|
23
|
+
if [[ "$TOOL_NAME" == "Write" ]]; then
|
|
24
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
|
|
25
|
+
elif [[ "$TOOL_NAME" == "Edit" ]]; then
|
|
26
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // ""')
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Check for debug patterns
|
|
30
|
+
WARNINGS=""
|
|
31
|
+
|
|
32
|
+
if echo "$NEW_CONTENT" | grep -qE 'console\.(log|debug|info|warn|error)\s*\('; then
|
|
33
|
+
WARNINGS="⚠️ Debug statement detected: console.log/debug. Remove before commit."
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if echo "$NEW_CONTENT" | grep -qE '^\s*debugger\s*;?\s*$'; then
|
|
37
|
+
WARNINGS="${WARNINGS}\n⚠️ Debugger statement detected. Remove before commit."
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if echo "$NEW_CONTENT" | grep -qE '^\s*print\s*\('; then
|
|
41
|
+
WARNINGS="${WARNINGS}\n⚠️ Print statement detected. Remove before commit."
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Output warning as additional context (non-blocking)
|
|
45
|
+
if [[ -n "$WARNINGS" ]]; then
|
|
46
|
+
jq -n --arg warn "$WARNINGS" '{
|
|
47
|
+
"continue": true,
|
|
48
|
+
"hookSpecificOutput": {
|
|
49
|
+
"additionalContext": $warn
|
|
50
|
+
}
|
|
51
|
+
}'
|
|
52
|
+
else
|
|
53
|
+
echo '{"continue": true}'
|
|
54
|
+
fi
|