agentic-loop 3.13.0 → 3.14.2
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/skills/idea/SKILL.md +56 -0
- package/.claude/skills/loopgram/SKILL.md +19 -0
- package/.claude/skills/prd/SKILL.md +2 -0
- package/README.md +1 -0
- package/bin/ralph.sh +17 -11
- package/dist/loopgram/claude.d.ts +18 -0
- package/dist/loopgram/claude.d.ts.map +1 -0
- package/dist/loopgram/claude.js +89 -0
- package/dist/loopgram/claude.js.map +1 -0
- package/dist/loopgram/context-search.d.ts +26 -0
- package/dist/loopgram/context-search.d.ts.map +1 -0
- package/dist/loopgram/context-search.js +175 -0
- package/dist/loopgram/context-search.js.map +1 -0
- package/dist/loopgram/conversation.d.ts +39 -0
- package/dist/loopgram/conversation.d.ts.map +1 -0
- package/dist/loopgram/conversation.js +158 -0
- package/dist/loopgram/conversation.js.map +1 -0
- package/dist/loopgram/index.d.ts +3 -0
- package/dist/loopgram/index.d.ts.map +1 -0
- package/dist/loopgram/index.js +246 -0
- package/dist/loopgram/index.js.map +1 -0
- package/dist/loopgram/loop-monitor.d.ts +16 -0
- package/dist/loopgram/loop-monitor.d.ts.map +1 -0
- package/dist/loopgram/loop-monitor.js +149 -0
- package/dist/loopgram/loop-monitor.js.map +1 -0
- package/dist/loopgram/loop-runner.d.ts +28 -0
- package/dist/loopgram/loop-runner.d.ts.map +1 -0
- package/dist/loopgram/loop-runner.js +157 -0
- package/dist/loopgram/loop-runner.js.map +1 -0
- package/dist/loopgram/prd-generator.d.ts +37 -0
- package/dist/loopgram/prd-generator.d.ts.map +1 -0
- package/dist/loopgram/prd-generator.js +134 -0
- package/dist/loopgram/prd-generator.js.map +1 -0
- package/dist/loopgram/saver.d.ts +9 -0
- package/dist/loopgram/saver.d.ts.map +1 -0
- package/dist/loopgram/saver.js +35 -0
- package/dist/loopgram/saver.js.map +1 -0
- package/dist/loopgram/types.d.ts +37 -0
- package/dist/loopgram/types.d.ts.map +1 -0
- package/dist/loopgram/types.js +5 -0
- package/dist/loopgram/types.js.map +1 -0
- package/package.json +6 -2
- package/ralph/hooks/common.sh +89 -0
- package/ralph/hooks/warn-debug.sh +14 -32
- package/ralph/hooks/warn-empty-catch.sh +13 -29
- package/ralph/hooks/warn-secrets.sh +19 -37
- package/ralph/hooks/warn-urls.sh +17 -33
- package/ralph/loop.sh +5 -2
- package/ralph/prd-check.sh +35 -8
- package/ralph/setup/quick-setup.sh +25 -12
- package/ralph/setup/ui.sh +0 -42
- package/ralph/setup.sh +71 -46
- package/ralph/utils.sh +167 -31
- package/templates/config/fastmcp.json +6 -1
- package/templates/config/fullstack.json +8 -0
- package/templates/config/node.json +8 -0
- package/templates/config/python.json +8 -0
|
@@ -1,63 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# shellcheck shell=bash
|
|
2
3
|
# warn-empty-catch.sh - Warn about empty catch blocks that silently swallow errors
|
|
3
4
|
# Hook: PostToolUse matcher: "Edit|Write"
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
source "$(dirname "$0")/common.sh"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
9
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
|
|
8
|
+
parse_hook_input
|
|
10
9
|
|
|
11
10
|
# Only check code files
|
|
12
|
-
|
|
13
|
-
|
|
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 // ""')
|
|
11
|
+
if ! is_code_file "ts,tsx,js,jsx,py"; then
|
|
12
|
+
hook_allow
|
|
13
|
+
exit 0
|
|
27
14
|
fi
|
|
28
15
|
|
|
29
16
|
WARNINGS=""
|
|
30
17
|
|
|
31
18
|
# JavaScript/TypeScript: catch (e) { } or catch { }
|
|
32
|
-
if echo "$NEW_CONTENT" | grep -qE 'catch
|
|
19
|
+
if echo "$NEW_CONTENT" | grep -qE 'catch[[:space:]]*\([^)]*\)[[:space:]]*\{[[:space:]]*\}'; then
|
|
33
20
|
WARNINGS="⚠️ Empty catch block detected. Handle the error or add a comment explaining why it's ignored."
|
|
34
21
|
fi
|
|
35
22
|
|
|
36
23
|
# Also check for catch with only a comment (no actual handling)
|
|
37
|
-
if echo "$NEW_CONTENT" | grep -qE 'catch
|
|
24
|
+
if echo "$NEW_CONTENT" | grep -qE 'catch[[:space:]]*\([^)]*\)[[:space:]]*\{[[:space:]]*(//[^}]*)?\}'; then
|
|
38
25
|
if [[ -z "$WARNINGS" ]]; then
|
|
39
26
|
WARNINGS="⚠️ Catch block with no error handling. Consider logging or rethrowing the error."
|
|
40
27
|
fi
|
|
41
28
|
fi
|
|
42
29
|
|
|
43
30
|
# Python: except: pass or except Exception: pass
|
|
44
|
-
if echo "$NEW_CONTENT" | grep -qE 'except
|
|
31
|
+
if echo "$NEW_CONTENT" | grep -qE 'except.*:[[:space:]]*pass[[:space:]]*$'; then
|
|
45
32
|
WARNINGS="⚠️ Empty except block (pass). Handle the exception or add logging."
|
|
46
33
|
fi
|
|
47
34
|
|
|
48
35
|
# Python: bare except with just pass on next line
|
|
49
|
-
if echo "$NEW_CONTENT" | grep -qE 'except
|
|
36
|
+
if echo "$NEW_CONTENT" | grep -qE 'except.*:[[:space:]]*$' && echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*pass[[:space:]]*$'; then
|
|
50
37
|
if [[ -z "$WARNINGS" ]]; then
|
|
51
38
|
WARNINGS="⚠️ Except block with only 'pass'. Consider logging or reraising the exception."
|
|
52
39
|
fi
|
|
53
40
|
fi
|
|
54
41
|
|
|
55
|
-
# Block if empty catch detected
|
|
42
|
+
# Block if empty catch detected (this hook blocks, doesn't just warn)
|
|
56
43
|
if [[ -n "$WARNINGS" ]]; then
|
|
57
|
-
|
|
58
|
-
"continue": false,
|
|
59
|
-
"message": $warn
|
|
60
|
-
}'
|
|
44
|
+
hook_block "$WARNINGS"
|
|
61
45
|
else
|
|
62
|
-
|
|
46
|
+
hook_allow
|
|
63
47
|
fi
|
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# shellcheck shell=bash
|
|
2
3
|
# warn-secrets.sh - Warn about potential secrets in written code
|
|
3
4
|
# Hook: PostToolUse matcher: "Edit|Write"
|
|
4
5
|
#
|
|
5
6
|
# Mirrors the pre-commit check-secrets patterns for consistency
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
source "$(dirname "$0")/common.sh"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
11
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
|
|
10
|
+
parse_hook_input
|
|
12
11
|
|
|
13
12
|
# Only check code files
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*)
|
|
18
|
-
echo '{"continue": true}'
|
|
19
|
-
exit 0
|
|
20
|
-
;;
|
|
21
|
-
esac
|
|
22
|
-
|
|
23
|
-
# Get the content that was written
|
|
24
|
-
NEW_CONTENT=""
|
|
25
|
-
if [[ "$TOOL_NAME" == "Write" ]]; then
|
|
26
|
-
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
|
|
27
|
-
elif [[ "$TOOL_NAME" == "Edit" ]]; then
|
|
28
|
-
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // ""')
|
|
13
|
+
if ! is_code_file "ts,tsx,js,jsx,py,json,yaml,yml,env,env.local,env.development,env.production"; then
|
|
14
|
+
hook_allow
|
|
15
|
+
exit 0
|
|
29
16
|
fi
|
|
30
17
|
|
|
31
18
|
WARNINGS=""
|
|
@@ -37,53 +24,48 @@ fi
|
|
|
37
24
|
|
|
38
25
|
# Stripe keys (sk_live_* or sk_test_*)
|
|
39
26
|
if echo "$NEW_CONTENT" | grep -qE 'sk_(live|test)_[0-9a-zA-Z]{24,}'; then
|
|
40
|
-
WARNINGS="${WARNINGS}
|
|
27
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: Stripe API key detected! Use environment variables."
|
|
41
28
|
fi
|
|
42
29
|
|
|
43
30
|
# GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_)
|
|
44
31
|
if echo "$NEW_CONTENT" | grep -qE 'gh[pousr]_[A-Za-z0-9_]{36,}'; then
|
|
45
|
-
WARNINGS="${WARNINGS}
|
|
32
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: GitHub token detected! Use environment variables."
|
|
46
33
|
fi
|
|
47
34
|
|
|
48
35
|
# Slack tokens (xoxb-, xoxp-, xoxa-, xoxr-, xoxs-)
|
|
49
36
|
if echo "$NEW_CONTENT" | grep -qE 'xox[baprs]-[0-9]{10,}-[0-9a-zA-Z]{24,}'; then
|
|
50
|
-
WARNINGS="${WARNINGS}
|
|
37
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: Slack token detected! Use environment variables."
|
|
51
38
|
fi
|
|
52
39
|
|
|
53
40
|
# SendGrid keys (SG.*)
|
|
54
41
|
if echo "$NEW_CONTENT" | grep -qE 'SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}'; then
|
|
55
|
-
WARNINGS="${WARNINGS}
|
|
42
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: SendGrid API key detected! Use environment variables."
|
|
56
43
|
fi
|
|
57
44
|
|
|
58
45
|
# Private keys
|
|
59
|
-
if echo "$NEW_CONTENT" | grep -
|
|
60
|
-
WARNINGS="${WARNINGS}
|
|
46
|
+
if echo "$NEW_CONTENT" | grep -q -- '-----BEGIN.*PRIVATE KEY-----'; then
|
|
47
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: Private key detected! Never commit private keys."
|
|
61
48
|
fi
|
|
62
49
|
|
|
63
50
|
# Generic API key patterns (api_key = "...", apikey: "...", etc.)
|
|
64
|
-
if echo "$NEW_CONTENT" | grep -qiE '(api[_-]?key|api[_-]?secret)
|
|
51
|
+
if echo "$NEW_CONTENT" | grep -qiE '(api[_-]?key|api[_-]?secret)[[:space:]]*[:=][[:space:]]*['"'"'"][a-zA-Z0-9_-]{20,}['"'"'"]'; then
|
|
65
52
|
# Check it's not a placeholder
|
|
66
53
|
if ! echo "$NEW_CONTENT" | grep -qiE '(example|placeholder|your[_-]?key|xxx|test|dummy|fake|sample|demo)'; then
|
|
67
|
-
WARNINGS="${WARNINGS}
|
|
54
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Possible hardcoded API key - use environment variables."
|
|
68
55
|
fi
|
|
69
56
|
fi
|
|
70
57
|
|
|
71
58
|
# Generic secrets (password = "...", token = "...", etc.)
|
|
72
|
-
if echo "$NEW_CONTENT" | grep -qiE '(password|passwd|pwd|secret|token)
|
|
59
|
+
if echo "$NEW_CONTENT" | grep -qiE '(password|passwd|pwd|secret|token)[[:space:]]*[:=][[:space:]]*['"'"'"][^'"'"'"]{8,}['"'"'"]'; then
|
|
73
60
|
# Check it's not a placeholder or type annotation
|
|
74
61
|
if ! echo "$NEW_CONTENT" | grep -qiE '(example|placeholder|xxx|test|dummy|type|interface|password:)'; then
|
|
75
|
-
WARNINGS="${WARNINGS}
|
|
62
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Possible hardcoded secret - use environment variables."
|
|
76
63
|
fi
|
|
77
64
|
fi
|
|
78
65
|
|
|
79
|
-
# Output warning
|
|
66
|
+
# Output warning or allow
|
|
80
67
|
if [[ -n "$WARNINGS" ]]; then
|
|
81
|
-
|
|
82
|
-
"continue": true,
|
|
83
|
-
"hookSpecificOutput": {
|
|
84
|
-
"additionalContext": $warn
|
|
85
|
-
}
|
|
86
|
-
}'
|
|
68
|
+
hook_warn "$WARNINGS"
|
|
87
69
|
else
|
|
88
|
-
|
|
70
|
+
hook_allow
|
|
89
71
|
fi
|
package/ralph/hooks/warn-urls.sh
CHANGED
|
@@ -1,35 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# shellcheck shell=bash
|
|
2
3
|
# warn-urls.sh - Warn about hardcoded URLs in written code
|
|
3
4
|
# Hook: PostToolUse matcher: "Edit|Write"
|
|
4
5
|
#
|
|
5
6
|
# Mirrors the pre-commit check-hardcoded-urls patterns for consistency
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
source "$(dirname "$0")/common.sh"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
11
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
|
|
10
|
+
parse_hook_input
|
|
12
11
|
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
;;
|
|
19
|
-
*.ts|*.tsx|*.js|*.jsx|*.py)
|
|
20
|
-
;;
|
|
21
|
-
*)
|
|
22
|
-
echo '{"continue": true}'
|
|
23
|
-
exit 0
|
|
24
|
-
;;
|
|
25
|
-
esac
|
|
12
|
+
# Skip test files
|
|
13
|
+
if is_test_file; then
|
|
14
|
+
hook_allow
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
26
17
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
elif [[ "$TOOL_NAME" == "Edit" ]]; then
|
|
32
|
-
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // ""')
|
|
18
|
+
# Only check code files
|
|
19
|
+
if ! is_code_file "ts,tsx,js,jsx,py"; then
|
|
20
|
+
hook_allow
|
|
21
|
+
exit 0
|
|
33
22
|
fi
|
|
34
23
|
|
|
35
24
|
WARNINGS=""
|
|
@@ -45,7 +34,7 @@ fi
|
|
|
45
34
|
# Check for 127.0.0.1 URLs
|
|
46
35
|
if echo "$NEW_CONTENT" | grep -qE 'https?://127\.0\.0\.1(:[0-9]+)?'; then
|
|
47
36
|
if ! echo "$NEW_CONTENT" | grep -qE '(\|\||\?\?|default|fallback).*127\.0\.0\.1'; then
|
|
48
|
-
WARNINGS="${WARNINGS}
|
|
37
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Hardcoded 127.0.0.1 URL detected - use environment variable"
|
|
49
38
|
fi
|
|
50
39
|
fi
|
|
51
40
|
|
|
@@ -60,18 +49,13 @@ if [[ -n "$PROD_URLS" ]]; then
|
|
|
60
49
|
PROD_URLS=$(echo "$PROD_URLS" | grep -v -E '(example|placeholder|test|mock)' || true)
|
|
61
50
|
if [[ -n "$PROD_URLS" ]]; then
|
|
62
51
|
FIRST_URL=$(echo "$PROD_URLS" | head -1)
|
|
63
|
-
WARNINGS="${WARNINGS}
|
|
52
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Hardcoded URL ($FIRST_URL) - consider using environment variable"
|
|
64
53
|
fi
|
|
65
54
|
fi
|
|
66
55
|
|
|
67
|
-
# Output warning
|
|
56
|
+
# Output warning or allow
|
|
68
57
|
if [[ -n "$WARNINGS" ]]; then
|
|
69
|
-
|
|
70
|
-
"continue": true,
|
|
71
|
-
"hookSpecificOutput": {
|
|
72
|
-
"additionalContext": $warn
|
|
73
|
-
}
|
|
74
|
-
}'
|
|
58
|
+
hook_warn "$WARNINGS"
|
|
75
59
|
else
|
|
76
|
-
|
|
60
|
+
hook_allow
|
|
77
61
|
fi
|
package/ralph/loop.sh
CHANGED
|
@@ -478,10 +478,13 @@ run_loop() {
|
|
|
478
478
|
rm -f "$RALPH_DIR/last_failure.txt"
|
|
479
479
|
rm -f "$RALPH_DIR/last_verification.log"
|
|
480
480
|
|
|
481
|
+
# Get story title for commit message and completion display
|
|
482
|
+
local title
|
|
483
|
+
title=$(jq -r --arg id "$story" '.stories[] | select(.id==$id) | .title' "$RALPH_DIR/prd.json")
|
|
484
|
+
|
|
481
485
|
# Auto-commit if git is available
|
|
482
486
|
if command -v git &>/dev/null && [[ -d ".git" ]]; then
|
|
483
|
-
local
|
|
484
|
-
title=$(jq -r --arg id "$story" '.stories[] | select(.id==$id) | .title' "$RALPH_DIR/prd.json")
|
|
487
|
+
local commit_log commit_success
|
|
485
488
|
commit_log=$(mktemp)
|
|
486
489
|
commit_success=false
|
|
487
490
|
|
package/ralph/prd-check.sh
CHANGED
|
@@ -282,7 +282,7 @@ _validate_and_fix_stories() {
|
|
|
282
282
|
|
|
283
283
|
# Issue counters (bash 3.2 compatible - no associative arrays)
|
|
284
284
|
local cnt_no_tests=0 cnt_backend_curl=0 cnt_backend_contract=0
|
|
285
|
-
local cnt_frontend_tsc=0 cnt_frontend_url=0 cnt_frontend_context=0
|
|
285
|
+
local cnt_frontend_tsc=0 cnt_frontend_url=0 cnt_frontend_context=0 cnt_frontend_mcp=0
|
|
286
286
|
local cnt_auth_security=0 cnt_list_pagination=0 cnt_prose_steps=0
|
|
287
287
|
local cnt_migration_prereq=0 cnt_naming_convention=0 cnt_bare_pytest=0
|
|
288
288
|
|
|
@@ -355,7 +355,7 @@ _validate_and_fix_stories() {
|
|
|
355
355
|
fi
|
|
356
356
|
fi
|
|
357
357
|
|
|
358
|
-
# Check 3: Frontend needs testUrl and
|
|
358
|
+
# Check 3: Frontend needs testUrl, contextFiles, and mcp
|
|
359
359
|
if [[ "$story_type" == "frontend" ]]; then
|
|
360
360
|
local has_url
|
|
361
361
|
has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
|
|
@@ -370,6 +370,14 @@ _validate_and_fix_stories() {
|
|
|
370
370
|
story_issues+="missing contextFiles, "
|
|
371
371
|
cnt_frontend_context=$((cnt_frontend_context + 1))
|
|
372
372
|
fi
|
|
373
|
+
|
|
374
|
+
# Frontend must have mcp tools for browser verification
|
|
375
|
+
local mcp_tools
|
|
376
|
+
mcp_tools=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .mcp // [] | length' "$prd_file")
|
|
377
|
+
if [[ "$mcp_tools" == "0" ]]; then
|
|
378
|
+
story_issues+="missing mcp (browser tools), "
|
|
379
|
+
cnt_frontend_mcp=$((cnt_frontend_mcp + 1))
|
|
380
|
+
fi
|
|
373
381
|
fi
|
|
374
382
|
|
|
375
383
|
# Check 4: Auth stories need security criteria
|
|
@@ -442,6 +450,7 @@ _validate_and_fix_stories() {
|
|
|
442
450
|
[[ $cnt_frontend_tsc -gt 0 ]] && echo " ${cnt_frontend_tsc}x frontend: add tsc/playwright"
|
|
443
451
|
[[ $cnt_frontend_url -gt 0 ]] && echo " ${cnt_frontend_url}x frontend: add testUrl"
|
|
444
452
|
[[ $cnt_frontend_context -gt 0 ]] && echo " ${cnt_frontend_context}x frontend: add contextFiles"
|
|
453
|
+
[[ $cnt_frontend_mcp -gt 0 ]] && echo " ${cnt_frontend_mcp}x frontend: add mcp browser tools"
|
|
445
454
|
[[ $cnt_auth_security -gt 0 ]] && echo " ${cnt_auth_security}x auth: add security criteria"
|
|
446
455
|
[[ $cnt_list_pagination -gt 0 ]] && echo " ${cnt_list_pagination}x list: add pagination"
|
|
447
456
|
[[ $cnt_migration_prereq -gt 0 ]] && echo " ${cnt_migration_prereq}x migration: add prerequisites (DB reset)"
|
|
@@ -467,27 +476,41 @@ _fix_stories_with_claude() {
|
|
|
467
476
|
local prd_file="$1"
|
|
468
477
|
local issues="$2"
|
|
469
478
|
|
|
479
|
+
# Read config values for context
|
|
480
|
+
local config_file="$RALPH_DIR/config.json"
|
|
481
|
+
local backend_url="" frontend_url=""
|
|
482
|
+
if [[ -f "$config_file" ]]; then
|
|
483
|
+
backend_url=$(jq -r '.urls.backend // .api.baseUrl // "http://localhost:8000"' "$config_file" 2>/dev/null)
|
|
484
|
+
frontend_url=$(jq -r '.urls.frontend // .playwright.baseUrl // "http://localhost:3000"' "$config_file" 2>/dev/null)
|
|
485
|
+
fi
|
|
486
|
+
|
|
470
487
|
local fix_prompt="Enhance test coverage for these stories. Output the COMPLETE updated prd.json.
|
|
471
488
|
|
|
472
489
|
STORIES TO OPTIMIZE:
|
|
473
490
|
$issues
|
|
474
491
|
|
|
492
|
+
CONFIG VALUES (use these):
|
|
493
|
+
- Backend URL: $backend_url (use as {config.urls.backend} in testSteps)
|
|
494
|
+
- Frontend URL: $frontend_url (use as {config.urls.frontend} in testUrl)
|
|
495
|
+
|
|
475
496
|
RULES:
|
|
476
497
|
1. Backend stories MUST have testSteps with curl commands that hit real endpoints
|
|
477
498
|
Example: curl -s -X POST {config.urls.backend}/api/users -d '...' | jq -e '.id'
|
|
478
499
|
2. Backend stories MUST have apiContract with endpoint, request, response
|
|
479
|
-
3. Frontend stories MUST have testUrl set to {config.urls.frontend}/page
|
|
500
|
+
3. Frontend stories MUST have testUrl set to {config.urls.frontend}/[page-path]
|
|
501
|
+
- Derive page path from story title (e.g., 'login form' → '/login', 'dashboard' → '/dashboard')
|
|
480
502
|
4. Frontend stories MUST have contextFiles array (include idea file path from originalContext)
|
|
481
|
-
5.
|
|
503
|
+
5. Frontend stories MUST have mcp array with browser tools: [\"playwright\", \"devtools\"]
|
|
504
|
+
6. Auth stories MUST have security acceptanceCriteria:
|
|
482
505
|
- Passwords hashed with bcrypt (cost 10+)
|
|
483
506
|
- Passwords NEVER in API responses
|
|
484
507
|
- Rate limiting on login attempts
|
|
485
|
-
|
|
508
|
+
7. List endpoints MUST have pagination acceptanceCriteria:
|
|
486
509
|
- Returns paginated results (max 100 per page)
|
|
487
510
|
- Accepts ?page=N&limit=N query params
|
|
488
|
-
|
|
511
|
+
8. Migration stories (creating alembic/versions, migrations/, or modifying models) MUST have prerequisites:
|
|
489
512
|
Example: \"prerequisites\": [{\"name\": \"Reset test DB\", \"command\": \"npm run db:reset:test\", \"when\": \"schema changes\"}]
|
|
490
|
-
|
|
513
|
+
9. Frontend/general stories that consume APIs MUST have notes about naming conventions:
|
|
491
514
|
Example: \"notes\": \"Transform API responses from snake_case to camelCase. Create typed interfaces with camelCase properties and map: const user = { userName: data.user_name }\"
|
|
492
515
|
|
|
493
516
|
CURRENT PRD:
|
|
@@ -591,7 +614,7 @@ validate_stories_quick() {
|
|
|
591
614
|
fi
|
|
592
615
|
fi
|
|
593
616
|
|
|
594
|
-
# Check 3: Frontend needs testUrl and
|
|
617
|
+
# Check 3: Frontend needs testUrl, contextFiles, and mcp
|
|
595
618
|
if [[ "$story_type" == "frontend" ]]; then
|
|
596
619
|
local has_url
|
|
597
620
|
has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
|
|
@@ -600,6 +623,10 @@ validate_stories_quick() {
|
|
|
600
623
|
local context_files
|
|
601
624
|
context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
|
|
602
625
|
[[ "$context_files" == "0" ]] && issues+="$story_id: missing contextFiles, "
|
|
626
|
+
|
|
627
|
+
local mcp_tools
|
|
628
|
+
mcp_tools=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .mcp // [] | length' "$prd_file")
|
|
629
|
+
[[ "$mcp_tools" == "0" ]] && issues+="$story_id: missing mcp, "
|
|
603
630
|
fi
|
|
604
631
|
|
|
605
632
|
# Check 4: Auth stories need security criteria
|
|
@@ -202,20 +202,33 @@ configure_mcp() {
|
|
|
202
202
|
# Create claude.json if it doesn't exist
|
|
203
203
|
[[ ! -f "$claude_json" ]] && echo '{}' > "$claude_json"
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
if jq -e '.mcpServers["chrome-devtools"]' "$claude_json" > /dev/null 2>&1; then
|
|
207
|
-
echo -e " ${DIM}Skipped: Chrome DevTools MCP already configured${NC}"
|
|
208
|
-
return
|
|
209
|
-
fi
|
|
205
|
+
local added_any=false
|
|
210
206
|
|
|
211
|
-
# Add Chrome
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
207
|
+
# Add Playwright MCP if not configured (uses chromium + headless to avoid Chrome conflicts)
|
|
208
|
+
if ! jq -e '.mcpServers["playwright"]' "$claude_json" > /dev/null 2>&1; then
|
|
209
|
+
local tmp=$(mktemp)
|
|
210
|
+
jq '.mcpServers["playwright"] = {
|
|
211
|
+
"command": "npx",
|
|
212
|
+
"args": ["-y", "@playwright/mcp@latest", "--browser", "chromium", "--headless"]
|
|
213
|
+
}' "$claude_json" > "$tmp" && mv "$tmp" "$claude_json"
|
|
214
|
+
echo -e " ${GREEN}${EMOJI_CHECK}${NC} Playwright MCP configured"
|
|
215
|
+
added_any=true
|
|
216
|
+
else
|
|
217
|
+
echo -e " ${DIM}Skipped: Playwright MCP already configured${NC}"
|
|
218
|
+
fi
|
|
217
219
|
|
|
218
|
-
|
|
220
|
+
# Add Chrome DevTools MCP if not configured
|
|
221
|
+
if ! jq -e '.mcpServers["chrome-devtools"]' "$claude_json" > /dev/null 2>&1; then
|
|
222
|
+
local tmp=$(mktemp)
|
|
223
|
+
jq '.mcpServers["chrome-devtools"] = {
|
|
224
|
+
"command": "npx",
|
|
225
|
+
"args": ["-y", "@anthropic-ai/mcp-server-chrome-devtools@0.0.5"]
|
|
226
|
+
}' "$claude_json" > "$tmp" && mv "$tmp" "$claude_json"
|
|
227
|
+
echo -e " ${GREEN}${EMOJI_CHECK}${NC} Chrome DevTools MCP configured"
|
|
228
|
+
added_any=true
|
|
229
|
+
else
|
|
230
|
+
echo -e " ${DIM}Skipped: Chrome DevTools MCP already configured${NC}"
|
|
231
|
+
fi
|
|
219
232
|
}
|
|
220
233
|
|
|
221
234
|
show_completion() {
|
package/ralph/setup/ui.sh
CHANGED
|
@@ -37,33 +37,6 @@ EOF
|
|
|
37
37
|
echo ""
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
show_welcome() {
|
|
41
|
-
echo -e " ${BOLD}Welcome!${NC} How would you like to get started?"
|
|
42
|
-
echo ""
|
|
43
|
-
echo -e " ${GREEN}[1]${NC} ${EMOJI_ROCKET} ${BOLD}Quick Setup${NC}"
|
|
44
|
-
echo -e " ${DIM}I know what I'm doing, just configure my project${NC}"
|
|
45
|
-
echo ""
|
|
46
|
-
echo -e " ${GREEN}[2]${NC} ${EMOJI_MOVIE} ${BOLD}Feature Tour${NC}"
|
|
47
|
-
echo -e " ${DIM}Show me what agentic-loop can do (auto-demo)${NC}"
|
|
48
|
-
echo ""
|
|
49
|
-
echo -e " ${GREEN}[3]${NC} ${EMOJI_BOOK} ${BOLD}New to Claude Code${NC}"
|
|
50
|
-
echo -e " ${DIM}Teach me the basics (interactive tutorial)${NC}"
|
|
51
|
-
echo ""
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
prompt_menu() {
|
|
55
|
-
local choice
|
|
56
|
-
while true; do
|
|
57
|
-
echo -ne " Press ${GREEN}1${NC}, ${GREEN}2${NC}, or ${GREEN}3${NC} (or ${DIM}q${NC} to quit): "
|
|
58
|
-
read -r -n 1 choice
|
|
59
|
-
echo ""
|
|
60
|
-
case "$choice" in
|
|
61
|
-
1|2|3|q|Q) echo "$choice"; return ;;
|
|
62
|
-
*) echo -e " ${RED}Invalid choice${NC}" ;;
|
|
63
|
-
esac
|
|
64
|
-
done
|
|
65
|
-
}
|
|
66
|
-
|
|
67
40
|
# Typewriter effect for demos
|
|
68
41
|
typewrite() {
|
|
69
42
|
local text="$1" delay="${2:-0.02}"
|
|
@@ -111,21 +84,6 @@ print_info() {
|
|
|
111
84
|
echo -e " ${CYAN}→${NC} $1"
|
|
112
85
|
}
|
|
113
86
|
|
|
114
|
-
# Spinner for long operations
|
|
115
|
-
spin() {
|
|
116
|
-
local pid=$1
|
|
117
|
-
local delay=0.1
|
|
118
|
-
local spinstr='|/-\'
|
|
119
|
-
while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do
|
|
120
|
-
local temp=${spinstr#?}
|
|
121
|
-
printf " [%c] " "$spinstr"
|
|
122
|
-
local spinstr=$temp${spinstr%"$temp"}
|
|
123
|
-
sleep $delay
|
|
124
|
-
printf "\b\b\b\b\b\b"
|
|
125
|
-
done
|
|
126
|
-
printf " \b\b\b\b\b\b"
|
|
127
|
-
}
|
|
128
|
-
|
|
129
87
|
# Confirmation prompt
|
|
130
88
|
confirm() {
|
|
131
89
|
local prompt="${1:-Proceed?}"
|
package/ralph/setup.sh
CHANGED
|
@@ -285,6 +285,7 @@ setup_claude_hooks() {
|
|
|
285
285
|
local settings_file=".claude/settings.json"
|
|
286
286
|
local src_hooks_dir="$pkg_root/ralph/hooks"
|
|
287
287
|
local project_hooks_dir=".ralph/hooks"
|
|
288
|
+
local global_hooks_dir="$HOME/.config/ralph/hooks"
|
|
288
289
|
|
|
289
290
|
echo "Installing Claude Code hooks..."
|
|
290
291
|
|
|
@@ -307,6 +308,11 @@ setup_claude_hooks() {
|
|
|
307
308
|
chmod +x "$project_hooks_dir"/*.sh 2>/dev/null || true
|
|
308
309
|
echo " Copied hooks to $project_hooks_dir/"
|
|
309
310
|
|
|
311
|
+
# Note if global hooks exist
|
|
312
|
+
if [[ -d "$global_hooks_dir" ]] && ls -1 "$global_hooks_dir"/*.sh &>/dev/null; then
|
|
313
|
+
echo " Found global hooks in $global_hooks_dir/"
|
|
314
|
+
fi
|
|
315
|
+
|
|
310
316
|
# Get absolute path to project hooks
|
|
311
317
|
local hooks_dir
|
|
312
318
|
hooks_dir="$(cd "$project_hooks_dir" && pwd)"
|
|
@@ -317,52 +323,70 @@ setup_claude_hooks() {
|
|
|
317
323
|
# Create settings file if it doesn't exist
|
|
318
324
|
[[ ! -f "$settings_file" ]] && echo '{}' > "$settings_file"
|
|
319
325
|
|
|
320
|
-
#
|
|
326
|
+
# Helper to resolve hook path (project takes priority over global)
|
|
327
|
+
_resolve_hook() {
|
|
328
|
+
local hook_name="$1"
|
|
329
|
+
if [[ -f "$project_hooks_dir/$hook_name" ]]; then
|
|
330
|
+
echo "$hooks_dir/$hook_name"
|
|
331
|
+
elif [[ -f "$global_hooks_dir/$hook_name" ]]; then
|
|
332
|
+
echo "$global_hooks_dir/$hook_name"
|
|
333
|
+
fi
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# Resolve each hook path (project hooks take priority over global)
|
|
337
|
+
local protect_prd warn_debug warn_secrets warn_urls warn_empty_catch log_tools inject_context save_learnings
|
|
338
|
+
protect_prd=$(_resolve_hook "protect-prd.sh")
|
|
339
|
+
warn_debug=$(_resolve_hook "warn-debug.sh")
|
|
340
|
+
warn_secrets=$(_resolve_hook "warn-secrets.sh")
|
|
341
|
+
warn_urls=$(_resolve_hook "warn-urls.sh")
|
|
342
|
+
warn_empty_catch=$(_resolve_hook "warn-empty-catch.sh")
|
|
343
|
+
log_tools=$(_resolve_hook "log-tools.sh")
|
|
344
|
+
inject_context=$(_resolve_hook "inject-context.sh")
|
|
345
|
+
save_learnings=$(_resolve_hook "save-learnings.sh")
|
|
346
|
+
|
|
347
|
+
# Build hooks arrays using jq for proper JSON
|
|
348
|
+
local pre_tool_hooks post_edit_hooks post_all_hooks session_start_hooks stop_hooks
|
|
349
|
+
|
|
350
|
+
# PreToolUse: protect-prd on Edit|Write
|
|
351
|
+
pre_tool_hooks="[]"
|
|
352
|
+
[[ -n "$protect_prd" ]] && pre_tool_hooks=$(jq -n --arg cmd "$protect_prd" '[{"type": "command", "command": $cmd, "timeout": 5}]')
|
|
353
|
+
|
|
354
|
+
# PostToolUse: warn-* hooks on Edit|Write
|
|
355
|
+
post_edit_hooks="[]"
|
|
356
|
+
for hook_path in "$warn_debug" "$warn_secrets" "$warn_urls" "$warn_empty_catch"; do
|
|
357
|
+
[[ -n "$hook_path" ]] && post_edit_hooks=$(echo "$post_edit_hooks" | jq --arg cmd "$hook_path" '. + [{"type": "command", "command": $cmd, "timeout": 5}]')
|
|
358
|
+
done
|
|
359
|
+
|
|
360
|
+
# PostToolUse: log-tools on all
|
|
361
|
+
post_all_hooks="[]"
|
|
362
|
+
[[ -n "$log_tools" ]] && post_all_hooks=$(jq -n --arg cmd "$log_tools" '[{"type": "command", "command": $cmd, "timeout": 3}]')
|
|
363
|
+
|
|
364
|
+
# SessionStart: inject-context
|
|
365
|
+
session_start_hooks="[]"
|
|
366
|
+
[[ -n "$inject_context" ]] && session_start_hooks=$(jq -n --arg cmd "$inject_context" '[{"type": "command", "command": $cmd, "timeout": 5}]')
|
|
367
|
+
|
|
368
|
+
# Stop: save-learnings
|
|
369
|
+
stop_hooks="[]"
|
|
370
|
+
[[ -n "$save_learnings" ]] && stop_hooks=$(jq -n --arg cmd "$save_learnings" '[{"type": "command", "command": $cmd, "timeout": 10}]')
|
|
371
|
+
|
|
372
|
+
# Build the complete hooks config
|
|
321
373
|
local hooks_config
|
|
322
|
-
hooks_config=$(
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
]
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
"
|
|
335
|
-
"hooks":
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
{"type": "command", "command": "$hooks_dir/warn-urls.sh", "timeout": 5},
|
|
339
|
-
{"type": "command", "command": "$hooks_dir/warn-empty-catch.sh", "timeout": 5}
|
|
340
|
-
]
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
"matcher": "*",
|
|
344
|
-
"hooks": [
|
|
345
|
-
{"type": "command", "command": "$hooks_dir/log-tools.sh", "timeout": 3}
|
|
346
|
-
]
|
|
347
|
-
}
|
|
348
|
-
],
|
|
349
|
-
"SessionStart": [
|
|
350
|
-
{
|
|
351
|
-
"hooks": [
|
|
352
|
-
{"type": "command", "command": "$hooks_dir/inject-context.sh", "timeout": 5}
|
|
353
|
-
]
|
|
354
|
-
}
|
|
355
|
-
],
|
|
356
|
-
"Stop": [
|
|
357
|
-
{
|
|
358
|
-
"hooks": [
|
|
359
|
-
{"type": "command", "command": "$hooks_dir/save-learnings.sh", "timeout": 10}
|
|
360
|
-
]
|
|
361
|
-
}
|
|
362
|
-
]
|
|
363
|
-
}
|
|
364
|
-
EOF
|
|
365
|
-
)
|
|
374
|
+
hooks_config=$(jq -n \
|
|
375
|
+
--argjson pre_tool "$pre_tool_hooks" \
|
|
376
|
+
--argjson post_edit "$post_edit_hooks" \
|
|
377
|
+
--argjson post_all "$post_all_hooks" \
|
|
378
|
+
--argjson session_start "$session_start_hooks" \
|
|
379
|
+
--argjson stop "$stop_hooks" \
|
|
380
|
+
'{
|
|
381
|
+
"PreToolUse": [{"matcher": "Edit|Write", "hooks": $pre_tool}],
|
|
382
|
+
"PostToolUse": [
|
|
383
|
+
{"matcher": "Edit|Write", "hooks": $post_edit},
|
|
384
|
+
{"matcher": "*", "hooks": $post_all}
|
|
385
|
+
],
|
|
386
|
+
"SessionStart": [{"hooks": $session_start}],
|
|
387
|
+
"Stop": [{"hooks": $stop}]
|
|
388
|
+
}'
|
|
389
|
+
)
|
|
366
390
|
|
|
367
391
|
# Merge hooks into settings
|
|
368
392
|
local tmp
|
|
@@ -562,13 +586,14 @@ setup_mcp() {
|
|
|
562
586
|
local added_any=false
|
|
563
587
|
|
|
564
588
|
# Add Playwright MCP if not configured
|
|
589
|
+
# Uses chromium + headless to avoid conflicts with user's Chrome session
|
|
565
590
|
if ! jq -e '.mcpServers["playwright"]' "$claude_json" > /dev/null 2>&1; then
|
|
566
591
|
echo "Configuring MCP servers..."
|
|
567
592
|
local tmp
|
|
568
593
|
tmp=$(mktemp)
|
|
569
594
|
jq '.mcpServers["playwright"] = {
|
|
570
595
|
"command": "npx",
|
|
571
|
-
"args": ["-y", "@playwright/mcp@latest"]
|
|
596
|
+
"args": ["-y", "@playwright/mcp@latest", "--browser", "chromium", "--headless"]
|
|
572
597
|
}' "$claude_json" > "$tmp" && mv "$tmp" "$claude_json"
|
|
573
598
|
echo " Added playwright MCP server (browser automation & testing)"
|
|
574
599
|
added_any=true
|