agentic-loop 3.17.3 → 3.18.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/package.json +1 -1
- package/ralph/hooks/hooks-config.json +0 -12
- package/ralph/hooks/install.sh +0 -13
- package/ralph/loop.sh +17 -0
- package/ralph/prd-check.sh +337 -132
- package/ralph/setup.sh +29 -9
- package/ralph/utils.sh +21 -2
- package/ralph/verify/api.sh +1 -0
- package/ralph/hooks/protect-prd.sh +0 -30
package/package.json
CHANGED
|
@@ -2,18 +2,6 @@
|
|
|
2
2
|
"_comment": "Copy this 'hooks' section into your ~/.claude/settings.json or .claude/settings.json",
|
|
3
3
|
"_instructions": "Replace VIBE_PATH with the path to your agentic-loop installation",
|
|
4
4
|
"hooks": {
|
|
5
|
-
"PreToolUse": [
|
|
6
|
-
{
|
|
7
|
-
"matcher": "Edit|Write",
|
|
8
|
-
"hooks": [
|
|
9
|
-
{
|
|
10
|
-
"type": "command",
|
|
11
|
-
"command": "VIBE_PATH/ralph/hooks/protect-prd.sh",
|
|
12
|
-
"timeout": 5
|
|
13
|
-
}
|
|
14
|
-
]
|
|
15
|
-
}
|
|
16
|
-
],
|
|
17
5
|
"PostToolUse": [
|
|
18
6
|
{
|
|
19
7
|
"matcher": "Edit|Write",
|
package/ralph/hooks/install.sh
CHANGED
|
@@ -111,18 +111,6 @@ fi
|
|
|
111
111
|
# Build hooks config with actual path
|
|
112
112
|
HOOKS_CONFIG=$(cat <<EOF
|
|
113
113
|
{
|
|
114
|
-
"PreToolUse": [
|
|
115
|
-
{
|
|
116
|
-
"matcher": "Edit|Write",
|
|
117
|
-
"hooks": [
|
|
118
|
-
{
|
|
119
|
-
"type": "command",
|
|
120
|
-
"command": "$SCRIPT_DIR/protect-prd.sh",
|
|
121
|
-
"timeout": 5
|
|
122
|
-
}
|
|
123
|
-
]
|
|
124
|
-
}
|
|
125
|
-
],
|
|
126
114
|
"PostToolUse": [
|
|
127
115
|
{
|
|
128
116
|
"matcher": "Edit|Write",
|
|
@@ -195,7 +183,6 @@ echo "$MERGED" > "$SETTINGS_FILE"
|
|
|
195
183
|
echo -e "${GREEN}✓ Hooks installed successfully!${NC}"
|
|
196
184
|
echo ""
|
|
197
185
|
echo "Hooks enabled:"
|
|
198
|
-
echo " • protect-prd.sh - Blocks edits to prd.json"
|
|
199
186
|
echo " • warn-debug.sh - Warns about console.log/debugger"
|
|
200
187
|
echo " • warn-secrets.sh - Warns about hardcoded secrets/API keys"
|
|
201
188
|
echo " • warn-urls.sh - Warns about hardcoded localhost URLs"
|
package/ralph/loop.sh
CHANGED
|
@@ -88,6 +88,23 @@ preflight_checks() {
|
|
|
88
88
|
fi
|
|
89
89
|
done
|
|
90
90
|
|
|
91
|
+
# Check for timeout utility (critical for session enforcement)
|
|
92
|
+
printf " Timeout utility... "
|
|
93
|
+
if command -v timeout &>/dev/null; then
|
|
94
|
+
print_success "ok (timeout)"
|
|
95
|
+
elif command -v gtimeout &>/dev/null; then
|
|
96
|
+
print_success "ok (gtimeout)"
|
|
97
|
+
else
|
|
98
|
+
print_warning "not found (using bash fallback)"
|
|
99
|
+
echo " Session timeouts use a bash fallback. For better reliability:"
|
|
100
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
101
|
+
echo " brew install coreutils"
|
|
102
|
+
else
|
|
103
|
+
echo " Install GNU coreutils"
|
|
104
|
+
fi
|
|
105
|
+
((warnings++))
|
|
106
|
+
fi
|
|
107
|
+
|
|
91
108
|
echo ""
|
|
92
109
|
if [[ $warnings -gt 0 ]]; then
|
|
93
110
|
print_warning "$warnings pre-loop warning(s) - loop may fail on connectivity issues"
|
package/ralph/prd-check.sh
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
# - Has prerequisites array with DB reset command
|
|
51
51
|
# - Prevents infinite retries on schema mismatch errors
|
|
52
52
|
#
|
|
53
|
-
# CUSTOM CHECKS (.ralph/checks/prd/
|
|
53
|
+
# CUSTOM CHECKS (.ralph/checks/prd/):
|
|
54
54
|
# - User-provided scripts that receive story JSON on stdin
|
|
55
55
|
# - Output issue descriptions to stdout (one per line)
|
|
56
56
|
# - Excluded from auto-fix (reported for manual review)
|
|
@@ -63,12 +63,22 @@
|
|
|
63
63
|
# ============================================================================
|
|
64
64
|
# AUTO-FIX
|
|
65
65
|
# ============================================================================
|
|
66
|
-
# When issues are found,
|
|
66
|
+
# When issues are found, a two-tier fix runs automatically:
|
|
67
67
|
#
|
|
68
|
-
# 1
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
68
|
+
# Tier 1 — Mechanical fixes (instant, no LLM):
|
|
69
|
+
# - Missing mcp on frontend → ["playwright", "devtools"]
|
|
70
|
+
# - Bare pytest → prefixed with detected runner (uv/poetry/pipenv)
|
|
71
|
+
# - Missing camelCase note → standard text appended to .notes
|
|
72
|
+
# - Missing migration prerequisites → template prerequisite array
|
|
73
|
+
# - Server-only testSteps → offline fallback appended
|
|
74
|
+
#
|
|
75
|
+
# Tier 2 — Parallel Claude subagents (one per story, concurrent):
|
|
76
|
+
# - For issues needing creative input (apiContract, prose testSteps, etc.)
|
|
77
|
+
# - Each story gets a small prompt with just its JSON + specific issues
|
|
78
|
+
# - All stories fix in parallel (wall-clock = time for 1 story)
|
|
79
|
+
# - Results merged back via update_json; failures left unchanged
|
|
80
|
+
#
|
|
81
|
+
# Timestamped backup preserved before any modifications.
|
|
72
82
|
#
|
|
73
83
|
# If Claude is unavailable or fix fails, loop continues with warnings.
|
|
74
84
|
#
|
|
@@ -81,7 +91,6 @@
|
|
|
81
91
|
# .api.baseUrl - API base URL (enables API config validation)
|
|
82
92
|
# .api.healthEndpoint - Health check path (default: /health, empty to disable)
|
|
83
93
|
# .ralph/checks/prd/check-* - Project-level custom checks (per-story)
|
|
84
|
-
# ~/.config/ralph/checks/prd/ - User-global custom checks (per-story)
|
|
85
94
|
# .checks.custom.<name> - Enable/disable individual custom checks
|
|
86
95
|
#
|
|
87
96
|
# ============================================================================
|
|
@@ -220,9 +229,13 @@ validate_prd() {
|
|
|
220
229
|
echo ""
|
|
221
230
|
fi
|
|
222
231
|
|
|
223
|
-
# Validate API smoke test configuration (skip in fast/cached mode)
|
|
232
|
+
# Validate API smoke test configuration in background (skip in fast/cached mode)
|
|
233
|
+
# Capture output to a temp file to avoid garbled terminal output
|
|
234
|
+
local api_check_pid="" api_check_output=""
|
|
224
235
|
if [[ "$dry_run" != "true" ]]; then
|
|
225
|
-
|
|
236
|
+
api_check_output=$(create_temp_file ".api-check.out")
|
|
237
|
+
_validate_api_config "$config" > "$api_check_output" 2>&1 &
|
|
238
|
+
api_check_pid=$!
|
|
226
239
|
fi
|
|
227
240
|
|
|
228
241
|
# Replace hardcoded paths with config placeholders
|
|
@@ -232,6 +245,12 @@ validate_prd() {
|
|
|
232
245
|
# dry_run flag — when "true", skip auto-fix
|
|
233
246
|
_validate_and_fix_stories "$prd_file" "$dry_run" || return 1
|
|
234
247
|
|
|
248
|
+
# Wait for background API health check and print its output
|
|
249
|
+
if [[ -n "$api_check_pid" ]]; then
|
|
250
|
+
wait "$api_check_pid" 2>/dev/null
|
|
251
|
+
[[ -s "$api_check_output" ]] && cat "$api_check_output"
|
|
252
|
+
fi
|
|
253
|
+
|
|
235
254
|
return 0
|
|
236
255
|
}
|
|
237
256
|
|
|
@@ -504,8 +523,8 @@ _validate_and_fix_stories() {
|
|
|
504
523
|
# Snapshot built-in issues before custom checks append
|
|
505
524
|
local builtin_story_issues="$story_issues"
|
|
506
525
|
|
|
507
|
-
# Check 8: User-defined custom checks (.ralph/checks/prd/
|
|
508
|
-
if [[ -d ".ralph/checks/prd" ]]
|
|
526
|
+
# Check 8: User-defined custom checks (.ralph/checks/prd/)
|
|
527
|
+
if [[ -d ".ralph/checks/prd" ]]; then
|
|
509
528
|
local story_json
|
|
510
529
|
story_json=$(jq --arg id "$story_id" '.stories[] | select(.id==$id)' "$prd_file")
|
|
511
530
|
local custom_output
|
|
@@ -555,12 +574,32 @@ _validate_and_fix_stories() {
|
|
|
555
574
|
return 0
|
|
556
575
|
fi
|
|
557
576
|
|
|
558
|
-
#
|
|
559
|
-
|
|
560
|
-
|
|
577
|
+
# Create backup before any modifications
|
|
578
|
+
local backup_file="${prd_file}.$(date +%Y%m%d-%H%M%S).bak"
|
|
579
|
+
cp "$prd_file" "$backup_file"
|
|
580
|
+
|
|
581
|
+
# Tier 1: Instant mechanical fixes (no LLM needed)
|
|
582
|
+
_apply_mechanical_fixes "$prd_file"
|
|
583
|
+
|
|
584
|
+
# Re-check what's still broken after mechanical fixes
|
|
585
|
+
# validate_stories_quick returns "ID: issue, ID: issue, ..." on one line
|
|
586
|
+
# Group into one line per story for _fix_stories_parallel
|
|
587
|
+
local remaining_raw
|
|
588
|
+
remaining_raw=$(validate_stories_quick "$prd_file")
|
|
589
|
+
local remaining_grouped=""
|
|
590
|
+
[[ -n "$remaining_raw" ]] && remaining_grouped=$(_group_issues_by_story "$remaining_raw")
|
|
591
|
+
|
|
592
|
+
if [[ -n "$remaining_grouped" ]]; then
|
|
593
|
+
# Tier 2: Parallel Claude subagents for creative fixes
|
|
594
|
+
if command -v claude &>/dev/null; then
|
|
595
|
+
_fix_stories_parallel "$prd_file" "$remaining_grouped" "$backup_file"
|
|
596
|
+
else
|
|
597
|
+
print_warning "Claude CLI not found - mechanical fixes applied, but some stories need manual review"
|
|
598
|
+
echo " Backup at: $backup_file"
|
|
599
|
+
return 0
|
|
600
|
+
fi
|
|
561
601
|
else
|
|
562
|
-
|
|
563
|
-
return 1
|
|
602
|
+
print_success "All issues resolved with mechanical fixes (backup at $backup_file)"
|
|
564
603
|
fi
|
|
565
604
|
else
|
|
566
605
|
print_success "Test coverage looks good"
|
|
@@ -579,49 +618,188 @@ _run_custom_prd_checks() {
|
|
|
579
618
|
local custom_issues=""
|
|
580
619
|
local custom_log="$RALPH_DIR/last_custom_check.log"
|
|
581
620
|
|
|
582
|
-
local
|
|
583
|
-
[[ -d "
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
621
|
+
local check_dir=".ralph/checks/prd"
|
|
622
|
+
[[ ! -d "$check_dir" ]] && return 0
|
|
623
|
+
|
|
624
|
+
for check_script in "$check_dir"/check-*; do
|
|
625
|
+
[[ ! -f "$check_script" || ! -x "$check_script" ]] && continue
|
|
626
|
+
|
|
627
|
+
local check_key
|
|
628
|
+
check_key=$(basename "$check_script")
|
|
629
|
+
check_key="${check_key%.*}"
|
|
630
|
+
# Read directly instead of get_config — jq's // operator treats false as falsy
|
|
631
|
+
local enabled="true"
|
|
632
|
+
if [[ -f "$RALPH_DIR/config.json" ]]; then
|
|
633
|
+
local raw
|
|
634
|
+
raw=$(jq -r --arg key "$check_key" '.checks.custom[$key]' "$RALPH_DIR/config.json" 2>/dev/null)
|
|
635
|
+
[[ -n "$raw" && "$raw" != "null" ]] && enabled="$raw"
|
|
636
|
+
fi
|
|
637
|
+
[[ "$enabled" == "false" ]] && continue
|
|
638
|
+
|
|
639
|
+
# Run check — capture stdout for issues, stderr to log for debugging
|
|
640
|
+
local output=""
|
|
641
|
+
if ! output=$(echo "$story_json" | run_with_timeout 30 "$check_script" "$story_id" "$prd_file" 2>>"$custom_log"); then
|
|
642
|
+
# Script failed to execute — warn, don't silently swallow
|
|
643
|
+
print_warning "Custom check '$check_key' failed for story $story_id (see .ralph/last_custom_check.log)"
|
|
644
|
+
fi
|
|
645
|
+
|
|
646
|
+
if [[ -n "$output" ]]; then
|
|
647
|
+
while IFS= read -r line; do
|
|
648
|
+
[[ -n "$line" ]] && custom_issues+="${line}, "
|
|
649
|
+
done <<< "$output"
|
|
650
|
+
fi
|
|
651
|
+
done
|
|
652
|
+
|
|
653
|
+
echo "$custom_issues"
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
# Group flat "ID: issue, ID: issue, ..." string into one line per story
|
|
657
|
+
# Input: "S1: missing curl tests, S1: missing apiContract, S2: missing testUrl, "
|
|
658
|
+
# Output: "S1: missing curl tests, missing apiContract\nS2: missing testUrl"
|
|
659
|
+
_group_issues_by_story() {
|
|
660
|
+
local raw="$1"
|
|
661
|
+
# Split on ", " boundaries that precede a story ID pattern (word: )
|
|
662
|
+
# Use awk to accumulate issues per story ID
|
|
663
|
+
echo "$raw" | tr ',' '\n' | sed 's/^ *//' | while IFS= read -r entry; do
|
|
664
|
+
[[ -z "$entry" ]] && continue
|
|
665
|
+
if [[ "$entry" =~ ^([A-Za-z0-9._-]+):\ (.+) ]]; then
|
|
666
|
+
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"
|
|
667
|
+
fi
|
|
668
|
+
done | awk -F'\t' '{
|
|
669
|
+
if (seen[$1]) {
|
|
670
|
+
issues[$1] = issues[$1] ", " $2
|
|
671
|
+
} else {
|
|
672
|
+
seen[$1] = 1
|
|
673
|
+
issues[$1] = $2
|
|
674
|
+
order[++n] = $1
|
|
675
|
+
}
|
|
676
|
+
} END {
|
|
677
|
+
for (i = 1; i <= n; i++) {
|
|
678
|
+
print order[i] ": " issues[order[i]]
|
|
679
|
+
}
|
|
680
|
+
}'
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
# Apply instant mechanical fixes using jq (no LLM needed)
|
|
684
|
+
# Fixes: missing mcp, bare pytest, missing camelCase note, missing migration prerequisites,
|
|
685
|
+
# server-only testSteps
|
|
686
|
+
_apply_mechanical_fixes() {
|
|
687
|
+
local prd_file="$1"
|
|
688
|
+
local fixed=0
|
|
689
|
+
|
|
690
|
+
# Detect Python runner once for bare pytest fixes
|
|
691
|
+
local py_runner
|
|
692
|
+
py_runner=$(detect_python_runner ".")
|
|
693
|
+
|
|
694
|
+
local story_ids
|
|
695
|
+
story_ids=$(jq -r '.stories[] | select(.passes != true) | .id' "$prd_file" 2>/dev/null)
|
|
696
|
+
|
|
697
|
+
while IFS= read -r story_id; do
|
|
698
|
+
[[ -z "$story_id" ]] && continue
|
|
699
|
+
|
|
700
|
+
local story_type
|
|
701
|
+
story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
|
|
702
|
+
|
|
703
|
+
# Fix: Frontend missing mcp → set to ["playwright", "devtools"]
|
|
704
|
+
if [[ "$story_type" == "frontend" ]]; then
|
|
705
|
+
local mcp_len
|
|
706
|
+
mcp_len=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .mcp // [] | length' "$prd_file")
|
|
707
|
+
if [[ "$mcp_len" == "0" ]]; then
|
|
708
|
+
update_json "$prd_file" --arg id "$story_id" \
|
|
709
|
+
'(.stories[] | select(.id==$id) | .mcp) = ["playwright", "devtools"]' && fixed=$((fixed + 1))
|
|
600
710
|
fi
|
|
601
|
-
|
|
711
|
+
fi
|
|
602
712
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
713
|
+
# Fix: Bare pytest → prefix with detected runner
|
|
714
|
+
if [[ -n "$py_runner" ]]; then
|
|
715
|
+
local test_steps_raw
|
|
716
|
+
test_steps_raw=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join("\n")' "$prd_file")
|
|
717
|
+
if echo "$test_steps_raw" | grep -qE '(^|[; ])pytest ' && ! echo "$test_steps_raw" | grep -qE "(uv run|poetry run|pipenv run) pytest"; then
|
|
718
|
+
update_json "$prd_file" --arg id "$story_id" --arg runner "$py_runner" \
|
|
719
|
+
'(.stories[] | select(.id==$id) | .testSteps) |= [.[]? | gsub("(?<pre>^|[; ])pytest "; "\(.pre)\($runner) pytest ")]' && fixed=$((fixed + 1))
|
|
608
720
|
fi
|
|
721
|
+
fi
|
|
609
722
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
723
|
+
# Fix: Frontend/general API consumer missing camelCase note
|
|
724
|
+
if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
|
|
725
|
+
local story_desc
|
|
726
|
+
story_desc=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.title + " " + (.acceptanceCriteria // [] | join(" ")) + " " + (.notes // ""))' "$prd_file")
|
|
727
|
+
if echo "$story_desc" | grep -qiE "(api|fetch|axios|endpoint|backend|response)"; then
|
|
728
|
+
local story_notes
|
|
729
|
+
story_notes=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .notes // ""' "$prd_file")
|
|
730
|
+
if ! echo "$story_notes" | grep -qiE "(camelCase|snake_case|naming)"; then
|
|
731
|
+
local camel_note="Transform API responses from snake_case to camelCase. Create typed interfaces with camelCase properties."
|
|
732
|
+
if [[ -z "$story_notes" ]]; then
|
|
733
|
+
update_json "$prd_file" --arg id "$story_id" --arg note "$camel_note" \
|
|
734
|
+
'(.stories[] | select(.id==$id) | .notes) = $note' && fixed=$((fixed + 1))
|
|
735
|
+
else
|
|
736
|
+
update_json "$prd_file" --arg id "$story_id" --arg note "$camel_note" \
|
|
737
|
+
'(.stories[] | select(.id==$id) | .notes) += (" " + $note)' && fixed=$((fixed + 1))
|
|
738
|
+
fi
|
|
739
|
+
fi
|
|
614
740
|
fi
|
|
615
|
-
|
|
616
|
-
done
|
|
741
|
+
fi
|
|
617
742
|
|
|
618
|
-
|
|
743
|
+
# Fix: Migration story missing prerequisites
|
|
744
|
+
local story_files
|
|
745
|
+
story_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.files.create // []) + (.files.modify // []) | join(" ")' "$prd_file")
|
|
746
|
+
if echo "$story_files" | grep -qiE "(alembic/versions|migrations/|\.migration\.|models\.py|models/|schema\.)"; then
|
|
747
|
+
local has_prereq
|
|
748
|
+
has_prereq=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .prerequisites // [] | length' "$prd_file")
|
|
749
|
+
if [[ "$has_prereq" == "0" ]]; then
|
|
750
|
+
update_json "$prd_file" --arg id "$story_id" \
|
|
751
|
+
'(.stories[] | select(.id==$id) | .prerequisites) = [{"name": "Reset test DB", "command": "npm run db:reset:test", "when": "schema changes"}]' && fixed=$((fixed + 1))
|
|
752
|
+
fi
|
|
753
|
+
fi
|
|
754
|
+
|
|
755
|
+
# Fix: All testSteps are server-dependent → append offline test step
|
|
756
|
+
local test_steps
|
|
757
|
+
test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
|
|
758
|
+
if [[ -n "$test_steps" ]]; then
|
|
759
|
+
local has_offline=false has_server=false
|
|
760
|
+
local step_list
|
|
761
|
+
step_list=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
|
|
762
|
+
while IFS= read -r single_step; do
|
|
763
|
+
[[ -z "$single_step" ]] && continue
|
|
764
|
+
if echo "$single_step" | grep -qE "^(curl |wget |http )"; then
|
|
765
|
+
has_server=true
|
|
766
|
+
else
|
|
767
|
+
has_offline=true
|
|
768
|
+
fi
|
|
769
|
+
done <<< "$step_list"
|
|
770
|
+
|
|
771
|
+
if [[ "$has_server" == "true" && "$has_offline" == "false" ]]; then
|
|
772
|
+
# Pick an offline step based on story type and project tooling
|
|
773
|
+
local offline_step="npx tsc --noEmit"
|
|
774
|
+
if [[ "$story_type" == "backend" ]]; then
|
|
775
|
+
if [[ -n "$py_runner" ]]; then
|
|
776
|
+
offline_step="$py_runner pytest tests/unit/"
|
|
777
|
+
elif [[ -f "go.mod" ]]; then
|
|
778
|
+
offline_step="go test ./..."
|
|
779
|
+
else
|
|
780
|
+
offline_step="npm test"
|
|
781
|
+
fi
|
|
782
|
+
fi
|
|
783
|
+
update_json "$prd_file" --arg id "$story_id" --arg step "$offline_step" \
|
|
784
|
+
'(.stories[] | select(.id==$id) | .testSteps) += [$step]' && fixed=$((fixed + 1))
|
|
785
|
+
fi
|
|
786
|
+
fi
|
|
787
|
+
|
|
788
|
+
done <<< "$story_ids"
|
|
789
|
+
|
|
790
|
+
if [[ $fixed -gt 0 ]]; then
|
|
791
|
+
echo " Applied $fixed mechanical fixes (no LLM needed)"
|
|
792
|
+
fi
|
|
793
|
+
|
|
794
|
+
return 0
|
|
619
795
|
}
|
|
620
796
|
|
|
621
|
-
#
|
|
622
|
-
|
|
797
|
+
# Fix stories with remaining issues using parallel Claude subagents (one per story)
|
|
798
|
+
# $1: prd_file $2: newline-separated "story_id: issues" lines $3: backup file path
|
|
799
|
+
_fix_stories_parallel() {
|
|
623
800
|
local prd_file="$1"
|
|
624
801
|
local issues="$2"
|
|
802
|
+
local backup_file="$3"
|
|
625
803
|
|
|
626
804
|
# Read config values for context
|
|
627
805
|
local config_file="$RALPH_DIR/config.json"
|
|
@@ -631,102 +809,129 @@ _fix_stories_with_claude() {
|
|
|
631
809
|
frontend_url=$(jq -r '.urls.frontend // .playwright.baseUrl // "http://localhost:3000"' "$config_file" 2>/dev/null)
|
|
632
810
|
fi
|
|
633
811
|
|
|
634
|
-
|
|
812
|
+
# Parse issues into per-story fix jobs
|
|
813
|
+
local pids=()
|
|
814
|
+
local story_ids_to_fix=()
|
|
815
|
+
local output_files=()
|
|
816
|
+
|
|
817
|
+
while IFS= read -r line; do
|
|
818
|
+
[[ -z "$line" ]] && continue
|
|
819
|
+
local sid="${line%%:*}"
|
|
820
|
+
local story_issues="${line#*: }"
|
|
821
|
+
[[ -z "$sid" || -z "$story_issues" ]] && continue
|
|
822
|
+
|
|
823
|
+
# Extract this story's JSON
|
|
824
|
+
local story_json
|
|
825
|
+
story_json=$(jq --arg id "$sid" '.stories[] | select(.id==$id)' "$prd_file" 2>/dev/null)
|
|
826
|
+
[[ -z "$story_json" ]] && continue
|
|
827
|
+
|
|
828
|
+
# Build a small per-story prompt
|
|
829
|
+
local prompt_file
|
|
830
|
+
prompt_file=$(create_temp_file ".prompt.txt")
|
|
831
|
+
local output_file
|
|
832
|
+
output_file=$(create_temp_file ".fix.json")
|
|
833
|
+
|
|
834
|
+
cat > "$prompt_file" <<PROMPT_EOF
|
|
835
|
+
Fix this story's issues. Output ONLY the fixed story JSON object (not the full PRD).
|
|
836
|
+
|
|
837
|
+
STORY JSON:
|
|
838
|
+
$story_json
|
|
635
839
|
|
|
636
|
-
|
|
637
|
-
$
|
|
840
|
+
ISSUES TO FIX:
|
|
841
|
+
$story_issues
|
|
638
842
|
|
|
639
|
-
CONFIG VALUES
|
|
843
|
+
CONFIG VALUES:
|
|
640
844
|
- Backend URL: $backend_url (use as {config.urls.backend} in testSteps)
|
|
641
845
|
- Frontend URL: $frontend_url (use as {config.urls.frontend} in testUrl)
|
|
642
846
|
|
|
643
847
|
RULES:
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
This prevents wasted retries when the server isn't running.
|
|
668
|
-
|
|
669
|
-
CURRENT PRD:
|
|
670
|
-
$(cat "$prd_file")
|
|
671
|
-
|
|
672
|
-
Output ONLY the fixed JSON, no explanation. Start with { and end with }."
|
|
673
|
-
|
|
674
|
-
local raw_response
|
|
675
|
-
raw_response=$(echo "$fix_prompt" | run_with_timeout "$CODE_REVIEW_TIMEOUT_SECONDS" claude -p 2>/dev/null)
|
|
676
|
-
|
|
677
|
-
# Extract JSON from response (Claude often wraps in markdown code fences)
|
|
678
|
-
local fixed_prd
|
|
679
|
-
# First strip markdown code fences if present
|
|
680
|
-
fixed_prd=$(echo "$raw_response" | sed 's/^```json//; s/^```$//' | sed -n '/^[[:space:]]*{/,/^[[:space:]]*}[[:space:]]*$/p' | head -1000)
|
|
681
|
-
|
|
682
|
-
# If sed extraction failed, try removing fences and using raw
|
|
683
|
-
if [[ -z "$fixed_prd" ]]; then
|
|
684
|
-
fixed_prd=$(echo "$raw_response" | sed 's/^```json//; s/^```//; s/```$//')
|
|
848
|
+
- Backend stories MUST have testSteps with curl commands hitting real endpoints
|
|
849
|
+
Example: curl -s -X POST {config.urls.backend}/api/users -d '...' | jq -e '.id'
|
|
850
|
+
- Backend stories MUST have apiContract with endpoint, request, response
|
|
851
|
+
- Frontend stories MUST have testUrl set to {config.urls.frontend}/[page-path]
|
|
852
|
+
- Frontend stories MUST have contextFiles array
|
|
853
|
+
- Auth stories MUST have security acceptanceCriteria (bcrypt, rate limiting)
|
|
854
|
+
- List endpoints MUST have pagination acceptanceCriteria (?page=N&limit=N)
|
|
855
|
+
- Stories with only curl testSteps MUST also have an offline test step (npm test, tsc --noEmit, pytest)
|
|
856
|
+
- Keep ALL existing fields. Only add/fix what's missing.
|
|
857
|
+
|
|
858
|
+
Output ONLY the fixed story JSON object. Start with { and end with }.
|
|
859
|
+
PROMPT_EOF
|
|
860
|
+
|
|
861
|
+
# Background a Claude call for this story
|
|
862
|
+
( run_with_timeout 60 claude -p < "$prompt_file" > "$output_file" 2>/dev/null ) &
|
|
863
|
+
pids+=($!)
|
|
864
|
+
story_ids_to_fix+=("$sid")
|
|
865
|
+
output_files+=("$output_file")
|
|
866
|
+
done <<< "$issues"
|
|
867
|
+
|
|
868
|
+
local job_count=${#pids[@]}
|
|
869
|
+
if [[ $job_count -eq 0 ]]; then
|
|
870
|
+
return 0
|
|
685
871
|
fi
|
|
686
872
|
|
|
687
|
-
|
|
688
|
-
local backup_file="${prd_file}.$(date +%Y%m%d-%H%M%S).bak"
|
|
689
|
-
cp "$prd_file" "$backup_file"
|
|
690
|
-
|
|
691
|
-
# Get original story count for validation
|
|
692
|
-
local orig_story_count
|
|
693
|
-
orig_story_count=$(jq '.stories | length' "$prd_file" 2>/dev/null || echo "0")
|
|
694
|
-
|
|
695
|
-
# Validate the response is valid JSON with required structure
|
|
696
|
-
if echo "$fixed_prd" | jq -e '.stories' >/dev/null 2>&1; then
|
|
697
|
-
# Critical: Check story count is preserved (not just that .stories exists)
|
|
698
|
-
local new_story_count
|
|
699
|
-
new_story_count=$(echo "$fixed_prd" | jq '.stories | length' 2>/dev/null || echo "0")
|
|
700
|
-
if [[ "$new_story_count" -lt "$orig_story_count" ]]; then
|
|
701
|
-
print_warning "Fixed PRD has fewer stories ($orig_story_count -> $new_story_count) - keeping original"
|
|
702
|
-
echo " Backup preserved at: $backup_file"
|
|
703
|
-
return 0
|
|
704
|
-
fi
|
|
873
|
+
echo " Fixing $job_count stories in parallel..."
|
|
705
874
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
875
|
+
# Wait for all background jobs
|
|
876
|
+
for pid in "${pids[@]}"; do
|
|
877
|
+
wait "$pid" 2>/dev/null
|
|
878
|
+
done
|
|
879
|
+
|
|
880
|
+
# Merge results back into PRD
|
|
881
|
+
local merged=0 failed=0
|
|
882
|
+
for i in "${!story_ids_to_fix[@]}"; do
|
|
883
|
+
local sid="${story_ids_to_fix[$i]}"
|
|
884
|
+
local output_file="${output_files[$i]}"
|
|
885
|
+
|
|
886
|
+
[[ ! -s "$output_file" ]] && { failed=$((failed + 1)); continue; }
|
|
887
|
+
|
|
888
|
+
# Extract JSON from response (strip markdown fences if present)
|
|
889
|
+
local raw_response
|
|
890
|
+
raw_response=$(cat "$output_file")
|
|
891
|
+
local fixed_story
|
|
892
|
+
fixed_story=$(echo "$raw_response" | sed 's/^```json//; s/^```$//' | sed -n '/^[[:space:]]*{/,/^[[:space:]]*}[[:space:]]*$/p')
|
|
893
|
+
|
|
894
|
+
if [[ -z "$fixed_story" ]]; then
|
|
895
|
+
fixed_story=$(echo "$raw_response" | sed 's/^```json//; s/^```//; s/```$//')
|
|
714
896
|
fi
|
|
715
897
|
|
|
716
|
-
#
|
|
717
|
-
|
|
718
|
-
|
|
898
|
+
# Validate it's a valid JSON object with an id field matching this story
|
|
899
|
+
local response_id
|
|
900
|
+
response_id=$(echo "$fixed_story" | jq -r '.id // empty' 2>/dev/null)
|
|
901
|
+
if [[ "$response_id" != "$sid" ]]; then
|
|
902
|
+
# Try to salvage: if valid JSON, force the correct id
|
|
903
|
+
if echo "$fixed_story" | jq -e '.' >/dev/null 2>&1; then
|
|
904
|
+
fixed_story=$(echo "$fixed_story" | jq --arg id "$sid" '.id = $id')
|
|
905
|
+
response_id="$sid"
|
|
906
|
+
else
|
|
907
|
+
failed=$((failed + 1))
|
|
908
|
+
continue
|
|
909
|
+
fi
|
|
910
|
+
fi
|
|
719
911
|
|
|
720
|
-
#
|
|
721
|
-
local
|
|
722
|
-
|
|
723
|
-
if
|
|
724
|
-
|
|
912
|
+
# Merge fixed story back into PRD using update_json
|
|
913
|
+
local fixed_story_escaped
|
|
914
|
+
fixed_story_escaped=$(echo "$fixed_story" | jq -c '.')
|
|
915
|
+
if update_json "$prd_file" --arg id "$sid" --argjson fixed "$fixed_story_escaped" \
|
|
916
|
+
'(.stories[] | select(.id==$id)) = $fixed'; then
|
|
917
|
+
merged=$((merged + 1))
|
|
918
|
+
else
|
|
919
|
+
failed=$((failed + 1))
|
|
725
920
|
fi
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
921
|
+
done
|
|
922
|
+
|
|
923
|
+
if [[ $merged -gt 0 ]]; then
|
|
924
|
+
print_success "Fixed $merged stories with Claude (backup at $backup_file)"
|
|
925
|
+
fi
|
|
926
|
+
if [[ $failed -gt 0 ]]; then
|
|
927
|
+
print_warning "$failed stories could not be auto-fixed — review with /prd"
|
|
928
|
+
fi
|
|
929
|
+
|
|
930
|
+
# Final validation pass
|
|
931
|
+
local remaining_issues
|
|
932
|
+
remaining_issues=$(validate_stories_quick "$prd_file")
|
|
933
|
+
if [[ -n "$remaining_issues" ]]; then
|
|
934
|
+
echo " Some stories may still need manual review"
|
|
730
935
|
fi
|
|
731
936
|
}
|
|
732
937
|
|
package/ralph/setup.sh
CHANGED
|
@@ -92,6 +92,33 @@ ralph_setup() {
|
|
|
92
92
|
local pkg_root
|
|
93
93
|
pkg_root="$(cd "$RALPH_LIB/.." && pwd)"
|
|
94
94
|
|
|
95
|
+
# Install timeout utility if missing (critical for session enforcement)
|
|
96
|
+
if ! command -v timeout &>/dev/null && ! command -v gtimeout &>/dev/null; then
|
|
97
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
98
|
+
if command -v brew &>/dev/null; then
|
|
99
|
+
echo ""
|
|
100
|
+
echo " Installing coreutils (provides gtimeout for session enforcement)..."
|
|
101
|
+
if brew install coreutils 2>/dev/null; then
|
|
102
|
+
print_success " coreutils installed"
|
|
103
|
+
else
|
|
104
|
+
print_warning " Failed to install coreutils — session timeouts will use a bash fallback"
|
|
105
|
+
echo " Try manually: brew install coreutils"
|
|
106
|
+
fi
|
|
107
|
+
echo ""
|
|
108
|
+
else
|
|
109
|
+
echo ""
|
|
110
|
+
print_warning "No timeout utility found — session timeouts will use a bash fallback"
|
|
111
|
+
echo " Install Homebrew (https://brew.sh), then: brew install coreutils"
|
|
112
|
+
echo ""
|
|
113
|
+
fi
|
|
114
|
+
else
|
|
115
|
+
echo ""
|
|
116
|
+
print_warning "No timeout utility found — session timeouts will use a bash fallback"
|
|
117
|
+
echo " Install GNU coreutils for reliable timeout enforcement"
|
|
118
|
+
echo ""
|
|
119
|
+
fi
|
|
120
|
+
fi
|
|
121
|
+
|
|
95
122
|
# Run all setup steps
|
|
96
123
|
setup_ralph_dir "$pkg_root"
|
|
97
124
|
setup_custom_checks
|
|
@@ -353,8 +380,7 @@ setup_claude_hooks() {
|
|
|
353
380
|
}
|
|
354
381
|
|
|
355
382
|
# Resolve each hook path (project hooks take priority over global)
|
|
356
|
-
local
|
|
357
|
-
protect_prd=$(_resolve_hook "protect-prd.sh")
|
|
383
|
+
local warn_debug warn_secrets warn_urls warn_empty_catch log_tools inject_context save_learnings
|
|
358
384
|
warn_debug=$(_resolve_hook "warn-debug.sh")
|
|
359
385
|
warn_secrets=$(_resolve_hook "warn-secrets.sh")
|
|
360
386
|
warn_urls=$(_resolve_hook "warn-urls.sh")
|
|
@@ -364,11 +390,7 @@ setup_claude_hooks() {
|
|
|
364
390
|
save_learnings=$(_resolve_hook "save-learnings.sh")
|
|
365
391
|
|
|
366
392
|
# Build hooks arrays using jq for proper JSON
|
|
367
|
-
local
|
|
368
|
-
|
|
369
|
-
# PreToolUse: protect-prd on Edit|Write
|
|
370
|
-
pre_tool_hooks="[]"
|
|
371
|
-
[[ -n "$protect_prd" ]] && pre_tool_hooks=$(jq -n --arg cmd "$protect_prd" '[{"type": "command", "command": $cmd, "timeout": 5}]')
|
|
393
|
+
local post_edit_hooks post_all_hooks session_start_hooks stop_hooks
|
|
372
394
|
|
|
373
395
|
# PostToolUse: warn-* hooks on Edit|Write
|
|
374
396
|
post_edit_hooks="[]"
|
|
@@ -391,13 +413,11 @@ setup_claude_hooks() {
|
|
|
391
413
|
# Build the complete hooks config
|
|
392
414
|
local hooks_config
|
|
393
415
|
hooks_config=$(jq -n \
|
|
394
|
-
--argjson pre_tool "$pre_tool_hooks" \
|
|
395
416
|
--argjson post_edit "$post_edit_hooks" \
|
|
396
417
|
--argjson post_all "$post_all_hooks" \
|
|
397
418
|
--argjson session_start "$session_start_hooks" \
|
|
398
419
|
--argjson stop "$stop_hooks" \
|
|
399
420
|
'{
|
|
400
|
-
"PreToolUse": [{"matcher": "Edit|Write", "hooks": $pre_tool}],
|
|
401
421
|
"PostToolUse": [
|
|
402
422
|
{"matcher": "Edit|Write", "hooks": $post_edit},
|
|
403
423
|
{"matcher": "*", "hooks": $post_all}
|
package/ralph/utils.sh
CHANGED
|
@@ -316,8 +316,27 @@ run_with_timeout() {
|
|
|
316
316
|
elif command -v gtimeout &>/dev/null; then
|
|
317
317
|
gtimeout "$seconds" "$@"
|
|
318
318
|
else
|
|
319
|
-
#
|
|
320
|
-
|
|
319
|
+
# Bash-native fallback: background the command, kill after timeout.
|
|
320
|
+
# Capture stdin to a temp file so the backgrounded process can read it
|
|
321
|
+
# (backgrounded commands lose access to the pipeline's stdin).
|
|
322
|
+
local stdin_file
|
|
323
|
+
stdin_file=$(mktemp)
|
|
324
|
+
cat > "$stdin_file"
|
|
325
|
+
"$@" < "$stdin_file" &
|
|
326
|
+
local cmd_pid=$!
|
|
327
|
+
rm -f "$stdin_file"
|
|
328
|
+
( sleep "$seconds" && kill -TERM "$cmd_pid" 2>/dev/null ) &
|
|
329
|
+
local watchdog_pid=$!
|
|
330
|
+
wait "$cmd_pid" 2>/dev/null
|
|
331
|
+
local exit_code=$?
|
|
332
|
+
kill "$watchdog_pid" 2>/dev/null
|
|
333
|
+
wait "$watchdog_pid" 2>/dev/null
|
|
334
|
+
# If the process received SIGTERM, return 124 (same as GNU timeout).
|
|
335
|
+
# Note: this cannot distinguish our watchdog from other SIGTERM sources.
|
|
336
|
+
if [[ $exit_code -eq 143 ]]; then # 143 = 128 + 15 (SIGTERM)
|
|
337
|
+
return 124
|
|
338
|
+
fi
|
|
339
|
+
return "$exit_code"
|
|
321
340
|
fi
|
|
322
341
|
}
|
|
323
342
|
|
package/ralph/verify/api.sh
CHANGED
|
@@ -140,6 +140,7 @@ run_frontend_smoke_test() {
|
|
|
140
140
|
# 3. Story-specific testUrl from PRD
|
|
141
141
|
local test_url
|
|
142
142
|
test_url=$(jq -r --arg id "$story" '.stories[] | select(.id==$id) | .testUrl // empty' "$RALPH_DIR/prd.json" 2>/dev/null)
|
|
143
|
+
test_url=$(_expand_config_vars "$test_url")
|
|
143
144
|
if [[ -n "$test_url" ]]; then
|
|
144
145
|
# testUrl can be full URL or just path
|
|
145
146
|
if [[ "$test_url" =~ ^https?:// ]]; then
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# protect-prd.sh - Protect prd.json from marking stories as passed
|
|
3
|
-
# Hook: PreToolUse matcher: "Edit|Write"
|
|
4
|
-
#
|
|
5
|
-
# Allows: Most edits to prd.json (adding fields, fixing test steps, etc.)
|
|
6
|
-
# Blocks: Edits that mark stories as "passes": true (Ralph handles this)
|
|
7
|
-
|
|
8
|
-
set -euo pipefail
|
|
9
|
-
|
|
10
|
-
INPUT=$(cat)
|
|
11
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
|
|
12
|
-
|
|
13
|
-
# Check if editing prd.json
|
|
14
|
-
if [[ "$FILE_PATH" == *"prd.json"* ]]; then
|
|
15
|
-
# Get the new content being written
|
|
16
|
-
NEW_STRING=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""')
|
|
17
|
-
|
|
18
|
-
# Block if trying to set passes to true (Ralph handles story completion)
|
|
19
|
-
if echo "$NEW_STRING" | grep -qE '"passes"\s*:\s*true'; then
|
|
20
|
-
echo "BLOCKED: Cannot mark stories as passed. Ralph handles this after verification." >&2
|
|
21
|
-
exit 2 # Exit code 2 = blocking error
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
# Allow all other prd.json edits (adding mcp, constraints, fixing testSteps, etc.)
|
|
25
|
-
echo '{"continue": true}'
|
|
26
|
-
exit 0
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
# Allow all other edits
|
|
30
|
-
echo '{"continue": true}'
|