agentic-loop 3.16.2 → 3.17.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.
@@ -101,7 +101,7 @@ Break the idea into small, executable stories:
101
101
 
102
102
  - Each story completable in one Claude session (~10-15 min)
103
103
  - Max 3-4 acceptance criteria per story
104
- - Max 10 stories (suggest phases if more needed)
104
+ - No limit on story count — generate as many stories as the idea needs
105
105
  - If appending, start IDs from the next available number
106
106
  - **Each story must include its own `techStack`, `constraints`, and `contextFiles`.** Include only what's relevant to that story — don't copy-paste identical context into every story.
107
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-loop",
3
- "version": "3.16.2",
3
+ "version": "3.17.0",
4
4
  "description": "Autonomous AI coding loop - PRD-driven development with Claude Code",
5
5
  "author": "Allie Jones <allie@allthrive.ai>",
6
6
  "license": "MIT",
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-flight checks to catch common issues before wasting iterations
5
+ # Pre-loop checks to catch common issues before wasting iterations
6
6
  preflight_checks() {
7
- echo "--- Pre-flight Checks ---"
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,7 +90,7 @@ preflight_checks() {
86
90
 
87
91
  echo ""
88
92
  if [[ $warnings -gt 0 ]]; then
89
- print_warning "$warnings pre-flight warning(s) - loop may fail on connectivity issues"
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
@@ -96,6 +100,214 @@ preflight_checks() {
96
100
  fi
97
101
  }
98
102
 
103
+ # Check if failure context is trivial (lint/format-only retries)
104
+ # Returns 0 (trivial) if ALL error lines match trivial patterns
105
+ _is_trivial_failure() {
106
+ local context="$1"
107
+
108
+ # Count non-empty, non-whitespace lines
109
+ local total_lines
110
+ total_lines=$(printf '%s\n' "$context" | grep -cE '\S' || echo "0")
111
+
112
+ # If very short context, consider trivial
113
+ [[ "$total_lines" -lt 3 ]] && return 0
114
+
115
+ # Count error/warning/fail lines that do NOT match trivial patterns
116
+ # Trivial patterns: auto-fix, formatting tools, style-only issues
117
+ local non_trivial_errors
118
+ non_trivial_errors=$(printf '%s\n' "$context" | grep -iE '(error|warning|fail)' | \
119
+ 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)' \
120
+ || echo "0")
121
+
122
+ # Trivial if no error lines survive the trivial-pattern filter
123
+ [[ "$non_trivial_errors" -eq 0 ]] && return 0
124
+
125
+ return 1
126
+ }
127
+
128
+ # Check if a proposed sign pattern is a duplicate of existing signs
129
+ # Returns 0 (is duplicate) if pattern is too similar to existing
130
+ _sign_is_duplicate() {
131
+ local pattern="$1"
132
+
133
+ [[ ! -f "$RALPH_DIR/signs.json" ]] && return 1
134
+
135
+ # Normalize: lowercase, strip punctuation
136
+ local normalized
137
+ normalized=$(printf '%s\n' "$pattern" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 ]//g' | tr -s ' ')
138
+
139
+ # Check each existing sign
140
+ local existing_patterns
141
+ existing_patterns=$(jq -r '.signs[].pattern' "$RALPH_DIR/signs.json" 2>/dev/null)
142
+
143
+ while IFS= read -r existing; do
144
+ [[ -z "$existing" ]] && continue
145
+
146
+ local existing_normalized
147
+ existing_normalized=$(printf '%s\n' "$existing" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 ]//g' | tr -s ' ')
148
+
149
+ # Substring match in either direction (only for patterns long enough to be meaningful)
150
+ local shorter_len=${#normalized}
151
+ [[ ${#existing_normalized} -lt $shorter_len ]] && shorter_len=${#existing_normalized}
152
+ if [[ $shorter_len -ge 30 ]]; then
153
+ if [[ "$normalized" == *"$existing_normalized"* ]] || [[ "$existing_normalized" == *"$normalized"* ]]; then
154
+ return 0
155
+ fi
156
+ fi
157
+
158
+ # Keyword overlap: extract words 4+ chars, flag as duplicate if >60% overlap
159
+ local new_words existing_words
160
+ new_words=$(printf '%s\n' "$normalized" | tr ' ' '\n' | awk 'length >= 4' | sort -u)
161
+ existing_words=$(printf '%s\n' "$existing_normalized" | tr ' ' '\n' | awk 'length >= 4' | sort -u)
162
+
163
+ local new_count overlap_count
164
+ new_count=$(printf '%s\n' "$new_words" | grep -cE '\S' || echo "0")
165
+ [[ "$new_count" -eq 0 ]] && continue
166
+
167
+ # Count overlapping words (use -xF for whole-line match, not substring)
168
+ overlap_count=0
169
+ while IFS= read -r word; do
170
+ [[ -z "$word" ]] && continue
171
+ if printf '%s\n' "$existing_words" | grep -qxF "$word"; then
172
+ overlap_count=$((overlap_count + 1))
173
+ fi
174
+ done <<< "$new_words"
175
+
176
+ # >60% overlap = duplicate
177
+ if [[ $((overlap_count * 100 / new_count)) -gt 60 ]]; then
178
+ return 0
179
+ fi
180
+ done <<< "$existing_patterns"
181
+
182
+ return 1
183
+ }
184
+
185
+ # Auto-promote a sign from retry failure context
186
+ # Called when a story passes after multiple retries
187
+ _maybe_promote_sign() {
188
+ local story="$1"
189
+ local retries="$2"
190
+ local config="$RALPH_DIR/config.json"
191
+
192
+ # Check config: read .autoPromoteSigns directly (avoid get_config - its // operator
193
+ # treats false as falsy and returns the default). Default to true if key is absent/null.
194
+ if [[ -f "$config" ]]; then
195
+ local auto_promote
196
+ auto_promote=$(jq -r '.autoPromoteSigns' "$config" 2>/dev/null)
197
+ if [[ "$auto_promote" == "false" ]]; then
198
+ return 0
199
+ fi
200
+ fi
201
+
202
+ # Read failure context (safety check - caller also gates on file existence)
203
+ local failure_context
204
+ if [[ ! -f "$RALPH_DIR/last_failure.txt" ]]; then
205
+ return 0
206
+ fi
207
+ failure_context=$(head -"$MAX_SIGN_CONTEXT_LINES" "$RALPH_DIR/last_failure.txt")
208
+
209
+ # Skip trivial failures (lint/format only)
210
+ if _is_trivial_failure "$failure_context"; then
211
+ log_progress "$story" "SIGN_AUTO" "Skipped - trivial failure (lint/format only)"
212
+ return 0
213
+ fi
214
+
215
+ # Load existing sign patterns for dedup context
216
+ local existing_signs=""
217
+ if [[ -f "$RALPH_DIR/signs.json" ]]; then
218
+ existing_signs=$(jq -r '.signs[].pattern' "$RALPH_DIR/signs.json" 2>/dev/null | head -"$MAX_SIGN_DEDUP_EXISTING")
219
+ fi
220
+
221
+ # Build extraction prompt
222
+ local prompt
223
+ prompt="You are analyzing a development failure that was resolved after $retries attempts.
224
+
225
+ Extract ONE reusable pattern (a \"sign\") that would prevent this failure in future stories.
226
+
227
+ ## Failure Context
228
+ \`\`\`
229
+ $failure_context
230
+ \`\`\`
231
+
232
+ ## Existing Signs (do NOT duplicate these)
233
+ $existing_signs
234
+
235
+ ## Rules
236
+ - Extract a single, actionable pattern that prevents this class of failure
237
+ - The pattern should be general enough to apply to future stories, not specific to this one
238
+ - If the failure is trivial, unclear, or you can't extract a useful pattern, respond with just: NONE
239
+ - Category must be one of: backend, frontend, testing, general, database, security
240
+
241
+ ## Good Examples
242
+ CATEGORY: backend
243
+ PATTERN: Always run database migrations before executing test suites
244
+
245
+ CATEGORY: testing
246
+ PATTERN: Use waitFor() instead of fixed delays when testing async UI updates
247
+
248
+ CATEGORY: frontend
249
+ PATTERN: Import CSS modules with .module.css extension in Next.js projects
250
+
251
+ ## Bad Examples (too specific, too vague)
252
+ PATTERN: Fix the login button color (too specific to one story)
253
+ PATTERN: Write better code (too vague to be actionable)
254
+ PATTERN: Always check for errors (too vague)
255
+
256
+ ## Response Format
257
+ Respond with exactly two lines (or just NONE):
258
+ CATEGORY: <category>
259
+ PATTERN: <pattern>"
260
+
261
+ # Call Claude with timeout (one-shot, non-interactive)
262
+ local response
263
+ response=$(printf '%s\n' "$prompt" | run_with_timeout "$SIGN_EXTRACTION_TIMEOUT_SECONDS" claude -p 2>/dev/null) || {
264
+ log_progress "$story" "SIGN_AUTO" "Skipped - Claude extraction timed out"
265
+ return 0
266
+ }
267
+
268
+ # Check for NONE response
269
+ if printf '%s\n' "$response" | grep -qi '^NONE'; then
270
+ log_progress "$story" "SIGN_AUTO" "Skipped - no actionable pattern found"
271
+ return 0
272
+ fi
273
+
274
+ # Parse response for CATEGORY: and PATTERN: lines (use sed, not grep -P for macOS)
275
+ local category pattern
276
+ category=$(echo "$response" | sed -n 's/^CATEGORY:[[:space:]]*//p' | head -1 | tr -d '\r')
277
+ pattern=$(echo "$response" | sed -n 's/^PATTERN:[[:space:]]*//p' | head -1 | tr -d '\r')
278
+
279
+ # Validate extracted values
280
+ if [[ -z "$category" || -z "$pattern" ]]; then
281
+ log_progress "$story" "SIGN_AUTO" "Skipped - could not parse Claude response"
282
+ return 0
283
+ fi
284
+
285
+ # Validate category
286
+ case "$category" in
287
+ backend|frontend|testing|general|database|security) ;;
288
+ *)
289
+ log_progress "$story" "SIGN_AUTO" "Skipped - invalid category: $category"
290
+ return 0
291
+ ;;
292
+ esac
293
+
294
+ # Check for duplicates before adding
295
+ if _sign_is_duplicate "$pattern"; then
296
+ log_progress "$story" "SIGN_AUTO" "Skipped - duplicate of existing sign"
297
+ return 0
298
+ fi
299
+
300
+ # Add the sign (3rd arg = autoPromoted, 4th arg = learnedFrom override)
301
+ if ralph_sign "$pattern" "$category" "true" "$story"; then
302
+ log_progress "$story" "SIGN_AUTO" "Added [$category]: $pattern"
303
+ print_info "Auto-promoted sign: [$category] $pattern"
304
+ else
305
+ log_progress "$story" "SIGN_AUTO" "Failed to add sign"
306
+ fi
307
+
308
+ return 0
309
+ }
310
+
99
311
  run_loop() {
100
312
  local max_iterations="$DEFAULT_MAX_ITERATIONS"
101
313
  local specific_story=""
@@ -128,7 +340,7 @@ run_loop() {
128
340
  # Validate prerequisites
129
341
  check_dependencies
130
342
 
131
- # Pre-flight checks to catch issues before wasting iterations
343
+ # Pre-loop checks to catch issues before wasting iterations
132
344
  preflight_checks
133
345
 
134
346
  if [[ ! -f "$RALPH_DIR/prd.json" ]]; then
@@ -181,9 +393,9 @@ run_loop() {
181
393
  local consecutive_timeouts=0
182
394
  local max_story_retries
183
395
  local max_timeouts=5 # Skip after 5 consecutive timeouts (likely too large/complex)
184
- # Default to 8 retries - enough for transient issues, catches infinite loops
185
- # Override with config.json: "maxStoryRetries": 12
186
- max_story_retries=$(get_config '.maxStoryRetries' "8")
396
+ # Default to 5 retries - enough for transient issues, stops before wasting cycles
397
+ # Override with config.json: "maxStoryRetries": 8
398
+ max_story_retries=$(get_config '.maxStoryRetries' "5")
187
399
  local total_attempts=0
188
400
  local skipped_stories=()
189
401
  local start_time
@@ -264,12 +476,10 @@ run_loop() {
264
476
  '(.stories[] | select(.id==$id)) |= . + {retryCount: $count}' \
265
477
  "$RALPH_DIR/prd.json" > "$RALPH_DIR/prd.json.tmp" && mv "$RALPH_DIR/prd.json.tmp" "$RALPH_DIR/prd.json"
266
478
 
267
- # Circuit breaker: skip to next story after max retries (prevents infinite loops)
268
- # Note: This is NOT meant to stop legitimate retrying - 8 attempts is enough.
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).
479
+ # Circuit breaker: stop the loop after max retries (stories depend on each other)
480
+ # If a story consistently fails after this many tries, it needs manual review.
271
481
  if [[ $consecutive_failures -gt $max_story_retries ]]; then
272
- print_error "Story $story has failed $consecutive_failures times - likely needs manual review"
482
+ print_error "Story $story has failed $consecutive_failures times - stopping loop"
273
483
  echo ""
274
484
  echo " This usually means:"
275
485
  echo " - Test steps are too vague or ambiguous"
@@ -279,20 +489,18 @@ run_loop() {
279
489
  echo " Failure context saved to: $RALPH_DIR/failures/$story.txt"
280
490
  mkdir -p "$RALPH_DIR/failures"
281
491
  cp "$RALPH_DIR/last_failure.txt" "$RALPH_DIR/failures/$story.txt" 2>/dev/null || true
282
- rm -f "$RALPH_DIR/last_failure.txt"
283
- skipped_stories+=("$story")
284
- jq --arg id "$story" '(.stories[] | select(.id==$id)) |= . + {skipped: true, skipReason: "exceeded max retries"}' "$RALPH_DIR/prd.json" > "$RALPH_DIR/prd.json.tmp" && mv "$RALPH_DIR/prd.json.tmp" "$RALPH_DIR/prd.json"
285
- last_story=""
286
- consecutive_failures=0
287
- continue
492
+ local passed failed
493
+ passed=$(jq '[.stories[] | select(.passes==true)] | length' "$RALPH_DIR/prd.json" 2>/dev/null || echo "0")
494
+ failed=$(jq '[.stories[] | select(.passes==false)] | length' "$RALPH_DIR/prd.json" 2>/dev/null || echo "0")
495
+ send_notification "🛑 Ralph stopped: $story failed $consecutive_failures times. $passed passed, $failed remaining"
496
+ print_progress_summary "$start_time" "$total_attempts" "0"
497
+ return 1
288
498
  fi
289
499
 
290
500
  # Show retry status (but don't make it scary - retrying is normal!)
291
501
  if [[ $consecutive_failures -gt 1 ]]; then
292
502
  if [[ $consecutive_failures -le 3 ]]; then
293
503
  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
504
  else
297
505
  print_warning "Attempt $consecutive_failures/$max_story_retries for $story (getting close to limit)"
298
506
  fi
@@ -494,6 +702,11 @@ run_loop() {
494
702
  update_json "$RALPH_DIR/prd.json" \
495
703
  --arg id "$story" '(.stories[] | select(.id==$id)) |= . + {passes: true, retryCount: 0}'
496
704
 
705
+ # Auto-promote sign if story required retries
706
+ if [[ $consecutive_failures -gt 1 && -f "$RALPH_DIR/last_failure.txt" ]]; then
707
+ _maybe_promote_sign "$story" "$consecutive_failures"
708
+ fi
709
+
497
710
  # Clear failure context on success
498
711
  rm -f "$RALPH_DIR/last_failure.txt"
499
712
  rm -f "$RALPH_DIR/last_verification.log"
package/ralph/prd.sh CHANGED
@@ -158,10 +158,7 @@ ralph_prd() {
158
158
  printf '%s\n' "- Context for the next story" >> "$prompt_file"
159
159
  printf '\n%s\n' "## Context Rot Check" >> "$prompt_file"
160
160
  printf '\n%s\n' "Before finalizing, validate:" >> "$prompt_file"
161
- printf '%s\n' "- If > 10 stories, recommend splitting into phases" >> "$prompt_file"
162
- printf '%s\n' '- If complexity is "high" AND > 5 stories, MUST split' >> "$prompt_file"
163
161
  printf '%s\n' "- Each story should be completable in one Claude session (~10 min)" >> "$prompt_file"
164
- printf '\n%s\n' "If splitting is needed, generate ONLY phase 1 and note deferred items." >> "$prompt_file"
165
162
  printf '\n%s\n' "## Context in Stories - IMPORTANT" >> "$prompt_file"
166
163
  printf '\n%s\n' "Each story must be self-contained. Include the idea file path in each story's 'contextFiles' array." >> "$prompt_file"
167
164
  printf '%s\n' "Include 'techStack' with the relevant subset of detected technologies for that story." >> "$prompt_file"
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 [[ -f "$RALPH_DIR/prd.json" ]]; then
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/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,7 @@ 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
22
25
 
23
26
  # Common project directories (avoid duplication across files)
24
27
  readonly FRONTEND_DIRS=("apps/web" "frontend" "client" "web")
@@ -598,7 +601,9 @@ fix_hardcoded_paths() {
598
601
  local original_content="$prd_content"
599
602
 
600
603
  # Check for hardcoded absolute paths (non-portable)
601
- if echo "$prd_content" | grep -qE '"/Users/|"/home/|"C:\\|"/var/|"/opt/' ; then
604
+ # Note: stderr suppressed on echo|grep -q pipes to silence "broken pipe" noise
605
+ # (grep -q exits early on match, closing the pipe while echo is still writing)
606
+ if echo "$prd_content" 2>/dev/null | grep -qE '"/Users/|"/home/|"C:\\|"/var/|"/opt/' ; then
602
607
  echo " Removing hardcoded absolute paths..."
603
608
  # Remove common absolute path prefixes, keep relative path
604
609
  prd_content=$(echo "$prd_content" | sed -E 's|"/Users/[^"]*/([^"]+)"|"\1"|g')
@@ -607,7 +612,7 @@ fix_hardcoded_paths() {
607
612
  fi
608
613
 
609
614
  # Replace hardcoded backend URLs with {config.urls.backend}
610
- if [[ -n "$backend_url" ]] && echo "$prd_content" | grep -qF "$backend_url" ; then
615
+ if [[ -n "$backend_url" ]] && echo "$prd_content" 2>/dev/null | grep -qF "$backend_url" ; then
611
616
  echo " Replacing hardcoded backend URL with {config.urls.backend}..."
612
617
  local escaped_url
613
618
  escaped_url=$(_escape_sed_pattern "$backend_url")
@@ -616,7 +621,7 @@ fix_hardcoded_paths() {
616
621
  fi
617
622
 
618
623
  # Replace hardcoded frontend URLs with {config.urls.frontend}
619
- if [[ -n "$frontend_url" ]] && echo "$prd_content" | grep -qF "$frontend_url" ; then
624
+ if [[ -n "$frontend_url" ]] && echo "$prd_content" 2>/dev/null | grep -qF "$frontend_url" ; then
620
625
  echo " Replacing hardcoded frontend URL with {config.urls.frontend}..."
621
626
  local escaped_url
622
627
  escaped_url=$(_escape_sed_pattern "$frontend_url")
@@ -625,7 +630,7 @@ fix_hardcoded_paths() {
625
630
  fi
626
631
 
627
632
  # Replace hardcoded health endpoints with config placeholder
628
- if echo "$prd_content" | grep -qE '/api(/v[0-9]+)?/health|/health' ; then
633
+ if echo "$prd_content" 2>/dev/null | grep -qE '/api(/v[0-9]+)?/health|/health' ; then
629
634
  echo " Replacing hardcoded health endpoints with {config.api.healthEndpoint}..."
630
635
  prd_content=$(echo "$prd_content" | sed -E 's|/api/v[0-9]+/health|{config.api.healthEndpoint}|g')
631
636
  prd_content=$(echo "$prd_content" | sed -E 's|/api/health|{config.api.healthEndpoint}|g')
@@ -637,7 +642,7 @@ fix_hardcoded_paths() {
637
642
  # Note: Use # as delimiter since | appears in regex alternation
638
643
  if [[ -z "$backend_url" ]]; then
639
644
  # 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
645
+ if echo "$prd_content" 2>/dev/null | grep -qE 'http://localhost:(8000|8001|8080|3001|4000|5000)' ; then
641
646
  echo " Replacing hardcoded localhost backend URLs with {config.urls.backend}..."
642
647
  prd_content=$(echo "$prd_content" | sed -E 's#http://localhost:(8000|8001|8080|3001|4000|5000)#{config.urls.backend}#g')
643
648
  modified=true
@@ -646,7 +651,7 @@ fix_hardcoded_paths() {
646
651
 
647
652
  if [[ -z "$frontend_url" ]]; then
648
653
  # Common frontend ports: 3000, 5173, 4200
649
- if echo "$prd_content" | grep -qE 'http://localhost:(3000|5173|4200)' ; then
654
+ if echo "$prd_content" 2>/dev/null | grep -qE 'http://localhost:(3000|5173|4200)' ; then
650
655
  echo " Replacing hardcoded localhost frontend URLs with {config.urls.frontend}..."
651
656
  prd_content=$(echo "$prd_content" | sed -E 's#http://localhost:(3000|5173|4200)#{config.urls.frontend}#g')
652
657
  modified=true