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.
- package/.claude/commands/tour.md +11 -7
- package/.claude/commands/vibe-help.md +5 -2
- package/.claude/commands/vibe-list.md +17 -2
- package/.claude/skills/prd/SKILL.md +21 -6
- package/.claude/skills/setup-review/SKILL.md +56 -0
- package/.claude/skills/tour/SKILL.md +11 -7
- package/.claude/skills/vibe-help/SKILL.md +2 -1
- package/.claude/skills/vibe-list/SKILL.md +5 -2
- package/.pre-commit-hooks.yaml +8 -0
- package/README.md +4 -0
- package/bin/agentic-loop.sh +7 -0
- package/bin/ralph.sh +29 -0
- package/dist/checks/check-signs-secrets.d.ts +9 -0
- package/dist/checks/check-signs-secrets.d.ts.map +1 -0
- package/dist/checks/check-signs-secrets.js +57 -0
- package/dist/checks/check-signs-secrets.js.map +1 -0
- package/dist/checks/index.d.ts +2 -5
- package/dist/checks/index.d.ts.map +1 -1
- package/dist/checks/index.js +4 -9
- package/dist/checks/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/ralph/hooks/common.sh +47 -0
- package/ralph/hooks/warn-debug.sh +12 -26
- package/ralph/hooks/warn-empty-catch.sh +21 -34
- package/ralph/hooks/warn-secrets.sh +39 -52
- package/ralph/hooks/warn-urls.sh +25 -45
- package/ralph/init.sh +58 -82
- package/ralph/loop.sh +506 -53
- package/ralph/prd-check.sh +177 -236
- package/ralph/prd.sh +5 -2
- package/ralph/setup/quick-setup.sh +2 -16
- package/ralph/setup.sh +68 -80
- package/ralph/signs.sh +8 -0
- package/ralph/uat.sh +2015 -0
- package/ralph/utils.sh +198 -69
- package/ralph/verify/tests.sh +65 -10
- package/templates/PROMPT.md +10 -4
- package/templates/UAT-PROMPT.md +197 -0
- package/templates/config/elixir.json +0 -2
- package/templates/config/fastmcp.json +0 -2
- package/templates/config/fullstack.json +2 -4
- package/templates/config/go.json +0 -2
- package/templates/config/minimal.json +0 -2
- package/templates/config/node.json +0 -2
- package/templates/config/python.json +0 -2
- package/templates/config/rust.json +0 -2
- package/templates/prd-example.json +6 -8
package/ralph/utils.sh
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
# shellcheck shell=bash
|
|
3
3
|
# utils.sh - Shared utility functions for ralph
|
|
4
4
|
|
|
5
|
+
# Get utils.sh directory to locate package.json
|
|
6
|
+
_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
_PACKAGE_JSON="$_UTILS_DIR/../package.json"
|
|
8
|
+
|
|
9
|
+
# Version constant (read from package.json)
|
|
10
|
+
RALPH_VERSION="$(jq -r '.version' "$_PACKAGE_JSON" 2>/dev/null || echo "unknown")"
|
|
11
|
+
readonly RALPH_VERSION
|
|
12
|
+
|
|
13
|
+
# Loop protocol version (for stream-json activity feed)
|
|
14
|
+
RALPH_LOOP_VERSION="2"
|
|
15
|
+
readonly RALPH_LOOP_VERSION
|
|
16
|
+
|
|
5
17
|
# Constants - Output limits
|
|
6
18
|
readonly MAX_LOG_LINES=30
|
|
7
19
|
readonly MAX_PROGRESS_LINES=10
|
|
@@ -16,7 +28,6 @@ readonly MAX_SIGN_DEDUP_EXISTING=20
|
|
|
16
28
|
# Constants - Timeouts (centralized to avoid magic numbers)
|
|
17
29
|
readonly ITERATION_DELAY_SECONDS=0
|
|
18
30
|
readonly DEFAULT_TIMEOUT_SECONDS=600
|
|
19
|
-
readonly DEFAULT_MAX_ITERATIONS=20
|
|
20
31
|
readonly CODE_REVIEW_TIMEOUT_SECONDS=120
|
|
21
32
|
readonly BROWSER_TIMEOUT_SECONDS=60
|
|
22
33
|
readonly BROWSER_PAGE_TIMEOUT_MS=30000
|
|
@@ -140,38 +151,24 @@ get_config() {
|
|
|
140
151
|
echo "$default"
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
|
|
146
|
-
local
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
# Migrate deprecated config fields in-place
|
|
155
|
+
# Safe to call multiple times - only writes if changes are needed
|
|
156
|
+
_migrate_config() {
|
|
157
|
+
local config="$RALPH_DIR/config.json"
|
|
158
|
+
[[ ! -f "$config" ]] && return 0
|
|
159
|
+
|
|
160
|
+
# testUrlBase -> urls.frontend
|
|
161
|
+
local legacy_url
|
|
162
|
+
legacy_url=$(jq -r '.testUrlBase // empty' "$config" 2>/dev/null)
|
|
163
|
+
if [[ -n "$legacy_url" ]]; then
|
|
164
|
+
local current_frontend
|
|
165
|
+
current_frontend=$(jq -r '.urls.frontend // empty' "$config" 2>/dev/null)
|
|
166
|
+
if [[ -z "$current_frontend" ]]; then
|
|
167
|
+
jq --arg url "$legacy_url" '.urls.frontend = $url | del(.testUrlBase)' "$config" > "${config}.tmp" && mv "${config}.tmp" "$config"
|
|
168
|
+
else
|
|
169
|
+
jq 'del(.testUrlBase)' "$config" > "${config}.tmp" && mv "${config}.tmp" "$config"
|
|
157
170
|
fi
|
|
158
171
|
fi
|
|
159
|
-
echo "$default"
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
# Clear a failure log file
|
|
163
|
-
# Usage: clear_failure_log "lint" # clears last_lint_failure.log
|
|
164
|
-
clear_failure_log() {
|
|
165
|
-
local log_name="$1"
|
|
166
|
-
rm -f "$RALPH_DIR/last_${log_name}_failure.log"
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
# Append content to a failure log file
|
|
170
|
-
# Usage: log_failure "lint" "Error details here"
|
|
171
|
-
log_failure() {
|
|
172
|
-
local log_name="$1"
|
|
173
|
-
local content="$2"
|
|
174
|
-
echo "$content" >> "$RALPH_DIR/last_${log_name}_failure.log"
|
|
175
172
|
}
|
|
176
173
|
|
|
177
174
|
# Deep merge user config with project config
|
|
@@ -431,33 +428,17 @@ validate_command() {
|
|
|
431
428
|
|
|
432
429
|
# Block obviously dangerous patterns (defense-in-depth, not security boundary)
|
|
433
430
|
local dangerous_patterns=(
|
|
434
|
-
# Destructive file operations
|
|
435
431
|
'rm[[:space:]]+-rf[[:space:]]+/' # rm -rf /
|
|
436
432
|
'rm[[:space:]]+-rf[[:space:]]+~' # rm -rf ~ (home dir)
|
|
437
|
-
'rm[[:space:]]+-rf[[:space:]]+\*' # rm -rf *
|
|
438
|
-
'rm[[:space:]]+-rf[[:space:]]+\.\.' # rm -rf ..
|
|
439
433
|
'rm[[:space:]].*--no-preserve-root' # rm with --no-preserve-root
|
|
440
|
-
# Remote code execution
|
|
441
434
|
'curl.*\|.*bash' # curl | bash
|
|
442
435
|
'curl.*\|.*sh[[:space:]]*$' # curl | sh
|
|
443
436
|
'wget.*\|.*bash' # wget | bash
|
|
444
437
|
'wget.*\|.*sh[[:space:]]*$' # wget | sh
|
|
445
|
-
'curl.*>[[:space:]]*/tmp/.*&&.*bash' # curl > /tmp/x && bash
|
|
446
|
-
# Code injection
|
|
447
|
-
'\$\([^)]*eval' # $(eval ...)
|
|
448
438
|
'eval[[:space:]]+\$' # eval $var
|
|
449
|
-
'eval[[:space:]]+["\x27]' # eval "..." or eval '...'
|
|
450
|
-
# System damage
|
|
451
439
|
'>[[:space:]]*/dev/sd' # write to disk devices
|
|
452
440
|
'>[[:space:]]*/dev/nvme' # write to nvme devices
|
|
453
441
|
'mkfs\.' # format filesystems
|
|
454
|
-
'dd[[:space:]]+if=' # dd commands
|
|
455
|
-
':(){:|:&};:' # fork bomb
|
|
456
|
-
# Credential theft
|
|
457
|
-
'cat.*\.ssh/id_' # read SSH keys
|
|
458
|
-
'cat.*/etc/shadow' # read shadow file
|
|
459
|
-
'cat.*\.aws/credentials' # read AWS creds
|
|
460
|
-
'cat.*\.env' # read env files (often has secrets)
|
|
461
442
|
)
|
|
462
443
|
|
|
463
444
|
for pattern in "${dangerous_patterns[@]}"; do
|
|
@@ -471,25 +452,6 @@ validate_command() {
|
|
|
471
452
|
return 0
|
|
472
453
|
}
|
|
473
454
|
|
|
474
|
-
# Validate a URL is safe (http/https only, no internal IPs in production)
|
|
475
|
-
validate_url() { # public-api
|
|
476
|
-
local url="$1"
|
|
477
|
-
|
|
478
|
-
# Must start with http:// or https://
|
|
479
|
-
if [[ ! "$url" =~ ^https?:// ]]; then
|
|
480
|
-
print_error "Invalid URL scheme (must be http or https): $url"
|
|
481
|
-
return 1
|
|
482
|
-
fi
|
|
483
|
-
|
|
484
|
-
# Block file:// and other dangerous schemes
|
|
485
|
-
if [[ "$url" =~ ^(file|ftp|data|javascript): ]]; then
|
|
486
|
-
print_error "Dangerous URL scheme: $url"
|
|
487
|
-
return 1
|
|
488
|
-
fi
|
|
489
|
-
|
|
490
|
-
return 0
|
|
491
|
-
}
|
|
492
|
-
|
|
493
455
|
# Safely execute a command (validates first, uses bash -c instead of eval)
|
|
494
456
|
safe_exec() {
|
|
495
457
|
local cmd="$1"
|
|
@@ -710,6 +672,92 @@ fix_hardcoded_paths() {
|
|
|
710
672
|
fi
|
|
711
673
|
}
|
|
712
674
|
|
|
675
|
+
# Detect the type of project based on files present
|
|
676
|
+
# Returns one of: fullstack, rust, go, elixir, hugo, fastmcp, django, fastapi, python, node, minimal
|
|
677
|
+
detect_project_type() {
|
|
678
|
+
# Check for fullstack patterns first (more specific)
|
|
679
|
+
if [[ -d "frontend" && -d "core" ]]; then
|
|
680
|
+
echo "fullstack"; return
|
|
681
|
+
elif [[ -d "frontend" && -d "backend" ]]; then
|
|
682
|
+
echo "fullstack"; return
|
|
683
|
+
elif [[ -d "apps" ]]; then
|
|
684
|
+
echo "fullstack"; return
|
|
685
|
+
fi
|
|
686
|
+
|
|
687
|
+
# Single-language projects
|
|
688
|
+
if [[ -f "Cargo.toml" ]]; then echo "rust"; return; fi
|
|
689
|
+
if [[ -f "go.mod" ]]; then echo "go"; return; fi
|
|
690
|
+
if [[ -f "mix.exs" ]]; then echo "elixir"; return; fi
|
|
691
|
+
|
|
692
|
+
# Hugo (Go static site generator)
|
|
693
|
+
for _hc in "hugo.toml" "hugo.yaml" "hugo.json" "config.toml"; do
|
|
694
|
+
if [[ -f "$_hc" ]] && [[ -d "content" || -d "layouts" || -d "themes" ]]; then
|
|
695
|
+
echo "hugo"; return
|
|
696
|
+
fi
|
|
697
|
+
done
|
|
698
|
+
|
|
699
|
+
# Python framework variants (most specific first)
|
|
700
|
+
if [[ -f "pyproject.toml" ]]; then
|
|
701
|
+
if grep -qiE "(fastmcp|\"fastmcp\"|'fastmcp')" pyproject.toml 2>/dev/null; then
|
|
702
|
+
echo "fastmcp"; return
|
|
703
|
+
elif grep -qiE "(django|\"django\"|'django')" pyproject.toml 2>/dev/null || [[ -f "manage.py" ]]; then
|
|
704
|
+
echo "django"; return
|
|
705
|
+
elif grep -qiE "(fastapi|\"fastapi\"|'fastapi')" pyproject.toml 2>/dev/null; then
|
|
706
|
+
echo "fastapi"; return
|
|
707
|
+
else
|
|
708
|
+
echo "python"; return
|
|
709
|
+
fi
|
|
710
|
+
elif [[ -f "requirements.txt" || -f "setup.py" ]]; then
|
|
711
|
+
if [[ -f "requirements.txt" ]]; then
|
|
712
|
+
if grep -qi 'fastmcp' requirements.txt 2>/dev/null; then echo "fastmcp"; return; fi
|
|
713
|
+
if grep -qi 'django' requirements.txt 2>/dev/null || [[ -f "manage.py" ]]; then echo "django"; return; fi
|
|
714
|
+
if grep -qi 'fastapi' requirements.txt 2>/dev/null; then echo "fastapi"; return; fi
|
|
715
|
+
fi
|
|
716
|
+
echo "python"; return
|
|
717
|
+
elif [[ -f "manage.py" ]]; then
|
|
718
|
+
echo "django"; return
|
|
719
|
+
fi
|
|
720
|
+
|
|
721
|
+
if [[ -f "package.json" ]]; then echo "node"; return; fi
|
|
722
|
+
|
|
723
|
+
echo "minimal"
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
# Detect framework type for CLAUDE.md generation
|
|
727
|
+
# Returns one of: fastmcp, fastapi, django, react, or empty string
|
|
728
|
+
detect_framework_type() {
|
|
729
|
+
if [[ -f "pyproject.toml" ]]; then
|
|
730
|
+
if grep -qiE "(fastmcp|\"fastmcp\"|'fastmcp')" pyproject.toml 2>/dev/null; then
|
|
731
|
+
echo "fastmcp"; return
|
|
732
|
+
elif grep -qiE "(fastapi|\"fastapi\"|'fastapi')" pyproject.toml 2>/dev/null; then
|
|
733
|
+
echo "fastapi"; return
|
|
734
|
+
elif grep -qiE "(django|\"django\"|'django')" pyproject.toml 2>/dev/null || [[ -f "manage.py" ]]; then
|
|
735
|
+
echo "django"; return
|
|
736
|
+
fi
|
|
737
|
+
elif [[ -f "requirements.txt" ]]; then
|
|
738
|
+
if grep -qi 'fastmcp' requirements.txt 2>/dev/null; then echo "fastmcp"; return; fi
|
|
739
|
+
if grep -qi 'fastapi' requirements.txt 2>/dev/null; then echo "fastapi"; return; fi
|
|
740
|
+
if grep -qi 'django' requirements.txt 2>/dev/null || [[ -f "manage.py" ]]; then echo "django"; return; fi
|
|
741
|
+
elif [[ -f "manage.py" ]]; then
|
|
742
|
+
echo "django"; return
|
|
743
|
+
fi
|
|
744
|
+
|
|
745
|
+
# Check for React in frontend package.json
|
|
746
|
+
local pkg="package.json"
|
|
747
|
+
local fe_dir=""
|
|
748
|
+
[[ -d "frontend" ]] && fe_dir="frontend"
|
|
749
|
+
[[ -d "client" ]] && fe_dir="client"
|
|
750
|
+
[[ -d "web" ]] && fe_dir="web"
|
|
751
|
+
[[ -d "apps/web" ]] && fe_dir="apps/web"
|
|
752
|
+
[[ -n "$fe_dir" && -f "${fe_dir}/package.json" ]] && pkg="${fe_dir}/package.json"
|
|
753
|
+
|
|
754
|
+
if [[ -f "$pkg" ]] && grep -q '"react"' "$pkg" 2>/dev/null; then
|
|
755
|
+
echo "react"; return
|
|
756
|
+
fi
|
|
757
|
+
|
|
758
|
+
echo ""
|
|
759
|
+
}
|
|
760
|
+
|
|
713
761
|
# Detect Python runner (uv, poetry, pipenv, or plain python)
|
|
714
762
|
detect_python_runner() {
|
|
715
763
|
local search_dir="${1:-.}"
|
|
@@ -732,7 +780,7 @@ detect_python_runner() {
|
|
|
732
780
|
return 0
|
|
733
781
|
fi
|
|
734
782
|
|
|
735
|
-
# Default to plain command (
|
|
783
|
+
# Default to plain command (no runner detected)
|
|
736
784
|
echo ""
|
|
737
785
|
return 0
|
|
738
786
|
}
|
|
@@ -761,9 +809,11 @@ detect_migration_tool() {
|
|
|
761
809
|
return 0
|
|
762
810
|
fi
|
|
763
811
|
|
|
764
|
-
# Django
|
|
812
|
+
# Django (use python3 for macOS compatibility when no runner detected)
|
|
765
813
|
if [[ -f "$search_dir/manage.py" ]] && [[ -d "$search_dir" ]] && find "$search_dir" -type d -name "migrations" -print -quit | grep -q .; then
|
|
766
|
-
|
|
814
|
+
local python_cmd="python3"
|
|
815
|
+
[[ -n "$py_runner" ]] && python_cmd="python"
|
|
816
|
+
echo "cd $search_dir && ${py_runner}${py_runner:+ }${python_cmd} manage.py migrate"
|
|
767
817
|
return 0
|
|
768
818
|
fi
|
|
769
819
|
|
|
@@ -812,6 +862,85 @@ find_all_migration_tools() {
|
|
|
812
862
|
printf '%s\n' "${tools[@]}" | sort -u
|
|
813
863
|
}
|
|
814
864
|
|
|
865
|
+
# Validate batch assignments in a PRD file
|
|
866
|
+
# Returns 0 if valid (or no batches), 1 with error messages if invalid
|
|
867
|
+
# Checks: all-or-none batch field, dependency ordering, file overlap within batch
|
|
868
|
+
validate_batch_assignments() {
|
|
869
|
+
local prd_file="$1"
|
|
870
|
+
local errors=""
|
|
871
|
+
local error_count=0
|
|
872
|
+
|
|
873
|
+
# Count stories with and without batch
|
|
874
|
+
local total_stories with_batch without_batch
|
|
875
|
+
total_stories=$(jq '.stories | length' "$prd_file" 2>/dev/null || echo "0")
|
|
876
|
+
with_batch=$(jq '[.stories[] | select(.batch != null)] | length' "$prd_file" 2>/dev/null || echo "0")
|
|
877
|
+
without_batch=$((total_stories - with_batch))
|
|
878
|
+
|
|
879
|
+
# No batches at all — valid, nothing to check
|
|
880
|
+
[[ "$with_batch" -eq 0 ]] && return 0
|
|
881
|
+
|
|
882
|
+
# Some but not all stories have batch — error
|
|
883
|
+
if [[ "$without_batch" -gt 0 ]]; then
|
|
884
|
+
local missing_ids
|
|
885
|
+
missing_ids=$(jq -r '[.stories[] | select(.batch == null) | .id] | join(", ")' "$prd_file" 2>/dev/null)
|
|
886
|
+
errors+="batch field missing on: $missing_ids\n"
|
|
887
|
+
error_count=$((error_count + 1))
|
|
888
|
+
fi
|
|
889
|
+
|
|
890
|
+
# Check dependency ordering: no dependsOn pointing to same or later batch
|
|
891
|
+
local dep_violations
|
|
892
|
+
dep_violations=$(jq -r '
|
|
893
|
+
.stories as $all |
|
|
894
|
+
[.stories[] | . as $s |
|
|
895
|
+
($s.dependsOn // [])[] as $dep |
|
|
896
|
+
($all[] | select(.id == $dep)) as $dep_story |
|
|
897
|
+
select($dep_story.batch != null and $s.batch != null and $dep_story.batch >= $s.batch) |
|
|
898
|
+
"\($s.id) (batch \($s.batch)) depends on \($dep) (batch \($dep_story.batch))"
|
|
899
|
+
] | .[]' "$prd_file" 2>/dev/null)
|
|
900
|
+
|
|
901
|
+
if [[ -n "$dep_violations" ]]; then
|
|
902
|
+
while IFS= read -r violation; do
|
|
903
|
+
[[ -z "$violation" ]] && continue
|
|
904
|
+
errors+="$violation\n"
|
|
905
|
+
error_count=$((error_count + 1))
|
|
906
|
+
done <<< "$dep_violations"
|
|
907
|
+
fi
|
|
908
|
+
|
|
909
|
+
# Check file overlap within same batch (create/modify only)
|
|
910
|
+
local file_overlaps
|
|
911
|
+
file_overlaps=$(jq -r '
|
|
912
|
+
. as $root |
|
|
913
|
+
[$root.stories[] | select(.batch != null) | .batch] | unique | .[] as $b |
|
|
914
|
+
[$root.stories[] | select(.batch == $b)] |
|
|
915
|
+
if length < 2 then empty
|
|
916
|
+
else
|
|
917
|
+
. as $stories |
|
|
918
|
+
range(length) as $i | range($i+1; length) as $j |
|
|
919
|
+
$stories[$i] as $a | $stories[$j] as $b_story |
|
|
920
|
+
(($a.files.create // []) + ($a.files.modify // [])) as $files_a |
|
|
921
|
+
(($b_story.files.create // []) + ($b_story.files.modify // [])) as $files_b |
|
|
922
|
+
($files_a | map(select(. as $f | $files_b | any(. == $f)))) as $shared |
|
|
923
|
+
select(($shared | length) > 0) |
|
|
924
|
+
"batch \($b): \($a.id) and \($b_story.id) share files: \($shared | join(", "))"
|
|
925
|
+
end
|
|
926
|
+
' "$prd_file" 2>/dev/null)
|
|
927
|
+
|
|
928
|
+
if [[ -n "$file_overlaps" ]]; then
|
|
929
|
+
while IFS= read -r overlap; do
|
|
930
|
+
[[ -z "$overlap" ]] && continue
|
|
931
|
+
errors+="$overlap\n"
|
|
932
|
+
error_count=$((error_count + 1))
|
|
933
|
+
done <<< "$file_overlaps"
|
|
934
|
+
fi
|
|
935
|
+
|
|
936
|
+
if [[ $error_count -gt 0 ]]; then
|
|
937
|
+
echo -e "$errors"
|
|
938
|
+
return 1
|
|
939
|
+
fi
|
|
940
|
+
|
|
941
|
+
return 0
|
|
942
|
+
}
|
|
943
|
+
|
|
815
944
|
# Ensure database migrations are applied before verification
|
|
816
945
|
# Migration commands are idempotent - they no-op if nothing pending
|
|
817
946
|
run_migrations_if_needed() {
|
package/ralph/verify/tests.sh
CHANGED
|
@@ -195,7 +195,9 @@ run_unit_tests() {
|
|
|
195
195
|
if [[ -f "package.json" ]] && grep -q '"test"' package.json; then
|
|
196
196
|
test_cmd="npm test"
|
|
197
197
|
elif [[ -f "pytest.ini" ]] || [[ -f "pyproject.toml" ]]; then
|
|
198
|
-
|
|
198
|
+
local py_runner
|
|
199
|
+
py_runner=$(detect_python_runner ".")
|
|
200
|
+
test_cmd="${py_runner}${py_runner:+ }pytest"
|
|
199
201
|
elif [[ -f "Cargo.toml" ]]; then
|
|
200
202
|
test_cmd="cargo test"
|
|
201
203
|
elif [[ -f "go.mod" ]]; then
|
|
@@ -217,8 +219,35 @@ run_unit_tests() {
|
|
|
217
219
|
else
|
|
218
220
|
print_error "failed"
|
|
219
221
|
echo ""
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
|
|
223
|
+
# Check for missing tool (uv, poetry, pytest, etc.)
|
|
224
|
+
if grep -qiE "command not found|no such file or directory" "$log_file" 2>/dev/null; then
|
|
225
|
+
local missing_tool=""
|
|
226
|
+
if grep -qi "uv.*command not found\|uv:.*not found" "$log_file" 2>/dev/null; then
|
|
227
|
+
missing_tool="uv"
|
|
228
|
+
elif grep -qi "poetry.*command not found\|poetry:.*not found" "$log_file" 2>/dev/null; then
|
|
229
|
+
missing_tool="poetry"
|
|
230
|
+
elif grep -qi "pytest.*command not found\|pytest:.*not found" "$log_file" 2>/dev/null; then
|
|
231
|
+
missing_tool="pytest"
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
if [[ -n "$missing_tool" ]]; then
|
|
235
|
+
echo " '$missing_tool' is not installed."
|
|
236
|
+
echo ""
|
|
237
|
+
echo " Run setup to auto-detect and configure your project tools:"
|
|
238
|
+
echo " npx agentic-loop setup"
|
|
239
|
+
echo ""
|
|
240
|
+
echo " See Step 3 of the Getting Started guide:"
|
|
241
|
+
echo " docs/GETTING-STARTED.md"
|
|
242
|
+
else
|
|
243
|
+
echo " Output (last $MAX_LOG_LINES lines):"
|
|
244
|
+
tail -"$MAX_LOG_LINES" "$log_file" | sed 's/^/ /'
|
|
245
|
+
fi
|
|
246
|
+
else
|
|
247
|
+
echo " Output (last $MAX_LOG_LINES lines):"
|
|
248
|
+
tail -"$MAX_LOG_LINES" "$log_file" | sed 's/^/ /'
|
|
249
|
+
fi
|
|
250
|
+
|
|
222
251
|
cp "$log_file" "$RALPH_DIR/last_test_failure.log"
|
|
223
252
|
rm -f "$log_file"
|
|
224
253
|
return 1
|
|
@@ -228,9 +257,9 @@ run_unit_tests() {
|
|
|
228
257
|
# Expand config placeholders in a string
|
|
229
258
|
# Usage: _expand_config_vars "curl {config.urls.backend}/api"
|
|
230
259
|
# Expands any {config.X.Y} placeholder from .ralph/config.json via jq.
|
|
231
|
-
# Known placeholders
|
|
260
|
+
# Known placeholders with fallback paths:
|
|
232
261
|
# {config.urls.backend} -> .urls.backend // .api.baseUrl
|
|
233
|
-
# {config.urls.frontend} -> .urls.frontend
|
|
262
|
+
# {config.urls.frontend} -> .urls.frontend
|
|
234
263
|
_expand_config_vars() {
|
|
235
264
|
local input="$1"
|
|
236
265
|
local config="$RALPH_DIR/config.json"
|
|
@@ -249,7 +278,7 @@ _expand_config_vars() {
|
|
|
249
278
|
|
|
250
279
|
if [[ "$result" == *"{config.urls.frontend}"* ]]; then
|
|
251
280
|
local val
|
|
252
|
-
val=$(jq -r '.urls.frontend //
|
|
281
|
+
val=$(jq -r '.urls.frontend // empty' "$config" 2>/dev/null)
|
|
253
282
|
[[ -n "$val" ]] && result="${result//\{config.urls.frontend\}/$val}"
|
|
254
283
|
fi
|
|
255
284
|
|
|
@@ -276,6 +305,17 @@ _expand_config_vars() {
|
|
|
276
305
|
echo "$result"
|
|
277
306
|
}
|
|
278
307
|
|
|
308
|
+
# Check if a command string contains unresolved auth placeholder variables
|
|
309
|
+
# Returns 0 (true) if auth placeholders found, 1 (false) if clean
|
|
310
|
+
_has_auth_placeholder() {
|
|
311
|
+
local cmd="$1"
|
|
312
|
+
local auth_vars='TOKEN|API_KEY|JWT|AUTH_TOKEN|BEARER_TOKEN|ACCESS_TOKEN|SECRET|PASSWORD|CREDENTIALS|API_SECRET|AUTH'
|
|
313
|
+
[[ "$cmd" =~ \$($auth_vars)[^A-Za-z_] ]] && return 0
|
|
314
|
+
[[ "$cmd" =~ \$($auth_vars)$ ]] && return 0
|
|
315
|
+
[[ "$cmd" =~ \$\{($auth_vars)\} ]] && return 0
|
|
316
|
+
return 1
|
|
317
|
+
}
|
|
318
|
+
|
|
279
319
|
# Verify PRD acceptance criteria / test steps
|
|
280
320
|
verify_prd_criteria() {
|
|
281
321
|
local story="$1"
|
|
@@ -296,13 +336,21 @@ verify_prd_criteria() {
|
|
|
296
336
|
# Clear previous PRD failure log
|
|
297
337
|
rm -f "$prd_failure_log"
|
|
298
338
|
|
|
299
|
-
local step_index
|
|
339
|
+
local step_index=-1
|
|
300
340
|
while IFS= read -r step; do
|
|
301
341
|
[[ -z "$step" ]] && continue
|
|
302
342
|
|
|
303
343
|
# Expand config placeholders (e.g., {config.urls.backend})
|
|
304
344
|
local expanded_step
|
|
305
345
|
expanded_step=$(_expand_config_vars "$step")
|
|
346
|
+
((step_index++)) || true
|
|
347
|
+
|
|
348
|
+
# Skip steps with unresolved auth placeholders — don't fail, just warn
|
|
349
|
+
if _has_auth_placeholder "$expanded_step"; then
|
|
350
|
+
echo -n " $expanded_step... "
|
|
351
|
+
print_warning "skipped (uses auth placeholder variable — set a real value in env or config)"
|
|
352
|
+
continue
|
|
353
|
+
fi
|
|
306
354
|
|
|
307
355
|
echo -n " $expanded_step... "
|
|
308
356
|
|
|
@@ -311,8 +359,16 @@ verify_prd_criteria() {
|
|
|
311
359
|
else
|
|
312
360
|
print_error "failed"
|
|
313
361
|
echo ""
|
|
314
|
-
|
|
315
|
-
|
|
362
|
+
|
|
363
|
+
# Check for connection refused — give actionable guidance
|
|
364
|
+
if grep -qiE "connection refused|couldn.t connect|failed to connect" "$log_file" 2>/dev/null; then
|
|
365
|
+
local dev_cmd
|
|
366
|
+
dev_cmd=$(get_config '.commands.dev' "docker compose up -d")
|
|
367
|
+
echo " Server not running. Start it before running Ralph: $dev_cmd"
|
|
368
|
+
else
|
|
369
|
+
echo " Output:"
|
|
370
|
+
tail -"$MAX_OUTPUT_PREVIEW_LINES" "$log_file" | sed 's/^/ /'
|
|
371
|
+
fi
|
|
316
372
|
|
|
317
373
|
# Save failure details for retry context
|
|
318
374
|
{
|
|
@@ -325,7 +381,6 @@ verify_prd_criteria() {
|
|
|
325
381
|
|
|
326
382
|
failed=1
|
|
327
383
|
fi
|
|
328
|
-
((step_index++)) || true
|
|
329
384
|
done <<< "$test_steps"
|
|
330
385
|
|
|
331
386
|
rm -f "$log_file"
|
package/templates/PROMPT.md
CHANGED
|
@@ -158,6 +158,8 @@ After completing the story:
|
|
|
158
158
|
- No commented-out code
|
|
159
159
|
- All tests passing
|
|
160
160
|
|
|
161
|
+
4. **Then stop.** Your session should end here. Ralph will automatically run verification (lint, tests, build) and mark the story as passed or failed. You do not need to mark anything — just finish implementing and stop.
|
|
162
|
+
|
|
161
163
|
---
|
|
162
164
|
|
|
163
165
|
## Rules
|
|
@@ -166,18 +168,22 @@ After completing the story:
|
|
|
166
168
|
2. **Follow the PRD** - It has all the context you need
|
|
167
169
|
3. **Read before coding** - Understand existing patterns first
|
|
168
170
|
4. **Test frequently** - Run tests after each significant change
|
|
169
|
-
5. **
|
|
171
|
+
5. **Don't edit prd.json** - You can read it for context, but Ralph manages story state (passes, retryCount, skipped). After your session ends, Ralph runs verification and marks the story. Editing these fields has no effect — Ralph resets them before verifying.
|
|
170
172
|
6. **Don't give up** - If verification fails, fix and retry
|
|
171
173
|
|
|
172
174
|
---
|
|
173
175
|
|
|
174
176
|
## If Blocked
|
|
175
177
|
|
|
176
|
-
If you encounter a blocker you cannot resolve:
|
|
178
|
+
If you encounter a blocker you cannot resolve after 2-3 attempts:
|
|
179
|
+
|
|
177
180
|
1. Document the issue in `.ralph/progress.txt`
|
|
178
181
|
2. Note what you tried and why it didn't work
|
|
179
|
-
3.
|
|
180
|
-
|
|
182
|
+
3. Signal Ralph to skip this story:
|
|
183
|
+
```bash
|
|
184
|
+
echo "BLOCKED: [reason]" > .ralph/.blocked
|
|
185
|
+
```
|
|
186
|
+
4. Then stop. Ralph will stop the loop so the issue can be resolved.
|
|
181
187
|
|
|
182
188
|
---
|
|
183
189
|
|