agentic-loop 3.19.0 → 3.21.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.
Files changed (51) hide show
  1. package/.claude/commands/tour.md +11 -7
  2. package/.claude/commands/vibe-help.md +5 -2
  3. package/.claude/commands/vibe-list.md +17 -2
  4. package/.claude/skills/prd/SKILL.md +21 -6
  5. package/.claude/skills/setup-review/SKILL.md +56 -0
  6. package/.claude/skills/tour/SKILL.md +11 -7
  7. package/.claude/skills/vibe-help/SKILL.md +2 -1
  8. package/.claude/skills/vibe-list/SKILL.md +5 -2
  9. package/.pre-commit-hooks.yaml +8 -0
  10. package/README.md +4 -0
  11. package/bin/agentic-loop.sh +7 -0
  12. package/bin/ralph.sh +29 -0
  13. package/dist/checks/check-signs-secrets.d.ts +9 -0
  14. package/dist/checks/check-signs-secrets.d.ts.map +1 -0
  15. package/dist/checks/check-signs-secrets.js +57 -0
  16. package/dist/checks/check-signs-secrets.js.map +1 -0
  17. package/dist/checks/index.d.ts +2 -5
  18. package/dist/checks/index.d.ts.map +1 -1
  19. package/dist/checks/index.js +4 -9
  20. package/dist/checks/index.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/package.json +2 -1
  26. package/ralph/hooks/common.sh +47 -0
  27. package/ralph/hooks/warn-debug.sh +12 -26
  28. package/ralph/hooks/warn-empty-catch.sh +21 -34
  29. package/ralph/hooks/warn-secrets.sh +39 -52
  30. package/ralph/hooks/warn-urls.sh +25 -45
  31. package/ralph/init.sh +58 -82
  32. package/ralph/loop.sh +506 -53
  33. package/ralph/prd-check.sh +177 -236
  34. package/ralph/prd.sh +5 -2
  35. package/ralph/setup/quick-setup.sh +2 -16
  36. package/ralph/setup.sh +68 -80
  37. package/ralph/signs.sh +8 -0
  38. package/ralph/uat.sh +2015 -0
  39. package/ralph/utils.sh +198 -69
  40. package/ralph/verify/tests.sh +65 -10
  41. package/templates/PROMPT.md +10 -4
  42. package/templates/UAT-PROMPT.md +197 -0
  43. package/templates/config/elixir.json +0 -2
  44. package/templates/config/fastmcp.json +0 -2
  45. package/templates/config/fullstack.json +2 -4
  46. package/templates/config/go.json +0 -2
  47. package/templates/config/minimal.json +0 -2
  48. package/templates/config/node.json +0 -2
  49. package/templates/config/python.json +0 -2
  50. package/templates/config/rust.json +0 -2
  51. package/templates/prd-example.json +6 -8
@@ -87,3 +87,50 @@ hook_block() {
87
87
  "message": $msg
88
88
  }'
89
89
  }
90
+
91
+ # Run a warn-* hook with shared boilerplate
92
+ # Usage: run_warn_hook <extensions> <check_fn> [--skip-tests] [--block]
93
+ # extensions: comma-separated file extensions to match (e.g., "ts,tsx,js,jsx,py")
94
+ # check_fn: function name that sets WARNINGS variable
95
+ # --skip-tests: skip test files
96
+ # --block: use hook_block instead of hook_warn on warning
97
+ run_warn_hook() {
98
+ local extensions="$1"
99
+ local check_fn="$2"
100
+ shift 2
101
+
102
+ local skip_tests=false
103
+ local block=false
104
+ while [[ $# -gt 0 ]]; do
105
+ case "$1" in
106
+ --skip-tests) skip_tests=true ;;
107
+ --block) block=true ;;
108
+ esac
109
+ shift
110
+ done
111
+
112
+ parse_hook_input
113
+
114
+ if [[ "$skip_tests" == "true" ]] && is_test_file; then
115
+ hook_allow
116
+ exit 0
117
+ fi
118
+
119
+ if ! is_code_file "$extensions"; then
120
+ hook_allow
121
+ exit 0
122
+ fi
123
+
124
+ WARNINGS=""
125
+ "$check_fn"
126
+
127
+ if [[ -n "$WARNINGS" ]]; then
128
+ if [[ "$block" == "true" ]]; then
129
+ hook_block "$WARNINGS"
130
+ else
131
+ hook_warn "$WARNINGS"
132
+ fi
133
+ else
134
+ hook_allow
135
+ fi
136
+ }
@@ -5,32 +5,18 @@
5
5
 
6
6
  source "$(dirname "$0")/common.sh"
7
7
 
8
- parse_hook_input
8
+ _check_debug() {
9
+ if echo "$NEW_CONTENT" | grep -qE 'console\.(log|debug|info|warn|error)[[:space:]]*\('; then
10
+ WARNINGS="⚠️ Debug statement detected: console.log/debug. Remove before commit."
11
+ fi
9
12
 
10
- # Only check code files
11
- if ! is_code_file "ts,tsx,js,jsx,py,go,rs"; then
12
- hook_allow
13
- exit 0
14
- fi
13
+ if echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*debugger[[:space:]]*;?[[:space:]]*$'; then
14
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Debugger statement detected. Remove before commit."
15
+ fi
15
16
 
16
- # Check for debug patterns
17
- WARNINGS=""
17
+ if echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*print[[:space:]]*\('; then
18
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Print statement detected. Remove before commit."
19
+ fi
20
+ }
18
21
 
19
- if echo "$NEW_CONTENT" | grep -qE 'console\.(log|debug|info|warn|error)[[:space:]]*\('; then
20
- WARNINGS="⚠️ Debug statement detected: console.log/debug. Remove before commit."
21
- fi
22
-
23
- if echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*debugger[[:space:]]*;?[[:space:]]*$'; then
24
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Debugger statement detected. Remove before commit."
25
- fi
26
-
27
- if echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*print[[:space:]]*\('; then
28
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Print statement detected. Remove before commit."
29
- fi
30
-
31
- # Output warning or allow
32
- if [[ -n "$WARNINGS" ]]; then
33
- hook_warn "$WARNINGS"
34
- else
35
- hook_allow
36
- fi
22
+ run_warn_hook "ts,tsx,js,jsx,py,go,rs" _check_debug
@@ -5,43 +5,30 @@
5
5
 
6
6
  source "$(dirname "$0")/common.sh"
7
7
 
8
- parse_hook_input
9
-
10
- # Only check code files
11
- if ! is_code_file "ts,tsx,js,jsx,py"; then
12
- hook_allow
13
- exit 0
14
- fi
15
-
16
- WARNINGS=""
17
-
18
- # JavaScript/TypeScript: catch (e) { } or catch { }
19
- if echo "$NEW_CONTENT" | grep -qE 'catch[[:space:]]*\([^)]*\)[[:space:]]*\{[[:space:]]*\}'; then
20
- WARNINGS="⚠️ Empty catch block detected. Handle the error or add a comment explaining why it's ignored."
21
- fi
8
+ _check_empty_catch() {
9
+ # JavaScript/TypeScript: catch (e) { } or catch { }
10
+ if echo "$NEW_CONTENT" | grep -qE 'catch[[:space:]]*\([^)]*\)[[:space:]]*\{[[:space:]]*\}'; then
11
+ WARNINGS="⚠️ Empty catch block detected. Handle the error or add a comment explaining why it's ignored."
12
+ fi
22
13
 
23
- # Also check for catch with only a comment (no actual handling)
24
- if echo "$NEW_CONTENT" | grep -qE 'catch[[:space:]]*\([^)]*\)[[:space:]]*\{[[:space:]]*(//[^}]*)?\}'; then
25
- if [[ -z "$WARNINGS" ]]; then
26
- WARNINGS="⚠️ Catch block with no error handling. Consider logging or rethrowing the error."
14
+ # Also check for catch with only a comment (no actual handling)
15
+ if echo "$NEW_CONTENT" | grep -qE 'catch[[:space:]]*\([^)]*\)[[:space:]]*\{[[:space:]]*(//[^}]*)?\}'; then
16
+ if [[ -z "$WARNINGS" ]]; then
17
+ WARNINGS="⚠️ Catch block with no error handling. Consider logging or rethrowing the error."
18
+ fi
27
19
  fi
28
- fi
29
20
 
30
- # Python: except: pass or except Exception: pass
31
- if echo "$NEW_CONTENT" | grep -qE 'except.*:[[:space:]]*pass[[:space:]]*$'; then
32
- WARNINGS="⚠️ Empty except block (pass). Handle the exception or add logging."
33
- fi
21
+ # Python: except: pass or except Exception: pass
22
+ if echo "$NEW_CONTENT" | grep -qE 'except.*:[[:space:]]*pass[[:space:]]*$'; then
23
+ WARNINGS="⚠️ Empty except block (pass). Handle the exception or add logging."
24
+ fi
34
25
 
35
- # Python: bare except with just pass on next line
36
- if echo "$NEW_CONTENT" | grep -qE 'except.*:[[:space:]]*$' && echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*pass[[:space:]]*$'; then
37
- if [[ -z "$WARNINGS" ]]; then
38
- WARNINGS="⚠️ Except block with only 'pass'. Consider logging or reraising the exception."
26
+ # Python: bare except with just pass on next line
27
+ if echo "$NEW_CONTENT" | grep -qE 'except.*:[[:space:]]*$' && echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*pass[[:space:]]*$'; then
28
+ if [[ -z "$WARNINGS" ]]; then
29
+ WARNINGS="⚠️ Except block with only 'pass'. Consider logging or reraising the exception."
30
+ fi
39
31
  fi
40
- fi
32
+ }
41
33
 
42
- # Block if empty catch detected (this hook blocks, doesn't just warn)
43
- if [[ -n "$WARNINGS" ]]; then
44
- hook_block "$WARNINGS"
45
- else
46
- hook_allow
47
- fi
34
+ run_warn_hook "ts,tsx,js,jsx,py" _check_empty_catch --block
@@ -7,65 +7,52 @@
7
7
 
8
8
  source "$(dirname "$0")/common.sh"
9
9
 
10
- parse_hook_input
11
-
12
- # Only check code files
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
16
- fi
17
-
18
- WARNINGS=""
19
-
20
- # AWS Access Key (AKIA followed by 16 alphanumeric chars)
21
- if echo "$NEW_CONTENT" | grep -qE 'AKIA[0-9A-Z]{16}'; then
22
- WARNINGS="🚨 SECURITY: Possible AWS Access Key detected! Use environment variables."
23
- fi
10
+ _check_secrets() {
11
+ # AWS Access Key (AKIA followed by 16 alphanumeric chars)
12
+ if echo "$NEW_CONTENT" | grep -qE 'AKIA[0-9A-Z]{16}'; then
13
+ WARNINGS="🚨 SECURITY: Possible AWS Access Key detected! Use environment variables."
14
+ fi
24
15
 
25
- # Stripe keys (sk_live_* or sk_test_*)
26
- if echo "$NEW_CONTENT" | grep -qE 'sk_(live|test)_[0-9a-zA-Z]{24,}'; then
27
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: Stripe API key detected! Use environment variables."
28
- fi
16
+ # Stripe keys (sk_live_* or sk_test_*)
17
+ if echo "$NEW_CONTENT" | grep -qE 'sk_(live|test)_[0-9a-zA-Z]{24,}'; then
18
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: Stripe API key detected! Use environment variables."
19
+ fi
29
20
 
30
- # GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_)
31
- if echo "$NEW_CONTENT" | grep -qE 'gh[pousr]_[A-Za-z0-9_]{36,}'; then
32
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: GitHub token detected! Use environment variables."
33
- fi
21
+ # GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_)
22
+ if echo "$NEW_CONTENT" | grep -qE 'gh[pousr]_[A-Za-z0-9_]{36,}'; then
23
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: GitHub token detected! Use environment variables."
24
+ fi
34
25
 
35
- # Slack tokens (xoxb-, xoxp-, xoxa-, xoxr-, xoxs-)
36
- if echo "$NEW_CONTENT" | grep -qE 'xox[baprs]-[0-9]{10,}-[0-9a-zA-Z]{24,}'; then
37
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: Slack token detected! Use environment variables."
38
- fi
26
+ # Slack tokens (xoxb-, xoxp-, xoxa-, xoxr-, xoxs-)
27
+ if echo "$NEW_CONTENT" | grep -qE 'xox[baprs]-[0-9]{10,}-[0-9a-zA-Z]{24,}'; then
28
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: Slack token detected! Use environment variables."
29
+ fi
39
30
 
40
- # SendGrid keys (SG.*)
41
- if echo "$NEW_CONTENT" | grep -qE 'SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}'; then
42
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: SendGrid API key detected! Use environment variables."
43
- fi
31
+ # SendGrid keys (SG.*)
32
+ if echo "$NEW_CONTENT" | grep -qE 'SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}'; then
33
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: SendGrid API key detected! Use environment variables."
34
+ fi
44
35
 
45
- # Private keys
46
- if echo "$NEW_CONTENT" | grep -q -- '-----BEGIN.*PRIVATE KEY-----'; then
47
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: Private key detected! Never commit private keys."
48
- fi
36
+ # Private keys
37
+ if echo "$NEW_CONTENT" | grep -q -- '-----BEGIN.*PRIVATE KEY-----'; then
38
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}🚨 SECURITY: Private key detected! Never commit private keys."
39
+ fi
49
40
 
50
- # Generic API key patterns (api_key = "...", apikey: "...", etc.)
51
- if echo "$NEW_CONTENT" | grep -qiE '(api[_-]?key|api[_-]?secret)[[:space:]]*[:=][[:space:]]*['"'"'"][a-zA-Z0-9_-]{20,}['"'"'"]'; then
52
- # Check it's not a placeholder
53
- if ! echo "$NEW_CONTENT" | grep -qiE '(example|placeholder|your[_-]?key|xxx|test|dummy|fake|sample|demo)'; then
54
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Possible hardcoded API key - use environment variables."
41
+ # Generic API key patterns (api_key = "...", apikey: "...", etc.)
42
+ if echo "$NEW_CONTENT" | grep -qiE '(api[_-]?key|api[_-]?secret)[[:space:]]*[:=][[:space:]]*['"'"'"][a-zA-Z0-9_-]{20,}['"'"'"]'; then
43
+ # Check it's not a placeholder
44
+ if ! echo "$NEW_CONTENT" | grep -qiE '(example|placeholder|your[_-]?key|xxx|test|dummy|fake|sample|demo)'; then
45
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Possible hardcoded API key - use environment variables."
46
+ fi
55
47
  fi
56
- fi
57
48
 
58
- # Generic secrets (password = "...", token = "...", etc.)
59
- if echo "$NEW_CONTENT" | grep -qiE '(password|passwd|pwd|secret|token)[[:space:]]*[:=][[:space:]]*['"'"'"][^'"'"'"]{8,}['"'"'"]'; then
60
- # Check it's not a placeholder or type annotation
61
- if ! echo "$NEW_CONTENT" | grep -qiE '(example|placeholder|xxx|test|dummy|type|interface|password:)'; then
62
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Possible hardcoded secret - use environment variables."
49
+ # Generic secrets (password = "...", token = "...", etc.)
50
+ if echo "$NEW_CONTENT" | grep -qiE '(password|passwd|pwd|secret|token)[[:space:]]*[:=][[:space:]]*['"'"'"][^'"'"'"]{8,}['"'"'"]'; then
51
+ # Check it's not a placeholder or type annotation
52
+ if ! echo "$NEW_CONTENT" | grep -qiE '(example|placeholder|xxx|test|dummy|type|interface|password:)'; then
53
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Possible hardcoded secret - use environment variables."
54
+ fi
63
55
  fi
64
- fi
56
+ }
65
57
 
66
- # Output warning or allow
67
- if [[ -n "$WARNINGS" ]]; then
68
- hook_warn "$WARNINGS"
69
- else
70
- hook_allow
71
- fi
58
+ run_warn_hook "ts,tsx,js,jsx,py,json,yaml,yml,env,env.local,env.development,env.production" _check_secrets
@@ -7,55 +7,35 @@
7
7
 
8
8
  source "$(dirname "$0")/common.sh"
9
9
 
10
- parse_hook_input
11
-
12
- # Skip test files
13
- if is_test_file; then
14
- hook_allow
15
- exit 0
16
- fi
17
-
18
- # Only check code files
19
- if ! is_code_file "ts,tsx,js,jsx,py"; then
20
- hook_allow
21
- exit 0
22
- fi
23
-
24
- WARNINGS=""
10
+ SAFE_DOMAINS="cdn.jsdelivr.net|cdnjs.cloudflare.com|unpkg.com|fonts.googleapis.com|fonts.gstatic.com|api.github.com|raw.githubusercontent.com|registry.npmjs.org|schema.org|www.w3.org|example.com|example.org"
25
11
 
26
- # Check for localhost URLs
27
- if echo "$NEW_CONTENT" | grep -qE 'https?://localhost(:[0-9]+)?'; then
28
- # Skip if it's in a fallback pattern (|| or ?? or default)
29
- if ! echo "$NEW_CONTENT" | grep -qE '(\|\||\?\?|default|fallback).*localhost'; then
30
- WARNINGS="⚠️ Hardcoded localhost URL detected - use environment variable (e.g., process.env.API_URL)"
12
+ _check_urls() {
13
+ # Check for localhost URLs
14
+ if echo "$NEW_CONTENT" | grep -qE 'https?://localhost(:[0-9]+)?'; then
15
+ if ! echo "$NEW_CONTENT" | grep -qE '(\|\||\?\?|default|fallback).*localhost'; then
16
+ WARNINGS="⚠️ Hardcoded localhost URL detected - use environment variable (e.g., process.env.API_URL)"
17
+ fi
31
18
  fi
32
- fi
33
19
 
34
- # Check for 127.0.0.1 URLs
35
- if echo "$NEW_CONTENT" | grep -qE 'https?://127\.0\.0\.1(:[0-9]+)?'; then
36
- if ! echo "$NEW_CONTENT" | grep -qE '(\|\||\?\?|default|fallback).*127\.0\.0\.1'; then
37
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Hardcoded 127.0.0.1 URL detected - use environment variable"
20
+ # Check for 127.0.0.1 URLs
21
+ if echo "$NEW_CONTENT" | grep -qE 'https?://127\.0\.0\.1(:[0-9]+)?'; then
22
+ if ! echo "$NEW_CONTENT" | grep -qE '(\|\||\?\?|default|fallback).*127\.0\.0\.1'; then
23
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Hardcoded 127.0.0.1 URL detected - use environment variable"
24
+ fi
38
25
  fi
39
- fi
40
-
41
- # Check for hardcoded production-looking URLs (skip CDNs and common safe domains)
42
- SAFE_DOMAINS="cdn.jsdelivr.net|cdnjs.cloudflare.com|unpkg.com|fonts.googleapis.com|fonts.gstatic.com|api.github.com|raw.githubusercontent.com|registry.npmjs.org|schema.org|www.w3.org|example.com|example.org"
43
-
44
- # Look for https:// URLs that aren't safe domains
45
- PROD_URLS=$(echo "$NEW_CONTENT" | grep -oE 'https://[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' | grep -vE "$SAFE_DOMAINS" || true)
46
26
 
47
- if [[ -n "$PROD_URLS" ]]; then
48
- # Skip if they look like example/placeholder URLs
49
- PROD_URLS=$(echo "$PROD_URLS" | grep -v -E '(example|placeholder|test|mock)' || true)
50
- if [[ -n "$PROD_URLS" ]]; then
51
- FIRST_URL=$(echo "$PROD_URLS" | head -1)
52
- WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Hardcoded URL ($FIRST_URL) - consider using environment variable"
27
+ # Look for https:// URLs that aren't safe domains
28
+ local prod_urls
29
+ prod_urls=$(echo "$NEW_CONTENT" | grep -oE 'https://[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' | grep -vE "$SAFE_DOMAINS" || true)
30
+
31
+ if [[ -n "$prod_urls" ]]; then
32
+ prod_urls=$(echo "$prod_urls" | grep -v -E '(example|placeholder|test|mock)' || true)
33
+ if [[ -n "$prod_urls" ]]; then
34
+ local first_url
35
+ first_url=$(echo "$prod_urls" | head -1)
36
+ WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Hardcoded URL ($first_url) - consider using environment variable"
37
+ fi
53
38
  fi
54
- fi
39
+ }
55
40
 
56
- # Output warning or allow
57
- if [[ -n "$WARNINGS" ]]; then
58
- hook_warn "$WARNINGS"
59
- else
60
- hook_allow
61
- fi
41
+ run_warn_hook "ts,tsx,js,jsx,py" _check_urls --skip-tests
package/ralph/init.sh CHANGED
@@ -84,7 +84,7 @@ configure_test_auth() {
84
84
  print_info "=== Test Authentication Setup ==="
85
85
  echo ""
86
86
  echo "Ralph needs test credentials to verify authenticated endpoints."
87
- echo "(You can skip this and edit .ralph/config.json later)"
87
+ echo "(You can skip this and add to .env later)"
88
88
  echo ""
89
89
 
90
90
  # Ask if they want to configure auth
@@ -92,7 +92,7 @@ configure_test_auth() {
92
92
  echo ""
93
93
 
94
94
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
95
- print_info "Skipped. Edit .ralph/config.json to add credentials later."
95
+ print_info "Skipped. Add RALPH_TEST_USER and RALPH_TEST_PASSWORD to .env when needed."
96
96
  return 0
97
97
  fi
98
98
 
@@ -103,84 +103,31 @@ configure_test_auth() {
103
103
 
104
104
  if [[ -z "$test_user" || -z "$test_password" ]]; then
105
105
  print_warning "Credentials not provided."
106
- echo " Options to add them later:"
107
- echo " 1. Edit .ralph/config.json (stored in plain text)"
108
- echo " 2. Set RALPH_TEST_USER and RALPH_TEST_PASSWORD env vars (recommended)"
106
+ echo " Add to .env later:"
107
+ echo " RALPH_TEST_USER=your-email"
108
+ echo " RALPH_TEST_PASSWORD=your-password"
109
109
  return 0
110
110
  fi
111
111
 
112
- # Update config.json with credentials
113
- local config="$RALPH_DIR/config.json"
114
- if [[ -f "$config" ]]; then
112
+ # Save credentials to .env (gitignored, never committed)
113
+ if [[ ! -f ".env" ]]; then
114
+ touch ".env"
115
+ fi
116
+
117
+ # Remove old entries if present
118
+ if grep -q "^RALPH_TEST_USER=" .env 2>/dev/null; then
115
119
  local tmpfile
116
120
  tmpfile=$(mktemp)
117
- if jq --arg user "$test_user" --arg pass "$test_password" \
118
- '.auth.testUser = $user | .auth.testPassword = $pass' \
119
- "$config" > "$tmpfile" 2>/dev/null; then
120
- mv "$tmpfile" "$config"
121
- print_success "Test credentials saved to .ralph/config.json"
122
- print_warning "Note: Credentials stored in plain text. Consider using env vars instead:"
123
- echo " export RALPH_TEST_USER='$test_user'"
124
- echo " export RALPH_TEST_PASSWORD='****'"
125
- else
126
- rm -f "$tmpfile"
127
- print_warning "Failed to update config. Edit .ralph/config.json manually."
128
- fi
121
+ grep -v "^RALPH_TEST_USER=\|^RALPH_TEST_PASSWORD=" .env > "$tmpfile" && mv "$tmpfile" .env
129
122
  fi
130
- }
131
123
 
132
- # Detect the type of project based on files present
133
- detect_project_type() {
134
- local project_type="minimal"
135
-
136
- # Check for fullstack patterns first (more specific)
137
- if [[ -d "frontend" && -d "core" ]]; then
138
- project_type="fullstack"
139
- elif [[ -d "frontend" && -d "backend" ]]; then
140
- project_type="fullstack"
141
- elif [[ -d "apps" ]]; then
142
- project_type="fullstack" # Monorepo
143
- # Then check for single-language projects
144
- elif [[ -f "Cargo.toml" ]]; then
145
- project_type="rust"
146
- elif [[ -f "go.mod" ]]; then
147
- project_type="go"
148
- elif [[ -f "mix.exs" ]]; then
149
- project_type="elixir"
150
- # Check for Python framework variants (more specific first)
151
- elif [[ -f "pyproject.toml" ]]; then
152
- # FastMCP detection (check for fastmcp in any quote style)
153
- if grep -qiE "(fastmcp|\"fastmcp\"|'fastmcp')" pyproject.toml 2>/dev/null; then
154
- project_type="fastmcp"
155
- # Django detection
156
- elif grep -qiE "(django|\"django\"|'django')" pyproject.toml 2>/dev/null || [[ -f "manage.py" ]]; then
157
- project_type="django"
158
- # FastAPI detection
159
- elif grep -qiE "(fastapi|\"fastapi\"|'fastapi')" pyproject.toml 2>/dev/null; then
160
- project_type="fastapi"
161
- else
162
- project_type="python"
163
- fi
164
- elif [[ -f "requirements.txt" || -f "setup.py" ]]; then
165
- # Check requirements.txt for frameworks
166
- if [[ -f "requirements.txt" ]]; then
167
- if grep -qi 'fastmcp' requirements.txt 2>/dev/null; then
168
- project_type="fastmcp"
169
- elif grep -qi 'django' requirements.txt 2>/dev/null || [[ -f "manage.py" ]]; then
170
- project_type="django"
171
- elif grep -qi 'fastapi' requirements.txt 2>/dev/null; then
172
- project_type="fastapi"
173
- else
174
- project_type="python"
175
- fi
176
- else
177
- project_type="python"
178
- fi
179
- elif [[ -f "package.json" ]]; then
180
- project_type="node"
181
- fi
124
+ # Append credentials
125
+ echo "" >> .env
126
+ echo "# Test credentials for browser automation (auto-added by ralph init)" >> .env
127
+ printf 'RALPH_TEST_USER=%s\n' "$test_user" >> .env
128
+ printf 'RALPH_TEST_PASSWORD=%s\n' "$test_password" >> .env
182
129
 
183
- echo "$project_type"
130
+ print_success "Test credentials saved to .env (gitignored — never committed)"
184
131
  }
185
132
 
186
133
  # Auto-detect and configure project-specific settings
@@ -215,7 +162,7 @@ auto_configure_project() {
215
162
  fi
216
163
  fi
217
164
 
218
- # 2. Detect testUrlBase by parsing actual config files for port values
165
+ # 2. Detect urls.frontend by parsing actual config files for port values
219
166
  local base_url=""
220
167
  local web_port=""
221
168
 
@@ -281,11 +228,11 @@ auto_configure_project() {
281
228
  fi
282
229
 
283
230
  if [[ -n "$base_url" ]]; then
284
- if jq -e '.testUrlBase' "$tmpfile" >/dev/null 2>&1 && [[ "$(jq -r '.testUrlBase' "$tmpfile")" != "" ]]; then
285
- : # Already set
286
- else
287
- jq --arg url "$base_url" '.testUrlBase = $url' "$tmpfile" > "${tmpfile}.new" && mv "${tmpfile}.new" "$tmpfile"
288
- echo " Auto-detected testUrlBase: $base_url"
231
+ local existing_frontend
232
+ existing_frontend=$(jq -r '.urls.frontend // empty' "$tmpfile" 2>/dev/null)
233
+ if [[ -z "$existing_frontend" ]]; then
234
+ jq --arg url "$base_url" '.urls.frontend = $url' "$tmpfile" > "${tmpfile}.new" && mv "${tmpfile}.new" "$tmpfile"
235
+ echo " Auto-detected urls.frontend: $base_url"
289
236
  updated=true
290
237
  fi
291
238
  fi
@@ -448,7 +395,7 @@ auto_configure_project() {
448
395
  if ! jq -e '.mcp.serverModule' "$tmpfile" >/dev/null 2>&1 || [[ "$(jq -r '.mcp.serverModule' "$tmpfile")" == "" ]]; then
449
396
  jq --arg mod "$mcp_module" '.mcp.serverModule = $mod' "$tmpfile" > "${tmpfile}.new" && mv "${tmpfile}.new" "$tmpfile"
450
397
  # Also update the dev command
451
- jq --arg cmd "python -m ${mcp_module}.server" '.commands.dev = $cmd' "$tmpfile" > "${tmpfile}.new" && mv "${tmpfile}.new" "$tmpfile"
398
+ jq --arg cmd "python3 -m ${mcp_module}.server" '.commands.dev = $cmd' "$tmpfile" > "${tmpfile}.new" && mv "${tmpfile}.new" "$tmpfile"
452
399
  echo " Auto-detected mcp.serverModule: $mcp_module"
453
400
  updated=true
454
401
  fi
@@ -586,11 +533,13 @@ auto_configure_project() {
586
533
  updated=true
587
534
  fi
588
535
  else
589
- # No tests found - check if warning is suppressed
536
+ # No tests found by auto-detection - check if already configured or warning suppressed
590
537
  local require_tests
591
538
  require_tests=$(jq -r '.checks.requireTests // true' "$tmpfile" 2>/dev/null)
539
+ local existing_test_dir
540
+ existing_test_dir=$(jq -r '.tests.directory // empty' "$tmpfile" 2>/dev/null)
592
541
 
593
- if [[ "$require_tests" == "true" ]]; then
542
+ if [[ "$require_tests" == "true" && -z "$existing_test_dir" ]]; then
594
543
  echo ""
595
544
  print_warning "No test directory or test files found."
596
545
  echo " Without tests, Ralph relies on lint, type-checking, and PRD test steps."
@@ -630,6 +579,20 @@ auto_configure_project() {
630
579
  echo " Auto-updated commands.dev: $new_dev"
631
580
  updated=true
632
581
  fi
582
+ else
583
+ # No runner detected — convert bare 'python' to 'python3' for macOS compat
584
+ local current_dev
585
+ current_dev=$(jq -r '.commands.dev // ""' "$tmpfile" 2>/dev/null)
586
+ if [[ "$current_dev" == "python "* ]]; then
587
+ local new_dev="python3 ${current_dev#python }"
588
+ jq --arg dev "$new_dev" '.commands.dev = $dev' "$tmpfile" > "${tmpfile}.new" && mv "${tmpfile}.new" "$tmpfile"
589
+ echo " Auto-updated commands.dev: $new_dev (macOS compat)"
590
+ updated=true
591
+ fi
592
+ echo ""
593
+ echo " Tip: No Python package manager detected. Consider uv for faster deps & consistent environments:"
594
+ echo " curl -LsSf https://astral.sh/uv/install.sh | sh"
595
+ echo " https://docs.astral.sh/uv/"
633
596
  fi
634
597
  fi
635
598
 
@@ -742,13 +705,22 @@ Commands:
742
705
  prd <notes> Generate PRD interactively (quick mode)
743
706
  prd --file <file> Generate PRD from file
744
707
  run Run autonomous loop until all stories pass
745
- run --max <n> Run with max iterations (default: 20)
708
+ run --max <n> Limit to n iterations (no limit by default)
746
709
  run --fast Skip code review for faster iterations
710
+ run --quiet Suppress the live activity feed
747
711
  stop Stop loop after current story finishes
748
712
  skip Skip the current story, move to next
749
713
  status Show current feature and story status
750
714
  check Run verification checks only
751
715
  verify <story-id> Verify a specific story
716
+ uat Run UAT loop (team explores, tests, fixes bugs)
717
+ uat --plan-only Generate test plan without executing
718
+ uat --focus <id|cat> Run specific test case or category
719
+ uat --no-fix Write tests but don't fix app bugs
720
+ uat --review Force review of existing plan
721
+ chaos-agent Run Chaos Agent (adversarial red team testing)
722
+ chaos-agent --plan-only Generate chaos plan without executing
723
+ chaos-agent --no-fix Find vulnerabilities without fixing
752
724
  sign <pattern> [cat] Add a learned pattern (sign)
753
725
  signs List all learned patterns
754
726
  backup Backup detected databases to .backups/
@@ -766,6 +738,10 @@ Examples:
766
738
  npx agentic-loop prd "Add a contact form"
767
739
  npx agentic-loop run
768
740
  npx agentic-loop run --max 10
741
+ npx agentic-loop uat
742
+ npx agentic-loop uat --focus auth
743
+ npx agentic-loop chaos-agent
744
+ npx agentic-loop chaos-agent --no-fix
769
745
  npx agentic-loop status
770
746
  npx agentic-loop sign "Always use camelCase" frontend
771
747