agentic-loop 3.16.3 → 3.17.1
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/code-check.sh +21 -2
- package/ralph/loop.sh +334 -32
- package/ralph/prd-check.sh +57 -4
- package/ralph/signs.sh +9 -2
- package/ralph/test.sh +6 -2
- package/ralph/utils.sh +12 -6
- package/ralph/verify/tests.sh +30 -30
package/package.json
CHANGED
package/ralph/code-check.sh
CHANGED
|
@@ -100,6 +100,7 @@ run_verification() {
|
|
|
100
100
|
export RALPH_STORY_TYPE="$story_type"
|
|
101
101
|
|
|
102
102
|
local failed=0
|
|
103
|
+
local failed_step=""
|
|
103
104
|
|
|
104
105
|
# ========================================
|
|
105
106
|
# STEP 1: Run lint checks
|
|
@@ -107,6 +108,7 @@ run_verification() {
|
|
|
107
108
|
echo " [1/5] Running lint checks..."
|
|
108
109
|
if ! run_configured_checks "$story_type"; then
|
|
109
110
|
failed=1
|
|
111
|
+
failed_step="lint"
|
|
110
112
|
fi
|
|
111
113
|
|
|
112
114
|
# ========================================
|
|
@@ -118,8 +120,10 @@ run_verification() {
|
|
|
118
120
|
# First check that test files exist for new code
|
|
119
121
|
if ! verify_test_files_exist; then
|
|
120
122
|
failed=1
|
|
123
|
+
failed_step="test files missing"
|
|
121
124
|
elif ! run_unit_tests; then
|
|
122
125
|
failed=1
|
|
126
|
+
failed_step="unit tests"
|
|
123
127
|
fi
|
|
124
128
|
fi
|
|
125
129
|
|
|
@@ -131,6 +135,7 @@ run_verification() {
|
|
|
131
135
|
echo " [3/5] Running PRD test steps..."
|
|
132
136
|
if ! verify_prd_criteria "$story"; then
|
|
133
137
|
failed=1
|
|
138
|
+
failed_step="PRD test steps"
|
|
134
139
|
fi
|
|
135
140
|
fi
|
|
136
141
|
|
|
@@ -140,6 +145,7 @@ run_verification() {
|
|
|
140
145
|
if [[ $failed -eq 0 ]]; then
|
|
141
146
|
if ! run_api_smoke_test "$story"; then
|
|
142
147
|
failed=1
|
|
148
|
+
failed_step="API smoke test"
|
|
143
149
|
fi
|
|
144
150
|
fi
|
|
145
151
|
|
|
@@ -149,6 +155,7 @@ run_verification() {
|
|
|
149
155
|
if [[ $failed -eq 0 ]]; then
|
|
150
156
|
if ! run_frontend_smoke_test "$story"; then
|
|
151
157
|
failed=1
|
|
158
|
+
failed_step="frontend smoke test"
|
|
152
159
|
fi
|
|
153
160
|
fi
|
|
154
161
|
|
|
@@ -160,7 +167,7 @@ run_verification() {
|
|
|
160
167
|
print_success "=== All verification passed ==="
|
|
161
168
|
return 0
|
|
162
169
|
else
|
|
163
|
-
print_error "=== Verification failed ==="
|
|
170
|
+
print_error "=== Verification failed at: $failed_step ==="
|
|
164
171
|
save_failure_context "$story"
|
|
165
172
|
return 1
|
|
166
173
|
fi
|
|
@@ -194,8 +201,20 @@ save_failure_context() {
|
|
|
194
201
|
echo ""
|
|
195
202
|
echo "=== Attempt $attempt failed for $story ==="
|
|
196
203
|
echo ""
|
|
204
|
+
# Include migration failure if present (verification may not have run)
|
|
205
|
+
if [[ -f "$RALPH_DIR/last_migration_failure.log" ]]; then
|
|
206
|
+
echo "--- Migration Error ---"
|
|
207
|
+
tail -30 "$RALPH_DIR/last_migration_failure.log"
|
|
208
|
+
echo ""
|
|
209
|
+
fi
|
|
210
|
+
# Include pre-commit failure if present
|
|
211
|
+
if [[ -f "$RALPH_DIR/last_precommit_failure.log" ]]; then
|
|
212
|
+
echo "--- Pre-commit Error ---"
|
|
213
|
+
tail -30 "$RALPH_DIR/last_precommit_failure.log"
|
|
214
|
+
echo ""
|
|
215
|
+
fi
|
|
216
|
+
# Include verification output (lint, tests, API, etc.)
|
|
197
217
|
if [[ -f "$RALPH_DIR/last_verification.log" ]]; then
|
|
198
|
-
# Shorter excerpt per attempt since we're accumulating
|
|
199
218
|
tail -50 "$RALPH_DIR/last_verification.log"
|
|
200
219
|
fi
|
|
201
220
|
echo ""
|
package/ralph/loop.sh
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
# shellcheck shell=bash
|
|
3
3
|
# loop.sh - The autonomous development loop
|
|
4
4
|
|
|
5
|
-
# Pre-
|
|
5
|
+
# Pre-loop checks to catch common issues before wasting iterations
|
|
6
6
|
preflight_checks() {
|
|
7
|
-
echo "
|
|
7
|
+
echo ""
|
|
8
|
+
echo " ┌──────────────────────────────────┐"
|
|
9
|
+
echo " │ ✅ Pre-Loop Checks │"
|
|
10
|
+
echo " └──────────────────────────────────┘"
|
|
11
|
+
echo ""
|
|
8
12
|
local warnings=0
|
|
9
13
|
|
|
10
14
|
# Check API connectivity if configured
|
|
@@ -86,14 +90,287 @@ preflight_checks() {
|
|
|
86
90
|
|
|
87
91
|
echo ""
|
|
88
92
|
if [[ $warnings -gt 0 ]]; then
|
|
89
|
-
print_warning "$warnings pre-
|
|
93
|
+
print_warning "$warnings pre-loop warning(s) - loop may fail on connectivity issues"
|
|
90
94
|
echo ""
|
|
91
95
|
read -r -p "Continue anyway? [Y/n] " response
|
|
92
96
|
if [[ "$response" =~ ^[Nn] ]]; then
|
|
93
97
|
echo "Aborted. Fix the issues and try again."
|
|
94
98
|
exit 1
|
|
95
99
|
fi
|
|
100
|
+
return 1 # Had warnings — don't cache this result
|
|
101
|
+
fi
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# PREFLIGHT / PRD CACHE
|
|
106
|
+
# ============================================================================
|
|
107
|
+
# Caches preflight and PRD validation results so restarts within 10 minutes
|
|
108
|
+
# skip the slow connectivity checks and Claude auto-fix.
|
|
109
|
+
# Cache is invalidated by TTL expiry or config/PRD file changes (by hash).
|
|
110
|
+
|
|
111
|
+
_file_hash() {
|
|
112
|
+
[[ ! -f "$1" ]] && echo "no_file" && return
|
|
113
|
+
if command -v md5sum &>/dev/null; then
|
|
114
|
+
md5sum "$1" 2>/dev/null | cut -d' ' -f1
|
|
115
|
+
else
|
|
116
|
+
md5 -q "$1" 2>/dev/null
|
|
117
|
+
fi
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_is_preflight_cached() {
|
|
121
|
+
local cache_file="$RALPH_DIR/.preflight_cache"
|
|
122
|
+
[[ ! -f "$cache_file" ]] && return 1
|
|
123
|
+
|
|
124
|
+
local cached_time cached_hash
|
|
125
|
+
read -r cached_time cached_hash < "$cache_file"
|
|
126
|
+
|
|
127
|
+
local now
|
|
128
|
+
now=$(date +%s)
|
|
129
|
+
[[ $(( now - cached_time )) -gt $PREFLIGHT_CACHE_TTL_SECONDS ]] && return 1
|
|
130
|
+
|
|
131
|
+
local config_hash
|
|
132
|
+
config_hash=$(_file_hash "$RALPH_DIR/config.json")
|
|
133
|
+
[[ "$cached_hash" != "$config_hash" ]] && return 1
|
|
134
|
+
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_write_preflight_cache() {
|
|
139
|
+
local config_hash
|
|
140
|
+
config_hash=$(_file_hash "$RALPH_DIR/config.json")
|
|
141
|
+
echo "$(date +%s) $config_hash" > "$RALPH_DIR/.preflight_cache"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_is_prd_cached() {
|
|
145
|
+
local cache_file="$RALPH_DIR/.prd_validated"
|
|
146
|
+
[[ ! -f "$cache_file" ]] && return 1
|
|
147
|
+
|
|
148
|
+
local cached_time cached_hash
|
|
149
|
+
read -r cached_time cached_hash < "$cache_file"
|
|
150
|
+
|
|
151
|
+
local now
|
|
152
|
+
now=$(date +%s)
|
|
153
|
+
[[ $(( now - cached_time )) -gt $PREFLIGHT_CACHE_TTL_SECONDS ]] && return 1
|
|
154
|
+
|
|
155
|
+
local prd_hash
|
|
156
|
+
prd_hash=$(_file_hash "$RALPH_DIR/prd.json")
|
|
157
|
+
[[ "$cached_hash" != "$prd_hash" ]] && return 1
|
|
158
|
+
|
|
159
|
+
return 0
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_write_prd_cache() {
|
|
163
|
+
local prd_hash
|
|
164
|
+
prd_hash=$(_file_hash "$RALPH_DIR/prd.json")
|
|
165
|
+
echo "$(date +%s) $prd_hash" > "$RALPH_DIR/.prd_validated"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Check if failure context is trivial (lint/format-only retries)
|
|
169
|
+
# Returns 0 (trivial) if ALL error lines match trivial patterns
|
|
170
|
+
_is_trivial_failure() {
|
|
171
|
+
local context="$1"
|
|
172
|
+
|
|
173
|
+
# Count non-empty, non-whitespace lines
|
|
174
|
+
local total_lines
|
|
175
|
+
total_lines=$(printf '%s\n' "$context" | grep -cE '\S' || echo "0")
|
|
176
|
+
|
|
177
|
+
# If very short context, consider trivial
|
|
178
|
+
[[ "$total_lines" -lt 3 ]] && return 0
|
|
179
|
+
|
|
180
|
+
# Count error/warning/fail lines that do NOT match trivial patterns
|
|
181
|
+
# Trivial patterns: auto-fix, formatting tools, style-only issues
|
|
182
|
+
local non_trivial_errors
|
|
183
|
+
non_trivial_errors=$(printf '%s\n' "$context" | grep -iE '(error|warning|fail)' | \
|
|
184
|
+
grep -cviE '(auto.?fix|prettier|eslint --fix|trailing whitespace|import order|isort|black|ruff format|ruff check.*--fix|no-unused-vars|Missing semicolon|Expected indentation)' \
|
|
185
|
+
|| echo "0")
|
|
186
|
+
|
|
187
|
+
# Trivial if no error lines survive the trivial-pattern filter
|
|
188
|
+
[[ "$non_trivial_errors" -eq 0 ]] && return 0
|
|
189
|
+
|
|
190
|
+
return 1
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Check if a proposed sign pattern is a duplicate of existing signs
|
|
194
|
+
# Returns 0 (is duplicate) if pattern is too similar to existing
|
|
195
|
+
_sign_is_duplicate() {
|
|
196
|
+
local pattern="$1"
|
|
197
|
+
|
|
198
|
+
[[ ! -f "$RALPH_DIR/signs.json" ]] && return 1
|
|
199
|
+
|
|
200
|
+
# Normalize: lowercase, strip punctuation
|
|
201
|
+
local normalized
|
|
202
|
+
normalized=$(printf '%s\n' "$pattern" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 ]//g' | tr -s ' ')
|
|
203
|
+
|
|
204
|
+
# Check each existing sign
|
|
205
|
+
local existing_patterns
|
|
206
|
+
existing_patterns=$(jq -r '.signs[].pattern' "$RALPH_DIR/signs.json" 2>/dev/null)
|
|
207
|
+
|
|
208
|
+
while IFS= read -r existing; do
|
|
209
|
+
[[ -z "$existing" ]] && continue
|
|
210
|
+
|
|
211
|
+
local existing_normalized
|
|
212
|
+
existing_normalized=$(printf '%s\n' "$existing" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 ]//g' | tr -s ' ')
|
|
213
|
+
|
|
214
|
+
# Substring match in either direction (only for patterns long enough to be meaningful)
|
|
215
|
+
local shorter_len=${#normalized}
|
|
216
|
+
[[ ${#existing_normalized} -lt $shorter_len ]] && shorter_len=${#existing_normalized}
|
|
217
|
+
if [[ $shorter_len -ge 30 ]]; then
|
|
218
|
+
if [[ "$normalized" == *"$existing_normalized"* ]] || [[ "$existing_normalized" == *"$normalized"* ]]; then
|
|
219
|
+
return 0
|
|
220
|
+
fi
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# Keyword overlap: extract words 4+ chars, flag as duplicate if >60% overlap
|
|
224
|
+
local new_words existing_words
|
|
225
|
+
new_words=$(printf '%s\n' "$normalized" | tr ' ' '\n' | awk 'length >= 4' | sort -u)
|
|
226
|
+
existing_words=$(printf '%s\n' "$existing_normalized" | tr ' ' '\n' | awk 'length >= 4' | sort -u)
|
|
227
|
+
|
|
228
|
+
local new_count overlap_count
|
|
229
|
+
new_count=$(printf '%s\n' "$new_words" | grep -cE '\S' || echo "0")
|
|
230
|
+
[[ "$new_count" -eq 0 ]] && continue
|
|
231
|
+
|
|
232
|
+
# Count overlapping words (use -xF for whole-line match, not substring)
|
|
233
|
+
overlap_count=0
|
|
234
|
+
while IFS= read -r word; do
|
|
235
|
+
[[ -z "$word" ]] && continue
|
|
236
|
+
if printf '%s\n' "$existing_words" | grep -qxF "$word"; then
|
|
237
|
+
overlap_count=$((overlap_count + 1))
|
|
238
|
+
fi
|
|
239
|
+
done <<< "$new_words"
|
|
240
|
+
|
|
241
|
+
# >60% overlap = duplicate
|
|
242
|
+
if [[ $((overlap_count * 100 / new_count)) -gt 60 ]]; then
|
|
243
|
+
return 0
|
|
244
|
+
fi
|
|
245
|
+
done <<< "$existing_patterns"
|
|
246
|
+
|
|
247
|
+
return 1
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Auto-promote a sign from retry failure context
|
|
251
|
+
# Called when a story passes after multiple retries
|
|
252
|
+
_maybe_promote_sign() {
|
|
253
|
+
local story="$1"
|
|
254
|
+
local retries="$2"
|
|
255
|
+
local config="$RALPH_DIR/config.json"
|
|
256
|
+
|
|
257
|
+
# Check config: read .autoPromoteSigns directly (avoid get_config - its // operator
|
|
258
|
+
# treats false as falsy and returns the default). Default to true if key is absent/null.
|
|
259
|
+
if [[ -f "$config" ]]; then
|
|
260
|
+
local auto_promote
|
|
261
|
+
auto_promote=$(jq -r '.autoPromoteSigns' "$config" 2>/dev/null)
|
|
262
|
+
if [[ "$auto_promote" == "false" ]]; then
|
|
263
|
+
return 0
|
|
264
|
+
fi
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
# Read failure context (safety check - caller also gates on file existence)
|
|
268
|
+
local failure_context
|
|
269
|
+
if [[ ! -f "$RALPH_DIR/last_failure.txt" ]]; then
|
|
270
|
+
return 0
|
|
96
271
|
fi
|
|
272
|
+
failure_context=$(head -"$MAX_SIGN_CONTEXT_LINES" "$RALPH_DIR/last_failure.txt")
|
|
273
|
+
|
|
274
|
+
# Skip trivial failures (lint/format only)
|
|
275
|
+
if _is_trivial_failure "$failure_context"; then
|
|
276
|
+
log_progress "$story" "SIGN_AUTO" "Skipped - trivial failure (lint/format only)"
|
|
277
|
+
return 0
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# Load existing sign patterns for dedup context
|
|
281
|
+
local existing_signs=""
|
|
282
|
+
if [[ -f "$RALPH_DIR/signs.json" ]]; then
|
|
283
|
+
existing_signs=$(jq -r '.signs[].pattern' "$RALPH_DIR/signs.json" 2>/dev/null | head -"$MAX_SIGN_DEDUP_EXISTING")
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# Build extraction prompt
|
|
287
|
+
local prompt
|
|
288
|
+
prompt="You are analyzing a development failure that was resolved after $retries attempts.
|
|
289
|
+
|
|
290
|
+
Extract ONE reusable pattern (a \"sign\") that would prevent this failure in future stories.
|
|
291
|
+
|
|
292
|
+
## Failure Context
|
|
293
|
+
\`\`\`
|
|
294
|
+
$failure_context
|
|
295
|
+
\`\`\`
|
|
296
|
+
|
|
297
|
+
## Existing Signs (do NOT duplicate these)
|
|
298
|
+
$existing_signs
|
|
299
|
+
|
|
300
|
+
## Rules
|
|
301
|
+
- Extract a single, actionable pattern that prevents this class of failure
|
|
302
|
+
- The pattern should be general enough to apply to future stories, not specific to this one
|
|
303
|
+
- If the failure is trivial, unclear, or you can't extract a useful pattern, respond with just: NONE
|
|
304
|
+
- Category must be one of: backend, frontend, testing, general, database, security
|
|
305
|
+
|
|
306
|
+
## Good Examples
|
|
307
|
+
CATEGORY: backend
|
|
308
|
+
PATTERN: Always run database migrations before executing test suites
|
|
309
|
+
|
|
310
|
+
CATEGORY: testing
|
|
311
|
+
PATTERN: Use waitFor() instead of fixed delays when testing async UI updates
|
|
312
|
+
|
|
313
|
+
CATEGORY: frontend
|
|
314
|
+
PATTERN: Import CSS modules with .module.css extension in Next.js projects
|
|
315
|
+
|
|
316
|
+
## Bad Examples (too specific, too vague)
|
|
317
|
+
PATTERN: Fix the login button color (too specific to one story)
|
|
318
|
+
PATTERN: Write better code (too vague to be actionable)
|
|
319
|
+
PATTERN: Always check for errors (too vague)
|
|
320
|
+
|
|
321
|
+
## Response Format
|
|
322
|
+
Respond with exactly two lines (or just NONE):
|
|
323
|
+
CATEGORY: <category>
|
|
324
|
+
PATTERN: <pattern>"
|
|
325
|
+
|
|
326
|
+
# Call Claude with timeout (one-shot, non-interactive)
|
|
327
|
+
local response
|
|
328
|
+
response=$(printf '%s\n' "$prompt" | run_with_timeout "$SIGN_EXTRACTION_TIMEOUT_SECONDS" claude -p 2>/dev/null) || {
|
|
329
|
+
log_progress "$story" "SIGN_AUTO" "Skipped - Claude extraction timed out"
|
|
330
|
+
return 0
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
# Check for NONE response
|
|
334
|
+
if printf '%s\n' "$response" | grep -qi '^NONE'; then
|
|
335
|
+
log_progress "$story" "SIGN_AUTO" "Skipped - no actionable pattern found"
|
|
336
|
+
return 0
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
# Parse response for CATEGORY: and PATTERN: lines (use sed, not grep -P for macOS)
|
|
340
|
+
local category pattern
|
|
341
|
+
category=$(echo "$response" | sed -n 's/^CATEGORY:[[:space:]]*//p' | head -1 | tr -d '\r')
|
|
342
|
+
pattern=$(echo "$response" | sed -n 's/^PATTERN:[[:space:]]*//p' | head -1 | tr -d '\r')
|
|
343
|
+
|
|
344
|
+
# Validate extracted values
|
|
345
|
+
if [[ -z "$category" || -z "$pattern" ]]; then
|
|
346
|
+
log_progress "$story" "SIGN_AUTO" "Skipped - could not parse Claude response"
|
|
347
|
+
return 0
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# Validate category
|
|
351
|
+
case "$category" in
|
|
352
|
+
backend|frontend|testing|general|database|security) ;;
|
|
353
|
+
*)
|
|
354
|
+
log_progress "$story" "SIGN_AUTO" "Skipped - invalid category: $category"
|
|
355
|
+
return 0
|
|
356
|
+
;;
|
|
357
|
+
esac
|
|
358
|
+
|
|
359
|
+
# Check for duplicates before adding
|
|
360
|
+
if _sign_is_duplicate "$pattern"; then
|
|
361
|
+
log_progress "$story" "SIGN_AUTO" "Skipped - duplicate of existing sign"
|
|
362
|
+
return 0
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
# Add the sign (3rd arg = autoPromoted, 4th arg = learnedFrom override)
|
|
366
|
+
if ralph_sign "$pattern" "$category" "true" "$story"; then
|
|
367
|
+
log_progress "$story" "SIGN_AUTO" "Added [$category]: $pattern"
|
|
368
|
+
print_info "Auto-promoted sign: [$category] $pattern"
|
|
369
|
+
else
|
|
370
|
+
log_progress "$story" "SIGN_AUTO" "Failed to add sign"
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
return 0
|
|
97
374
|
}
|
|
98
375
|
|
|
99
376
|
run_loop() {
|
|
@@ -128,8 +405,16 @@ run_loop() {
|
|
|
128
405
|
# Validate prerequisites
|
|
129
406
|
check_dependencies
|
|
130
407
|
|
|
131
|
-
# Pre-
|
|
132
|
-
|
|
408
|
+
# Pre-loop checks to catch issues before wasting iterations
|
|
409
|
+
if [[ "$fast_mode" == "true" ]]; then
|
|
410
|
+
print_info "Fast mode: skipping connectivity checks"
|
|
411
|
+
elif _is_preflight_cached; then
|
|
412
|
+
print_info "Pre-loop checks passed recently, skipping"
|
|
413
|
+
else
|
|
414
|
+
if preflight_checks; then
|
|
415
|
+
_write_preflight_cache
|
|
416
|
+
fi
|
|
417
|
+
fi
|
|
133
418
|
|
|
134
419
|
if [[ ! -f "$RALPH_DIR/prd.json" ]]; then
|
|
135
420
|
# Check for misplaced PRD in subdirectories
|
|
@@ -171,8 +456,17 @@ run_loop() {
|
|
|
171
456
|
fi
|
|
172
457
|
|
|
173
458
|
# Validate PRD structure
|
|
174
|
-
if
|
|
175
|
-
|
|
459
|
+
if [[ "$fast_mode" == "true" ]]; then
|
|
460
|
+
print_info "Fast mode: structural PRD check only"
|
|
461
|
+
validate_prd "$RALPH_DIR/prd.json" "true" || return 1
|
|
462
|
+
elif _is_prd_cached; then
|
|
463
|
+
print_info "PRD validated recently, structural check only"
|
|
464
|
+
validate_prd "$RALPH_DIR/prd.json" "true" || return 1
|
|
465
|
+
else
|
|
466
|
+
if ! validate_prd "$RALPH_DIR/prd.json"; then
|
|
467
|
+
return 1
|
|
468
|
+
fi
|
|
469
|
+
_write_prd_cache
|
|
176
470
|
fi
|
|
177
471
|
|
|
178
472
|
local iteration=0
|
|
@@ -181,9 +475,9 @@ run_loop() {
|
|
|
181
475
|
local consecutive_timeouts=0
|
|
182
476
|
local max_story_retries
|
|
183
477
|
local max_timeouts=5 # Skip after 5 consecutive timeouts (likely too large/complex)
|
|
184
|
-
# Default to
|
|
185
|
-
# Override with config.json: "maxStoryRetries":
|
|
186
|
-
max_story_retries=$(get_config '.maxStoryRetries' "
|
|
478
|
+
# Default to 5 retries - enough for transient issues, stops before wasting cycles
|
|
479
|
+
# Override with config.json: "maxStoryRetries": 8
|
|
480
|
+
max_story_retries=$(get_config '.maxStoryRetries' "5")
|
|
187
481
|
local total_attempts=0
|
|
188
482
|
local skipped_stories=()
|
|
189
483
|
local start_time
|
|
@@ -264,35 +558,32 @@ run_loop() {
|
|
|
264
558
|
'(.stories[] | select(.id==$id)) |= . + {retryCount: $count}' \
|
|
265
559
|
"$RALPH_DIR/prd.json" > "$RALPH_DIR/prd.json.tmp" && mv "$RALPH_DIR/prd.json.tmp" "$RALPH_DIR/prd.json"
|
|
266
560
|
|
|
267
|
-
# Circuit breaker:
|
|
268
|
-
#
|
|
269
|
-
# If a story consistently fails after this many tries, it likely needs manual review
|
|
270
|
-
# (vague test steps, missing prerequisites, or fundamentally broken requirements).
|
|
561
|
+
# Circuit breaker: stop the loop after max retries (stories depend on each other)
|
|
562
|
+
# If a story consistently fails after this many tries, it needs manual review.
|
|
271
563
|
if [[ $consecutive_failures -gt $max_story_retries ]]; then
|
|
272
|
-
print_error "Story $story has failed $consecutive_failures times -
|
|
564
|
+
print_error "Story $story has failed $consecutive_failures times - stopping loop"
|
|
273
565
|
echo ""
|
|
274
|
-
echo " This usually means:"
|
|
275
|
-
echo " - Test steps are too vague or ambiguous"
|
|
276
|
-
echo " - Missing prerequisites (DB setup, env vars, etc.)"
|
|
277
|
-
echo " - Story scope is too large - consider breaking it up"
|
|
278
|
-
echo ""
|
|
279
|
-
echo " Failure context saved to: $RALPH_DIR/failures/$story.txt"
|
|
280
566
|
mkdir -p "$RALPH_DIR/failures"
|
|
281
567
|
cp "$RALPH_DIR/last_failure.txt" "$RALPH_DIR/failures/$story.txt" 2>/dev/null || true
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
568
|
+
# Show the actual last error instead of generic guesses
|
|
569
|
+
if [[ -f "$RALPH_DIR/last_failure.txt" ]]; then
|
|
570
|
+
echo " Last failure:"
|
|
571
|
+
tail -20 "$RALPH_DIR/last_failure.txt" | sed 's/^/ /'
|
|
572
|
+
fi
|
|
573
|
+
echo ""
|
|
574
|
+
echo " Full failure context saved to: $RALPH_DIR/failures/$story.txt"
|
|
575
|
+
local passed failed
|
|
576
|
+
passed=$(jq '[.stories[] | select(.passes==true)] | length' "$RALPH_DIR/prd.json" 2>/dev/null || echo "0")
|
|
577
|
+
failed=$(jq '[.stories[] | select(.passes==false)] | length' "$RALPH_DIR/prd.json" 2>/dev/null || echo "0")
|
|
578
|
+
send_notification "🛑 Ralph stopped: $story failed $consecutive_failures times. $passed passed, $failed remaining"
|
|
579
|
+
print_progress_summary "$start_time" "$total_attempts" "0"
|
|
580
|
+
return 1
|
|
288
581
|
fi
|
|
289
582
|
|
|
290
583
|
# Show retry status (but don't make it scary - retrying is normal!)
|
|
291
584
|
if [[ $consecutive_failures -gt 1 ]]; then
|
|
292
585
|
if [[ $consecutive_failures -le 3 ]]; then
|
|
293
586
|
print_info "Attempt $consecutive_failures for $story (normal - refining solution)"
|
|
294
|
-
elif [[ $consecutive_failures -le 8 ]]; then
|
|
295
|
-
print_warning "Attempt $consecutive_failures/$max_story_retries for $story"
|
|
296
587
|
else
|
|
297
588
|
print_warning "Attempt $consecutive_failures/$max_story_retries for $story (getting close to limit)"
|
|
298
589
|
fi
|
|
@@ -424,18 +715,22 @@ run_loop() {
|
|
|
424
715
|
break
|
|
425
716
|
done
|
|
426
717
|
|
|
427
|
-
rm -f "$claude_output_log"
|
|
428
|
-
|
|
429
718
|
if [[ $crash_attempt -ge $max_crash_retries ]]; then
|
|
430
719
|
echo ""
|
|
431
720
|
print_warning "Claude API unavailable after $max_crash_retries attempts"
|
|
721
|
+
if [[ -f "$claude_output_log" ]]; then
|
|
722
|
+
echo " Last error:"
|
|
723
|
+
tail -5 "$claude_output_log" | sed 's/^/ /'
|
|
724
|
+
fi
|
|
432
725
|
print_info "Waiting 60s before retrying... (Ctrl+C to stop, then 'npx agentic-loop run' to restart)"
|
|
433
726
|
log_progress "$story" "CLI_CRASH" "API unavailable, waiting 60s before next iteration"
|
|
434
|
-
rm -f "$prompt_file"
|
|
727
|
+
rm -f "$prompt_file" "$claude_output_log"
|
|
435
728
|
sleep 60 # Longer cooldown before retrying
|
|
436
729
|
continue # Continue main loop instead of stopping
|
|
437
730
|
fi
|
|
438
731
|
|
|
732
|
+
rm -f "$claude_output_log"
|
|
733
|
+
|
|
439
734
|
if [[ $claude_exit_code -ne 0 ]]; then
|
|
440
735
|
((consecutive_timeouts++))
|
|
441
736
|
print_warning "Claude session ended (timeout or error) - timeout $consecutive_timeouts/$max_timeouts"
|
|
@@ -494,8 +789,15 @@ run_loop() {
|
|
|
494
789
|
update_json "$RALPH_DIR/prd.json" \
|
|
495
790
|
--arg id "$story" '(.stories[] | select(.id==$id)) |= . + {passes: true, retryCount: 0}'
|
|
496
791
|
|
|
792
|
+
# Auto-promote sign if story required retries
|
|
793
|
+
if [[ $consecutive_failures -gt 1 && -f "$RALPH_DIR/last_failure.txt" ]]; then
|
|
794
|
+
_maybe_promote_sign "$story" "$consecutive_failures"
|
|
795
|
+
fi
|
|
796
|
+
|
|
497
797
|
# Clear failure context on success
|
|
498
798
|
rm -f "$RALPH_DIR/last_failure.txt"
|
|
799
|
+
rm -f "$RALPH_DIR"/last_*_failure.log
|
|
800
|
+
rm -f "$RALPH_DIR"/last_*_check.log
|
|
499
801
|
rm -f "$RALPH_DIR/last_verification.log"
|
|
500
802
|
|
|
501
803
|
# Get story title for commit message and completion display
|
package/ralph/prd-check.sh
CHANGED
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
# Returns 0 if valid (possibly after auto-fix), 1 if unrecoverable error
|
|
103
103
|
validate_prd() {
|
|
104
104
|
local prd_file="$1"
|
|
105
|
+
local dry_run="${2:-false}"
|
|
105
106
|
|
|
106
107
|
# Check file exists
|
|
107
108
|
if [[ ! -f "$prd_file" ]]; then
|
|
@@ -219,15 +220,17 @@ validate_prd() {
|
|
|
219
220
|
echo ""
|
|
220
221
|
fi
|
|
221
222
|
|
|
222
|
-
# Validate API smoke test configuration
|
|
223
|
-
|
|
223
|
+
# Validate API smoke test configuration (skip in fast/cached mode)
|
|
224
|
+
if [[ "$dry_run" != "true" ]]; then
|
|
225
|
+
_validate_api_config "$config"
|
|
226
|
+
fi
|
|
224
227
|
|
|
225
228
|
# Replace hardcoded paths with config placeholders
|
|
226
229
|
fix_hardcoded_paths "$prd_file" "$config"
|
|
227
230
|
|
|
228
231
|
# Validate and fix individual stories
|
|
229
|
-
#
|
|
230
|
-
_validate_and_fix_stories "$prd_file" "$
|
|
232
|
+
# dry_run flag — when "true", skip auto-fix
|
|
233
|
+
_validate_and_fix_stories "$prd_file" "$dry_run" || return 1
|
|
231
234
|
|
|
232
235
|
return 0
|
|
233
236
|
}
|
|
@@ -323,6 +326,7 @@ _validate_and_fix_stories() {
|
|
|
323
326
|
local cnt_frontend_tsc=0 cnt_frontend_url=0 cnt_frontend_context=0 cnt_frontend_mcp=0
|
|
324
327
|
local cnt_auth_security=0 cnt_list_pagination=0 cnt_prose_steps=0
|
|
325
328
|
local cnt_migration_prereq=0 cnt_naming_convention=0 cnt_bare_pytest=0
|
|
329
|
+
local cnt_server_only=0
|
|
326
330
|
local cnt_custom=0
|
|
327
331
|
|
|
328
332
|
echo " Checking test coverage..."
|
|
@@ -471,6 +475,32 @@ _validate_and_fix_stories() {
|
|
|
471
475
|
fi
|
|
472
476
|
fi
|
|
473
477
|
|
|
478
|
+
# Check 9: Stories where ALL testSteps depend on a live server
|
|
479
|
+
# If every testStep is a curl/wget/httpie command and none are offline
|
|
480
|
+
# (npm test, pytest, tsc, playwright, cargo test, go test, etc.),
|
|
481
|
+
# the story will always fail without a running server.
|
|
482
|
+
if [[ -n "$test_steps" ]]; then
|
|
483
|
+
local has_offline_step=false
|
|
484
|
+
local has_server_step=false
|
|
485
|
+
local step_list
|
|
486
|
+
step_list=$(jq -r --arg id "$story_id" \
|
|
487
|
+
'.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
|
|
488
|
+
|
|
489
|
+
while IFS= read -r single_step; do
|
|
490
|
+
[[ -z "$single_step" ]] && continue
|
|
491
|
+
if echo "$single_step" | grep -qE "^(curl |wget |http )"; then
|
|
492
|
+
has_server_step=true
|
|
493
|
+
else
|
|
494
|
+
has_offline_step=true
|
|
495
|
+
fi
|
|
496
|
+
done <<< "$step_list"
|
|
497
|
+
|
|
498
|
+
if [[ "$has_server_step" == "true" && "$has_offline_step" == "false" ]]; then
|
|
499
|
+
story_issues+="all testSteps need a live server (add offline test: npm test, tsc --noEmit, pytest), "
|
|
500
|
+
cnt_server_only=$((cnt_server_only + 1))
|
|
501
|
+
fi
|
|
502
|
+
fi
|
|
503
|
+
|
|
474
504
|
# Snapshot built-in issues before custom checks append
|
|
475
505
|
local builtin_story_issues="$story_issues"
|
|
476
506
|
|
|
@@ -517,6 +547,7 @@ _validate_and_fix_stories() {
|
|
|
517
547
|
[[ $cnt_migration_prereq -gt 0 ]] && echo " ${cnt_migration_prereq}x migration: add prerequisites (DB reset)"
|
|
518
548
|
[[ $cnt_naming_convention -gt 0 ]] && echo " ${cnt_naming_convention}x API consumer: add camelCase transformation note"
|
|
519
549
|
[[ $cnt_bare_pytest -gt 0 ]] && echo " ${cnt_bare_pytest}x use 'uv run pytest' not bare 'pytest'"
|
|
550
|
+
[[ $cnt_server_only -gt 0 ]] && echo " ${cnt_server_only}x all testSteps need live server (add offline fallback)"
|
|
520
551
|
[[ $cnt_custom -gt 0 ]] && echo " ${cnt_custom} stories with custom check issues"
|
|
521
552
|
|
|
522
553
|
# Skip auto-fix in dry-run mode
|
|
@@ -630,6 +661,10 @@ RULES:
|
|
|
630
661
|
Example: \"notes\": \"Transform API responses from snake_case to camelCase. Create typed interfaces with camelCase properties and map: const user = { userName: data.user_name }\"
|
|
631
662
|
10. Each story should include its own techStack and constraints fields. Do NOT add these at the PRD root level.
|
|
632
663
|
Move any root-level techStack, globalConstraints, originalContext, testing, architecture, or testUsers into the relevant stories.
|
|
664
|
+
11. Stories where ALL testSteps are curl commands MUST also include at least one offline test step
|
|
665
|
+
that can verify code correctness without a running server.
|
|
666
|
+
Examples: \"npm test\", \"npx tsc --noEmit\", \"pytest tests/unit/\", \"go test ./...\"
|
|
667
|
+
This prevents wasted retries when the server isn't running.
|
|
633
668
|
|
|
634
669
|
CURRENT PRD:
|
|
635
670
|
$(cat "$prd_file")
|
|
@@ -788,6 +823,24 @@ validate_stories_quick() {
|
|
|
788
823
|
fi
|
|
789
824
|
fi
|
|
790
825
|
fi
|
|
826
|
+
|
|
827
|
+
# Check 8: All testSteps are server-dependent
|
|
828
|
+
if [[ -n "$test_steps" ]]; then
|
|
829
|
+
local has_offline=false has_server=false
|
|
830
|
+
local steps
|
|
831
|
+
steps=$(jq -r --arg id "$story_id" \
|
|
832
|
+
'.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
|
|
833
|
+
while IFS= read -r s; do
|
|
834
|
+
[[ -z "$s" ]] && continue
|
|
835
|
+
if echo "$s" | grep -qE "^(curl |wget |http )"; then
|
|
836
|
+
has_server=true
|
|
837
|
+
else
|
|
838
|
+
has_offline=true
|
|
839
|
+
fi
|
|
840
|
+
done <<< "$steps"
|
|
841
|
+
[[ "$has_server" == "true" && "$has_offline" == "false" ]] && \
|
|
842
|
+
issues+="$story_id: all testSteps need live server, "
|
|
843
|
+
fi
|
|
791
844
|
done <<< "$story_ids"
|
|
792
845
|
|
|
793
846
|
echo "$issues"
|
package/ralph/signs.sh
CHANGED
|
@@ -16,6 +16,8 @@ ralph_sign() {
|
|
|
16
16
|
|
|
17
17
|
local pattern="$1"
|
|
18
18
|
local category="${2:-general}"
|
|
19
|
+
local auto_promoted="${3:-false}"
|
|
20
|
+
local learned_from_override="${4:-}"
|
|
19
21
|
|
|
20
22
|
# Ensure .ralph directory exists
|
|
21
23
|
if [[ ! -d "$RALPH_DIR" ]]; then
|
|
@@ -34,8 +36,11 @@ ralph_sign() {
|
|
|
34
36
|
local sign_id="sign-$(printf '%03d' $((sign_count + 1)))"
|
|
35
37
|
|
|
36
38
|
# Get current story if available (for learnedFrom field)
|
|
39
|
+
# Override can be passed as 4th arg (used by auto-promote, since story is already marked passed)
|
|
37
40
|
local learned_from=""
|
|
38
|
-
if [[ -
|
|
41
|
+
if [[ -n "$learned_from_override" ]]; then
|
|
42
|
+
learned_from="$learned_from_override"
|
|
43
|
+
elif [[ -f "$RALPH_DIR/prd.json" ]]; then
|
|
39
44
|
learned_from=$(jq -r '.stories[] | select(.passes==false) | .id' "$RALPH_DIR/prd.json" 2>/dev/null | head -1)
|
|
40
45
|
fi
|
|
41
46
|
|
|
@@ -52,11 +57,13 @@ ralph_sign() {
|
|
|
52
57
|
--arg category "$category" \
|
|
53
58
|
--arg learnedFrom "$learned_from" \
|
|
54
59
|
--arg createdAt "$timestamp" \
|
|
60
|
+
--argjson autoPromoted "$( [[ "$auto_promoted" == "true" ]] && echo "true" || echo "false" )" \
|
|
55
61
|
'.signs += [{
|
|
56
62
|
id: $id,
|
|
57
63
|
pattern: $pattern,
|
|
58
64
|
category: $category,
|
|
59
65
|
learnedFrom: (if $learnedFrom == "" then null else $learnedFrom end),
|
|
66
|
+
autoPromoted: $autoPromoted,
|
|
60
67
|
createdAt: $createdAt
|
|
61
68
|
}]' "$RALPH_DIR/signs.json" > "$tmpfile" && jq -e . "$tmpfile" >/dev/null 2>&1; then
|
|
62
69
|
mv "$tmpfile" "$RALPH_DIR/signs.json"
|
|
@@ -100,7 +107,7 @@ ralph_signs() {
|
|
|
100
107
|
[[ -z "$category" ]] && continue
|
|
101
108
|
|
|
102
109
|
echo "[$category]"
|
|
103
|
-
jq -r --arg cat "$category" '.signs[] | select(.category==$cat) | " - \(.pattern)"' "$RALPH_DIR/signs.json"
|
|
110
|
+
jq -r --arg cat "$category" '.signs[] | select(.category==$cat) | " - \(.pattern)\(if .autoPromoted == true then " (auto)" else "" end)"' "$RALPH_DIR/signs.json"
|
|
104
111
|
echo ""
|
|
105
112
|
done <<< "$categories"
|
|
106
113
|
}
|
package/ralph/test.sh
CHANGED
|
@@ -134,11 +134,15 @@ run_all_prd_tests() {
|
|
|
134
134
|
[[ -z "$step" ]] && continue
|
|
135
135
|
((total++))
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
# Expand config placeholders (e.g., {config.urls.backend})
|
|
138
|
+
local expanded_step
|
|
139
|
+
expanded_step=$(_expand_config_vars "$step")
|
|
140
|
+
|
|
141
|
+
echo -n " $expanded_step... "
|
|
138
142
|
|
|
139
143
|
local step_log
|
|
140
144
|
step_log=$(mktemp)
|
|
141
|
-
if safe_exec "$
|
|
145
|
+
if safe_exec "$expanded_step" "$step_log"; then
|
|
142
146
|
print_success "passed"
|
|
143
147
|
((passed++))
|
|
144
148
|
else
|
package/ralph/utils.sh
CHANGED
|
@@ -10,6 +10,8 @@ readonly MAX_OUTPUT_PREVIEW_LINES=20
|
|
|
10
10
|
readonly MAX_ERROR_PREVIEW_LINES=40
|
|
11
11
|
readonly MAX_LINT_ERROR_LINES=20
|
|
12
12
|
readonly MAX_PROGRESS_FILE_LINES=1000
|
|
13
|
+
readonly MAX_SIGN_CONTEXT_LINES=150
|
|
14
|
+
readonly MAX_SIGN_DEDUP_EXISTING=20
|
|
13
15
|
|
|
14
16
|
# Constants - Timeouts (centralized to avoid magic numbers)
|
|
15
17
|
readonly ITERATION_DELAY_SECONDS=0
|
|
@@ -19,6 +21,8 @@ readonly CODE_REVIEW_TIMEOUT_SECONDS=120
|
|
|
19
21
|
readonly BROWSER_TIMEOUT_SECONDS=60
|
|
20
22
|
readonly BROWSER_PAGE_TIMEOUT_MS=30000
|
|
21
23
|
readonly CURL_TIMEOUT_SECONDS=10
|
|
24
|
+
readonly SIGN_EXTRACTION_TIMEOUT_SECONDS=30
|
|
25
|
+
readonly PREFLIGHT_CACHE_TTL_SECONDS=600
|
|
22
26
|
|
|
23
27
|
# Common project directories (avoid duplication across files)
|
|
24
28
|
readonly FRONTEND_DIRS=("apps/web" "frontend" "client" "web")
|
|
@@ -598,7 +602,9 @@ fix_hardcoded_paths() {
|
|
|
598
602
|
local original_content="$prd_content"
|
|
599
603
|
|
|
600
604
|
# Check for hardcoded absolute paths (non-portable)
|
|
601
|
-
|
|
605
|
+
# Note: stderr suppressed on echo|grep -q pipes to silence "broken pipe" noise
|
|
606
|
+
# (grep -q exits early on match, closing the pipe while echo is still writing)
|
|
607
|
+
if echo "$prd_content" 2>/dev/null | grep -qE '"/Users/|"/home/|"C:\\|"/var/|"/opt/' ; then
|
|
602
608
|
echo " Removing hardcoded absolute paths..."
|
|
603
609
|
# Remove common absolute path prefixes, keep relative path
|
|
604
610
|
prd_content=$(echo "$prd_content" | sed -E 's|"/Users/[^"]*/([^"]+)"|"\1"|g')
|
|
@@ -607,7 +613,7 @@ fix_hardcoded_paths() {
|
|
|
607
613
|
fi
|
|
608
614
|
|
|
609
615
|
# Replace hardcoded backend URLs with {config.urls.backend}
|
|
610
|
-
if [[ -n "$backend_url" ]] && echo "$prd_content" | grep -qF "$backend_url" ; then
|
|
616
|
+
if [[ -n "$backend_url" ]] && echo "$prd_content" 2>/dev/null | grep -qF "$backend_url" ; then
|
|
611
617
|
echo " Replacing hardcoded backend URL with {config.urls.backend}..."
|
|
612
618
|
local escaped_url
|
|
613
619
|
escaped_url=$(_escape_sed_pattern "$backend_url")
|
|
@@ -616,7 +622,7 @@ fix_hardcoded_paths() {
|
|
|
616
622
|
fi
|
|
617
623
|
|
|
618
624
|
# Replace hardcoded frontend URLs with {config.urls.frontend}
|
|
619
|
-
if [[ -n "$frontend_url" ]] && echo "$prd_content" | grep -qF "$frontend_url" ; then
|
|
625
|
+
if [[ -n "$frontend_url" ]] && echo "$prd_content" 2>/dev/null | grep -qF "$frontend_url" ; then
|
|
620
626
|
echo " Replacing hardcoded frontend URL with {config.urls.frontend}..."
|
|
621
627
|
local escaped_url
|
|
622
628
|
escaped_url=$(_escape_sed_pattern "$frontend_url")
|
|
@@ -625,7 +631,7 @@ fix_hardcoded_paths() {
|
|
|
625
631
|
fi
|
|
626
632
|
|
|
627
633
|
# Replace hardcoded health endpoints with config placeholder
|
|
628
|
-
if echo "$prd_content" | grep -qE '/api(/v[0-9]+)?/health|/health' ; then
|
|
634
|
+
if echo "$prd_content" 2>/dev/null | grep -qE '/api(/v[0-9]+)?/health|/health' ; then
|
|
629
635
|
echo " Replacing hardcoded health endpoints with {config.api.healthEndpoint}..."
|
|
630
636
|
prd_content=$(echo "$prd_content" | sed -E 's|/api/v[0-9]+/health|{config.api.healthEndpoint}|g')
|
|
631
637
|
prd_content=$(echo "$prd_content" | sed -E 's|/api/health|{config.api.healthEndpoint}|g')
|
|
@@ -637,7 +643,7 @@ fix_hardcoded_paths() {
|
|
|
637
643
|
# Note: Use # as delimiter since | appears in regex alternation
|
|
638
644
|
if [[ -z "$backend_url" ]]; then
|
|
639
645
|
# Common backend ports: 8000, 8001, 8080, 3001, 4000, 5000
|
|
640
|
-
if echo "$prd_content" | grep -qE 'http://localhost:(8000|8001|8080|3001|4000|5000)' ; then
|
|
646
|
+
if echo "$prd_content" 2>/dev/null | grep -qE 'http://localhost:(8000|8001|8080|3001|4000|5000)' ; then
|
|
641
647
|
echo " Replacing hardcoded localhost backend URLs with {config.urls.backend}..."
|
|
642
648
|
prd_content=$(echo "$prd_content" | sed -E 's#http://localhost:(8000|8001|8080|3001|4000|5000)#{config.urls.backend}#g')
|
|
643
649
|
modified=true
|
|
@@ -646,7 +652,7 @@ fix_hardcoded_paths() {
|
|
|
646
652
|
|
|
647
653
|
if [[ -z "$frontend_url" ]]; then
|
|
648
654
|
# Common frontend ports: 3000, 5173, 4200
|
|
649
|
-
if echo "$prd_content" | grep -qE 'http://localhost:(3000|5173|4200)' ; then
|
|
655
|
+
if echo "$prd_content" 2>/dev/null | grep -qE 'http://localhost:(3000|5173|4200)' ; then
|
|
650
656
|
echo " Replacing hardcoded localhost frontend URLs with {config.urls.frontend}..."
|
|
651
657
|
prd_content=$(echo "$prd_content" | sed -E 's#http://localhost:(3000|5173|4200)#{config.urls.frontend}#g')
|
|
652
658
|
modified=true
|
package/ralph/verify/tests.sh
CHANGED
|
@@ -226,8 +226,11 @@ run_unit_tests() {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
# Expand config placeholders in a string
|
|
229
|
-
# Usage:
|
|
230
|
-
# Expands
|
|
229
|
+
# Usage: _expand_config_vars "curl {config.urls.backend}/api"
|
|
230
|
+
# Expands any {config.X.Y} placeholder from .ralph/config.json via jq.
|
|
231
|
+
# Known placeholders have fallback paths for backward compatibility:
|
|
232
|
+
# {config.urls.backend} -> .urls.backend // .api.baseUrl
|
|
233
|
+
# {config.urls.frontend} -> .urls.frontend // .testUrlBase
|
|
231
234
|
_expand_config_vars() {
|
|
232
235
|
local input="$1"
|
|
233
236
|
local config="$RALPH_DIR/config.json"
|
|
@@ -237,41 +240,38 @@ _expand_config_vars() {
|
|
|
237
240
|
|
|
238
241
|
local result="$input"
|
|
239
242
|
|
|
240
|
-
#
|
|
243
|
+
# Known placeholders with backward-compatible fallback paths
|
|
241
244
|
if [[ "$result" == *"{config.urls.backend}"* ]]; then
|
|
242
|
-
local
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
result="${result//\{config.urls.backend\}/$backend_url}"
|
|
246
|
-
fi
|
|
245
|
+
local val
|
|
246
|
+
val=$(jq -r '.urls.backend // .api.baseUrl // empty' "$config" 2>/dev/null)
|
|
247
|
+
[[ -n "$val" ]] && result="${result//\{config.urls.backend\}/$val}"
|
|
247
248
|
fi
|
|
248
249
|
|
|
249
|
-
# Expand {config.urls.frontend}
|
|
250
250
|
if [[ "$result" == *"{config.urls.frontend}"* ]]; then
|
|
251
|
-
local
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
result="${result//\{config.urls.frontend\}/$frontend_url}"
|
|
255
|
-
fi
|
|
256
|
-
fi
|
|
257
|
-
|
|
258
|
-
# Expand {config.directories.backend}
|
|
259
|
-
if [[ "$result" == *"{config.directories.backend}"* ]]; then
|
|
260
|
-
local backend_dir
|
|
261
|
-
backend_dir=$(jq -r '.directories.backend // empty' "$config" 2>/dev/null)
|
|
262
|
-
if [[ -n "$backend_dir" ]]; then
|
|
263
|
-
result="${result//\{config.directories.backend\}/$backend_dir}"
|
|
264
|
-
fi
|
|
251
|
+
local val
|
|
252
|
+
val=$(jq -r '.urls.frontend // .testUrlBase // empty' "$config" 2>/dev/null)
|
|
253
|
+
[[ -n "$val" ]] && result="${result//\{config.urls.frontend\}/$val}"
|
|
265
254
|
fi
|
|
266
255
|
|
|
267
|
-
#
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
256
|
+
# Generic expansion for any remaining {config.X.Y.Z} placeholders
|
|
257
|
+
# Handles {config.urls.app}, {config.api.healthEndpoint}, {config.directories.*}, etc.
|
|
258
|
+
local max_expansions=10
|
|
259
|
+
while [[ "$result" =~ \{config\.([a-zA-Z0-9_.]+)\} ]] && [[ $max_expansions -gt 0 ]]; do
|
|
260
|
+
local placeholder="${BASH_REMATCH[0]}"
|
|
261
|
+
local config_path="${BASH_REMATCH[1]}"
|
|
262
|
+
local jq_path=".${config_path}"
|
|
263
|
+
|
|
264
|
+
local val
|
|
265
|
+
val=$(jq -r "$jq_path // empty" "$config" 2>/dev/null)
|
|
266
|
+
if [[ -n "$val" ]]; then
|
|
267
|
+
result="${result//$placeholder/$val}"
|
|
268
|
+
else
|
|
269
|
+
# Unresolvable — warn and stop to avoid infinite loop
|
|
270
|
+
print_warning "Unresolved config placeholder: $placeholder (key '$config_path' not in config.json)" >&2
|
|
271
|
+
break
|
|
273
272
|
fi
|
|
274
|
-
|
|
273
|
+
((max_expansions--))
|
|
274
|
+
done
|
|
275
275
|
|
|
276
276
|
echo "$result"
|
|
277
277
|
}
|