agentic-loop 3.4.2 → 3.4.4
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/README.md +2 -0
- package/package.json +1 -1
- package/ralph/ci.sh +1 -0
- package/ralph/loop.sh +3 -3
- package/ralph/test.sh +12 -4
- package/ralph/verify/tests.sh +100 -37
- package/templates/PROMPT.md +7 -2
- package/templates/github/workflows/nightly.yml +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,8 @@ Ralph reads the PRD and implements each story autonomously. It spawns Claude, ru
|
|
|
24
24
|
- `/vibe-check`, `/review` - On-demand quality and security checks
|
|
25
25
|
- Pre-commit hooks - Block secrets, hardcoded URLs, debug statements
|
|
26
26
|
- Claude Code hooks - Real-time warnings while coding
|
|
27
|
+
- GitHub Actions CI/CD - Fast PR checks + comprehensive nightly tests
|
|
28
|
+
- Test file enforcement - Fails if new code lacks corresponding tests
|
|
27
29
|
|
|
28
30
|
---
|
|
29
31
|
|
package/package.json
CHANGED
package/ralph/ci.sh
CHANGED
package/ralph/loop.sh
CHANGED
|
@@ -325,12 +325,12 @@ run_loop() {
|
|
|
325
325
|
timeout_seconds=$(get_config '.maxSessionSeconds' "$DEFAULT_TIMEOUT_SECONDS")
|
|
326
326
|
|
|
327
327
|
# Run Claude - first story gets fresh session, subsequent continue the session
|
|
328
|
-
local
|
|
328
|
+
local -a claude_args=(-p --dangerously-skip-permissions --verbose)
|
|
329
329
|
if [[ "$session_started" == "true" ]]; then
|
|
330
|
-
|
|
330
|
+
claude_args=(--continue "${claude_args[@]}")
|
|
331
331
|
fi
|
|
332
332
|
|
|
333
|
-
if ! cat "$prompt_file" | run_with_timeout "$timeout_seconds" $
|
|
333
|
+
if ! cat "$prompt_file" | run_with_timeout "$timeout_seconds" claude "${claude_args[@]}"; then
|
|
334
334
|
print_warning "Claude session ended (timeout or error)"
|
|
335
335
|
log_progress "$story" "TIMEOUT" "Claude session ended after ${timeout_seconds}s"
|
|
336
336
|
rm -f "$prompt_file"
|
package/ralph/test.sh
CHANGED
|
@@ -14,8 +14,6 @@ ralph_test() {
|
|
|
14
14
|
echo ""
|
|
15
15
|
|
|
16
16
|
local failed=0
|
|
17
|
-
local total=0
|
|
18
|
-
local passed=0
|
|
19
17
|
|
|
20
18
|
case "$mode" in
|
|
21
19
|
all)
|
|
@@ -76,11 +74,18 @@ run_full_test_suite() {
|
|
|
76
74
|
echo "Running: $test_cmd"
|
|
77
75
|
echo ""
|
|
78
76
|
|
|
79
|
-
|
|
77
|
+
local log_file
|
|
78
|
+
log_file=$(mktemp)
|
|
79
|
+
|
|
80
|
+
if safe_exec "$test_cmd" "$log_file"; then
|
|
80
81
|
print_success "Unit tests passed"
|
|
82
|
+
rm -f "$log_file"
|
|
81
83
|
return 0
|
|
82
84
|
else
|
|
83
85
|
print_error "Unit tests failed"
|
|
86
|
+
echo ""
|
|
87
|
+
tail -50 "$log_file"
|
|
88
|
+
rm -f "$log_file"
|
|
84
89
|
return 1
|
|
85
90
|
fi
|
|
86
91
|
}
|
|
@@ -131,13 +136,16 @@ run_all_prd_tests() {
|
|
|
131
136
|
|
|
132
137
|
echo -n " $step... "
|
|
133
138
|
|
|
134
|
-
|
|
139
|
+
local step_log
|
|
140
|
+
step_log=$(mktemp)
|
|
141
|
+
if safe_exec "$step" "$step_log"; then
|
|
135
142
|
print_success "passed"
|
|
136
143
|
((passed++))
|
|
137
144
|
else
|
|
138
145
|
print_error "failed"
|
|
139
146
|
((failed++))
|
|
140
147
|
fi
|
|
148
|
+
rm -f "$step_log"
|
|
141
149
|
done <<< "$test_steps"
|
|
142
150
|
|
|
143
151
|
echo ""
|
package/ralph/verify/tests.sh
CHANGED
|
@@ -4,26 +4,81 @@
|
|
|
4
4
|
|
|
5
5
|
# Check that new/modified source files have corresponding test files
|
|
6
6
|
# This catches the case where Claude writes code but forgets tests
|
|
7
|
+
# Config: .checks.requireTests = true|false (default: false)
|
|
7
8
|
verify_test_files_exist() {
|
|
8
9
|
local story_type="${RALPH_STORY_TYPE:-general}"
|
|
9
10
|
|
|
11
|
+
# Check if this check is enabled in config
|
|
12
|
+
local require_tests
|
|
13
|
+
require_tests=$(get_config '.checks.requireTests' "false")
|
|
14
|
+
if [[ "$require_tests" != "true" ]]; then
|
|
15
|
+
return 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
10
18
|
# Skip for frontend stories (handled differently with .test.tsx pattern)
|
|
11
19
|
[[ "$story_type" == "frontend" ]] && return 0
|
|
12
20
|
|
|
13
21
|
echo -n " Test files exist for new code... "
|
|
14
22
|
|
|
23
|
+
local missing_tests=()
|
|
24
|
+
local checked=0
|
|
25
|
+
|
|
26
|
+
# Check Python files if this is a Python project
|
|
27
|
+
if [[ -f "pyproject.toml" ]] || [[ -f "requirements.txt" ]] || [[ -f "setup.py" ]]; then
|
|
28
|
+
_check_python_test_files missing_tests checked
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Check Go files if this is a Go project
|
|
32
|
+
if [[ -f "go.mod" ]]; then
|
|
33
|
+
_check_go_test_files missing_tests checked
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# If nothing to check, skip
|
|
37
|
+
if [[ $checked -eq 0 ]]; then
|
|
38
|
+
print_success "skipped (no new source files)"
|
|
39
|
+
return 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if [[ ${#missing_tests[@]} -eq 0 ]]; then
|
|
43
|
+
print_success "passed ($checked files checked)"
|
|
44
|
+
return 0
|
|
45
|
+
else
|
|
46
|
+
print_error "missing tests"
|
|
47
|
+
echo ""
|
|
48
|
+
echo " The following files need test files:"
|
|
49
|
+
for file in "${missing_tests[@]}"; do
|
|
50
|
+
echo " $file"
|
|
51
|
+
done
|
|
52
|
+
echo ""
|
|
53
|
+
echo " Create test files for new code before completing the story."
|
|
54
|
+
echo " To disable this check: set .checks.requireTests = false in config.json"
|
|
55
|
+
|
|
56
|
+
# Save for failure context
|
|
57
|
+
{
|
|
58
|
+
echo "Missing test files for new code:"
|
|
59
|
+
for file in "${missing_tests[@]}"; do
|
|
60
|
+
echo " $file"
|
|
61
|
+
done
|
|
62
|
+
} > "$RALPH_DIR/last_test_existence_failure.log"
|
|
63
|
+
|
|
64
|
+
return 1
|
|
65
|
+
fi
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Helper: Check Python files have corresponding test files
|
|
69
|
+
# Usage: _check_python_test_files <missing_array_name> <checked_var_name>
|
|
70
|
+
_check_python_test_files() {
|
|
71
|
+
local -n _missing=$1
|
|
72
|
+
local -n _checked=$2
|
|
73
|
+
|
|
15
74
|
# Get list of modified Python files (excluding tests themselves)
|
|
16
75
|
local modified_files
|
|
17
76
|
modified_files=$(git diff --name-only HEAD~1 2>/dev/null | grep '\.py$' | grep -v 'test_' | grep -v '_test\.py' | grep -v '/tests/' || true)
|
|
18
77
|
|
|
19
|
-
|
|
20
|
-
if [[ -z "$modified_files" ]]; then
|
|
21
|
-
print_success "skipped (no new Python files)"
|
|
22
|
-
return 0
|
|
23
|
-
fi
|
|
78
|
+
[[ -z "$modified_files" ]] && return 0
|
|
24
79
|
|
|
25
|
-
local
|
|
26
|
-
|
|
80
|
+
local backend_dir
|
|
81
|
+
backend_dir=$(get_config '.directories.backend' "")
|
|
27
82
|
|
|
28
83
|
while IFS= read -r src_file; do
|
|
29
84
|
[[ -z "$src_file" ]] && continue
|
|
@@ -35,11 +90,11 @@ verify_test_files_exist() {
|
|
|
35
90
|
[[ "$src_file" == *"/alembic/"* ]] && continue
|
|
36
91
|
[[ "$src_file" == *"config"* ]] && continue
|
|
37
92
|
[[ "$src_file" == *"settings"* ]] && continue
|
|
93
|
+
[[ "$src_file" == *"conftest"* ]] && continue
|
|
38
94
|
|
|
39
|
-
((
|
|
95
|
+
((_checked++))
|
|
40
96
|
|
|
41
|
-
|
|
42
|
-
local base_name dir_name test_file
|
|
97
|
+
local base_name dir_name
|
|
43
98
|
base_name=$(basename "$src_file" .py)
|
|
44
99
|
dir_name=$(dirname "$src_file")
|
|
45
100
|
|
|
@@ -52,9 +107,6 @@ verify_test_files_exist() {
|
|
|
52
107
|
"tests/${base_name}_test.py"
|
|
53
108
|
)
|
|
54
109
|
|
|
55
|
-
# Check for backend dir patterns
|
|
56
|
-
local backend_dir
|
|
57
|
-
backend_dir=$(get_config '.directories.backend' "")
|
|
58
110
|
if [[ -n "$backend_dir" ]]; then
|
|
59
111
|
possible_tests+=(
|
|
60
112
|
"$backend_dir/tests/test_${base_name}.py"
|
|
@@ -71,35 +123,46 @@ verify_test_files_exist() {
|
|
|
71
123
|
done
|
|
72
124
|
|
|
73
125
|
if [[ "$found" == "false" ]]; then
|
|
74
|
-
|
|
126
|
+
_missing+=("$src_file → test_${base_name}.py")
|
|
75
127
|
fi
|
|
76
128
|
done <<< "$modified_files"
|
|
129
|
+
}
|
|
77
130
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
echo ""
|
|
84
|
-
echo " The following files need test files:"
|
|
85
|
-
for file in "${missing_tests[@]}"; do
|
|
86
|
-
local base_name
|
|
87
|
-
base_name=$(basename "$file" .py)
|
|
88
|
-
echo " $file → test_${base_name}.py"
|
|
89
|
-
done
|
|
90
|
-
echo ""
|
|
91
|
-
echo " Create test files for new code before completing the story."
|
|
131
|
+
# Helper: Check Go files have corresponding test files
|
|
132
|
+
# Usage: _check_go_test_files <missing_array_name> <checked_var_name>
|
|
133
|
+
_check_go_test_files() {
|
|
134
|
+
local -n _missing=$1
|
|
135
|
+
local -n _checked=$2
|
|
92
136
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
for file in "${missing_tests[@]}"; do
|
|
97
|
-
echo " $file"
|
|
98
|
-
done
|
|
99
|
-
} > "$RALPH_DIR/last_test_existence_failure.log"
|
|
137
|
+
# Get list of modified Go files (excluding tests themselves)
|
|
138
|
+
local modified_files
|
|
139
|
+
modified_files=$(git diff --name-only HEAD~1 2>/dev/null | grep '\.go$' | grep -v '_test\.go$' || true)
|
|
100
140
|
|
|
101
|
-
|
|
102
|
-
|
|
141
|
+
[[ -z "$modified_files" ]] && return 0
|
|
142
|
+
|
|
143
|
+
while IFS= read -r src_file; do
|
|
144
|
+
[[ -z "$src_file" ]] && continue
|
|
145
|
+
[[ ! -f "$src_file" ]] && continue
|
|
146
|
+
|
|
147
|
+
# Skip generated files, main.go, etc.
|
|
148
|
+
[[ "$src_file" == *"_generated.go" ]] && continue
|
|
149
|
+
[[ "$src_file" == *"/vendor/"* ]] && continue
|
|
150
|
+
[[ "$(basename "$src_file")" == "main.go" ]] && continue
|
|
151
|
+
[[ "$(basename "$src_file")" == "doc.go" ]] && continue
|
|
152
|
+
|
|
153
|
+
((_checked++))
|
|
154
|
+
|
|
155
|
+
local base_name dir_name
|
|
156
|
+
base_name=$(basename "$src_file" .go)
|
|
157
|
+
dir_name=$(dirname "$src_file")
|
|
158
|
+
|
|
159
|
+
# Go convention: foo.go -> foo_test.go in same directory
|
|
160
|
+
local test_file="$dir_name/${base_name}_test.go"
|
|
161
|
+
|
|
162
|
+
if [[ ! -f "$test_file" ]]; then
|
|
163
|
+
_missing+=("$src_file → ${base_name}_test.go")
|
|
164
|
+
fi
|
|
165
|
+
done <<< "$modified_files"
|
|
103
166
|
}
|
|
104
167
|
|
|
105
168
|
# Run unit tests
|
package/templates/PROMPT.md
CHANGED
|
@@ -24,19 +24,24 @@ For each story, you must:
|
|
|
24
24
|
|
|
25
25
|
**Every new code file MUST have a corresponding test file.**
|
|
26
26
|
|
|
27
|
-
For **
|
|
27
|
+
For **Python** backend stories:
|
|
28
28
|
- New file `foo.py` → create `tests/test_foo.py`
|
|
29
29
|
- Test each public function/method
|
|
30
30
|
- Test error cases (invalid input, missing data, API failures)
|
|
31
31
|
- Test edge cases (empty lists, None values, boundary conditions)
|
|
32
32
|
- Use pytest fixtures for database/API mocking
|
|
33
33
|
|
|
34
|
+
For **Go** projects:
|
|
35
|
+
- New file `foo.go` → create `foo_test.go` in same directory
|
|
36
|
+
- Use table-driven tests for multiple cases
|
|
37
|
+
- Test error paths and edge cases
|
|
38
|
+
|
|
34
39
|
For **frontend** stories (TypeScript/React):
|
|
35
40
|
- New component `Foo.tsx` → create `Foo.test.tsx`
|
|
36
41
|
- Test rendering, user interactions, error states
|
|
37
42
|
- Test loading states and empty states
|
|
38
43
|
|
|
39
|
-
**Do NOT skip tests.** If
|
|
44
|
+
**Do NOT skip tests.** If test enforcement is enabled, verification will fail without tests.
|
|
40
45
|
|
|
41
46
|
### 3. Verify It Actually Works
|
|
42
47
|
|