agentic-loop 3.11.0 → 3.12.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/README.md CHANGED
@@ -4,28 +4,51 @@
4
4
 
5
5
  You describe what you want to build. Claude Code writes a PRD (Product Requirements Document) with small, testable stories. Ralph executes each story automatically - coding, testing, and committing in a loop until everything passes.
6
6
 
7
- > **Optimized for:** Python, TypeScript, React, Go/Hugo, FastMCP, and Docker projects.
8
-
9
7
  ---
10
8
 
11
- ## What It Does
9
+ ## What It Does and How to work with Agentic Loop
12
10
 
13
- **Brainstorm ideas with `/idea`**
14
- Describe a feature in plain English. Claude asks clarifying questions, explores your codebase, and generates a PRD with atomic stories that can be implemented one at a time.
11
+ ### The Two-Terminal Workflow
15
12
 
16
- **Execute with Ralph**
17
- Ralph reads the PRD and implements each story autonomously. It spawns Claude, runs verification (lint, tests, browser checks), and either commits on success or retries with error context on failure.
13
+ ```
14
+ ┌──────────────────────────────────────────────────────────────────────────────────┐
15
+ │ TERMINAL 1: Claude CLI │ TERMINAL 2: Execute │
16
+ ├────────────────────────────────────────┼─────────────────────────────────────────┤
17
+ │ │ │
18
+ │ claude --dangerously-skip-permissions │ npx agentic-loop run │
19
+ │ │ │
20
+ │ PLAN FEATURES │ ┌─ prd-check (once) ───────────────┐ │
21
+ │ /idea 'your feature or bugfix' │ │ Validate all stories upfront │ │
22
+ │ → Claude asks questions │ │ Auto-fix missing test steps │ │
23
+ │ → Explores codebase │ └──────────────────────────────────┘ │
24
+ │ → Generates PRD │ ↓ │
25
+ │ │ ┌─ loop (per story) ───────────────┐ │
26
+ │ ENHANCE AS YOU LEARN │ │ │ │
27
+ │ → Add signs when Ralph repeats │ │ Read prd.json → get next story │ │
28
+ │ the same mistake │ │ Load signs.json, config.json │ │
29
+ │ → Tune timeouts, retries, checks │ │ Load last_failure.txt (if retry) │ │
30
+ │ → Refine test commands for your │ │ Build prompt with full context │ │
31
+ │ stack │ │ Spawn Claude → write code │ │
32
+ │ │ │ │ │
33
+ │ (OPTIONAL) CUSTOMIZE YOUR LOOP │ │ code-check: │ │
34
+ │ /my-dna → your coding style │ │ [1] Lint │ │
35
+ │ /styleguide → UI consistency │ │ [2] Tests │ │
36
+ │ /sign → teach patterns │ │ [3] PRD test steps │ │
37
+ │ config.json → tune your setup │ │ [4] API smoke │ │
38
+ │ │ │ [5] Frontend smoke │ │
39
+ │ │ │ │ │
40
+ │ │ │ Pass → commit, next story │ │
41
+ │ │ │ Fail → save to last_failure.txt, │ │
42
+ │ │ │ retry │ │
43
+ │ │ └──────────────────────────────────┘ │
44
+ │ │ │
45
+ └────────────────────────────────────────┴─────────────────────────────────────────┘
46
+ ```
18
47
 
19
- **Customize your output**
20
- - `/my-dna` - Add your voice and values so the code reflects your style
21
- - `/styleguide` - Generate a UI component reference for consistent design
48
+ **Terminal 1** is where you shape *what* gets built and *how* Ralph builds it.
49
+ **Terminal 2** is where Ralph executes autonomously.
22
50
 
23
- **Built-in guardrails**
24
- - `/vibe-check`, `/review` - On-demand quality and security checks
25
- - Pre-commit hooks - Block secrets, hardcoded URLs, debug statements
26
- - Claude Code hooks - Real-time warnings while coding
27
- - GitHub Actions CI/CD - Fast PR checks + comprehensive nightly tests
28
- - Test file enforcement - Fails if new code lacks corresponding tests
51
+ Your loop gets smarter over time. When Ralph struggles with something, add a sign. When tests flake, tune the config. The customization never really stops—it's how you make Ralph work for *your* project.
29
52
 
30
53
  ---
31
54
 
@@ -54,34 +77,13 @@ npx agentic-loop run # Execute PRDs autonomously
54
77
 
55
78
  ---
56
79
 
57
- ## How Ralph Works
58
-
59
- ```
60
- ┌─────────────────────────────────────────────────────────────┐
61
- │ RALPH LOOP │
62
- ├─────────────────────────────────────────────────────────────┤
63
- │ 1. Read prd.json → find next story where passes=false │
64
- │ 2. Build prompt (story + context + failures + signs) │
65
- │ 3. Spawn Claude with prompt + MCP browser tools │
66
- │ 4. Run verification (lint, tests, testSteps) │
67
- │ 5. Pass? → commit, next story │
68
- │ Fail? → save error, retry with failure context │
69
- │ 6. Repeat until all stories pass │
70
- └─────────────────────────────────────────────────────────────┘
71
- ```
72
-
73
- **What's a PRD?**
74
- A JSON file (`.ralph/prd.json`) containing your feature broken into small stories. Each story has acceptance criteria, test steps, and a test URL. Ralph implements them one by one. See [`templates/prd-example.json`](templates/prd-example.json) for a complete example.
75
-
76
- **What are Signs?**
77
- Patterns Ralph learns from failures. If Ralph keeps making the same mistake, add a sign: `npx agentic-loop sign "Always use camelCase for API fields" backend`. Future stories will see this guidance.
78
-
79
- ---
80
-
81
80
  ## Docs
82
81
 
83
- - [How Ralph Works](docs/RALPH.md) - Architecture, config, verification pipeline
84
- - [Technical Architecture](docs/ARCHITECTURE.md) - Deep dive for developers
82
+ - **[Beginners Guide](docs/BEGINNERS.md)** - New to this? Start here (no coding experience required)
83
+ - [PRD Check](docs/PRD-CHECK.md) - Story validation before coding starts
84
+ - [Code Check](docs/CODE-CHECK.md) - Verification pipeline after each story
85
+ - [Customization](docs/CUSTOMIZATION.md) - Personalization and guardrails
86
+ - [How Ralph Works](docs/RALPH.md) - Architecture, config, full reference
85
87
  - [Cheatsheet](docs/CHEATSHEET.md) - All commands at a glance
86
88
  - [Hooks Reference](docs/HOOKS.md) - Pre-commit and Claude Code hooks
87
89
  - [Troubleshooting](docs/TROUBLESHOOTING.md) - Common issues and fixes
@@ -1 +1 @@
1
- {"version":3,"file":"check-snake-case-ts.d.ts","sourceRoot":"","sources":["../../src/checks/check-snake-case-ts.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,IAAI,EAA2B,MAAM,mBAAmB,CAAC;AAEvE,eAAO,MAAM,gBAAgB,EAAE,IAmF9B,CAAC"}
1
+ {"version":3,"file":"check-snake-case-ts.d.ts","sourceRoot":"","sources":["../../src/checks/check-snake-case-ts.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,IAAI,EAA2B,MAAM,mBAAmB,CAAC;AAEvE,eAAO,MAAM,gBAAgB,EAAE,IAmH9B,CAAC"}
@@ -68,6 +68,34 @@ export const checkSnakeCaseTs = {
68
68
  }
69
69
  }
70
70
  }
71
+ // Check for snake_case property access (e.g., data.user_name, data?.user_name)
72
+ // This catches direct usage of snake_case from API responses without transformation
73
+ // Supports both regular (.) and optional chaining (?.) access
74
+ const propertyAccessRegex = /(\?)?\.([a-z][a-z0-9]*(?:_[a-z0-9]+)+)(?:\s*[,;)\]}]|\s*$|\s*\.|\s*\?\.|\s*!\.|\s*&&|\s*\|\||\s*\?|\s*:|\s*===|\s*!==|\s*==|\s*!=)/g;
75
+ let accessMatch;
76
+ while ((accessMatch = propertyAccessRegex.exec(line)) !== null) {
77
+ const isOptionalChain = accessMatch[1] === '?';
78
+ const propName = accessMatch[2];
79
+ const suggestedName = toCamelCase(propName);
80
+ const accessor = isOptionalChain ? '?.' : '.';
81
+ // Skip if it's in a comment
82
+ const beforeMatch = line.substring(0, accessMatch.index);
83
+ if (beforeMatch.includes('//') || beforeMatch.includes('/*')) {
84
+ continue;
85
+ }
86
+ // Skip common exceptions (CSS properties, external library patterns)
87
+ if (['line_number', 'column_number', 'stack_trace'].includes(propName)) {
88
+ continue;
89
+ }
90
+ results.push({
91
+ line: lineNum,
92
+ column: accessMatch.index + 1,
93
+ message: `Property access "${accessor}${propName}" uses snake_case - transform API response to camelCase "${accessor}${suggestedName}"`,
94
+ severity: 'warning',
95
+ ruleId: 'snake-case/access',
96
+ fix: suggestedName,
97
+ });
98
+ }
71
99
  }
72
100
  return results;
73
101
  },
@@ -1 +1 @@
1
- {"version":3,"file":"check-snake-case-ts.js","sourceRoot":"","sources":["../../src/checks/check-snake-case-ts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"check-snake-case-ts.js","sourceRoot":"","sources":["../../src/checks/check-snake-case-ts.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-loop",
3
- "version": "3.11.0",
3
+ "version": "3.12.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/init.sh CHANGED
@@ -379,6 +379,30 @@ auto_configure_project() {
379
379
  echo " Auto-detected api.baseUrl: $api_url"
380
380
  updated=true
381
381
  fi
382
+
383
+ # 4b. Detect api.healthEndpoint - probe common endpoints if server is running
384
+ if ! jq -e '.api.healthEndpoint' "$tmpfile" >/dev/null 2>&1 || [[ "$(jq -r '.api.healthEndpoint' "$tmpfile")" == "null" ]]; then
385
+ if command -v curl &>/dev/null; then
386
+ local health_endpoint=""
387
+ # Common health endpoint paths to try (most specific first)
388
+ local health_paths=("/api/v1/health" "/api/health" "/health" "/healthz" "/api/v1/healthz" "/status" "/")
389
+
390
+ for path in "${health_paths[@]}"; do
391
+ local http_code
392
+ http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 "${api_url}${path}" 2>/dev/null) || http_code="000"
393
+ if [[ "$http_code" =~ ^[23] ]]; then
394
+ health_endpoint="$path"
395
+ break
396
+ fi
397
+ done
398
+
399
+ if [[ -n "$health_endpoint" ]]; then
400
+ jq --arg ep "$health_endpoint" '.api.healthEndpoint = $ep' "$tmpfile" > "${tmpfile}.new" && mv "${tmpfile}.new" "$tmpfile"
401
+ echo " Auto-detected api.healthEndpoint: $health_endpoint"
402
+ updated=true
403
+ fi
404
+ fi
405
+ fi
382
406
  fi
383
407
 
384
408
  # 5. Detect package manager
package/ralph/loop.sh CHANGED
@@ -223,6 +223,21 @@ run_loop() {
223
223
  fi
224
224
 
225
225
  if [[ -z "$story" ]]; then
226
+ # Safety check: verify PRD is valid before claiming all stories passed
227
+ # An empty/corrupt PRD would also result in no stories found
228
+ local total_stories
229
+ total_stories=$(jq '.stories | length' "$RALPH_DIR/prd.json" 2>/dev/null || echo "0")
230
+ if [[ "$total_stories" == "0" || "$total_stories" == "null" ]]; then
231
+ print_error "PRD appears to be empty or corrupted!"
232
+ echo ""
233
+ echo " The prd.json file has no stories. This usually means:"
234
+ echo " - The file was accidentally cleared"
235
+ echo " - A write operation failed"
236
+ echo ""
237
+ echo " Check for backups: ls -la $RALPH_DIR/*.bak"
238
+ echo " Or restore from git: git checkout $RALPH_DIR/prd.json"
239
+ return 1
240
+ fi
226
241
  print_progress_summary "$start_time" "$total_attempts" "${#skipped_stories[@]}"
227
242
  send_notification "✅ Ralph finished: All stories passed!"
228
243
  archive_feature
@@ -50,6 +50,11 @@
50
50
  # - Has prerequisites array with DB reset command
51
51
  # - Prevents infinite retries on schema mismatch errors
52
52
  #
53
+ # API configuration validation:
54
+ # - If api.baseUrl configured, checks health endpoint is reachable
55
+ # - Warns if default /health returns 404 (misconfigured healthEndpoint)
56
+ # - Catches endpoint mismatches before loop starts
57
+ #
53
58
  # ============================================================================
54
59
  # AUTO-FIX
55
60
  # ============================================================================
@@ -68,6 +73,8 @@
68
73
  #
69
74
  # .checks.requireTests - Warn if no test directory configured
70
75
  # .tests.directory - Where tests live (for requireTests check)
76
+ # .api.baseUrl - API base URL (enables API config validation)
77
+ # .api.healthEndpoint - Health check path (default: /health, empty to disable)
71
78
  #
72
79
  # ============================================================================
73
80
  # USAGE
@@ -177,6 +184,9 @@ validate_prd() {
177
184
  fi
178
185
  fi
179
186
 
187
+ # Validate API smoke test configuration
188
+ _validate_api_config "$config"
189
+
180
190
  # Replace hardcoded paths with config placeholders
181
191
  fix_hardcoded_paths "$prd_file" "$config"
182
192
 
@@ -190,6 +200,79 @@ validate_prd() {
190
200
  # INTERNAL FUNCTIONS
191
201
  # ============================================================================
192
202
 
203
+ # Validate API smoke test configuration
204
+ # Checks that configured health endpoint is reachable (warns if not)
205
+ _validate_api_config() {
206
+ local config="$1"
207
+
208
+ [[ ! -f "$config" ]] && return 0
209
+
210
+ local base_url
211
+ base_url=$(jq -r '.api.baseUrl // empty' "$config" 2>/dev/null)
212
+
213
+ # No API configured, skip
214
+ [[ -z "$base_url" ]] && return 0
215
+
216
+ echo " Validating API configuration..."
217
+
218
+ local health_endpoint
219
+ local health_endpoint_raw
220
+ health_endpoint_raw=$(jq -r '.api.healthEndpoint' "$config" 2>/dev/null)
221
+
222
+ # If explicitly set to empty string, disable health check
223
+ if [[ "$health_endpoint_raw" == "" ]]; then
224
+ print_info "Health check disabled (healthEndpoint is empty)"
225
+ return 0
226
+ fi
227
+
228
+ # If not configured at all (null), warn about the default
229
+ if [[ "$health_endpoint_raw" == "null" ]]; then
230
+ echo ""
231
+ print_warning "No api.healthEndpoint configured - defaulting to /health"
232
+ echo " If your API uses a different health endpoint, add to .ralph/config.json:"
233
+ echo " {\"api\": {\"baseUrl\": \"$base_url\", \"healthEndpoint\": \"/your/health/path\"}}"
234
+ echo ""
235
+ health_endpoint="/health"
236
+ else
237
+ health_endpoint="$health_endpoint_raw"
238
+ fi
239
+
240
+ # Test the health endpoint
241
+ local url="${base_url}${health_endpoint}"
242
+ local http_code
243
+
244
+ http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$url" 2>/dev/null) || http_code="000"
245
+
246
+ if [[ "$http_code" == "000" ]]; then
247
+ print_warning "API not reachable at $base_url (server may not be running yet)"
248
+ return 0
249
+ elif [[ "$http_code" == "404" ]]; then
250
+ echo ""
251
+ print_error "API health endpoint not found: $health_endpoint (HTTP 404)"
252
+ echo ""
253
+ echo " The configured health endpoint doesn't exist at: $url"
254
+ echo ""
255
+ echo " Fix in .ralph/config.json:"
256
+ echo " {\"api\": {\"baseUrl\": \"$base_url\", \"healthEndpoint\": \"/correct/path\"}}"
257
+ echo ""
258
+ echo " Common health endpoints:"
259
+ echo " /api/health, /api/v1/health, /healthz, /status, /"
260
+ echo ""
261
+ echo " Set to empty string to disable health check:"
262
+ echo " {\"api\": {\"healthEndpoint\": \"\"}}"
263
+ echo ""
264
+ # Don't fail - API tests will fail later with more context
265
+ return 0
266
+ elif [[ "$http_code" =~ ^5 ]]; then
267
+ print_warning "API health check returned error: HTTP $http_code"
268
+ return 0
269
+ else
270
+ print_success "API health check passed: $health_endpoint (HTTP $http_code)"
271
+ fi
272
+
273
+ return 0
274
+ }
275
+
193
276
  # Validate individual stories and auto-fix with Claude if needed
194
277
  _validate_and_fix_stories() {
195
278
  local prd_file="$1"
@@ -201,7 +284,7 @@ _validate_and_fix_stories() {
201
284
  local cnt_no_tests=0 cnt_backend_curl=0 cnt_backend_contract=0
202
285
  local cnt_frontend_tsc=0 cnt_frontend_url=0 cnt_frontend_context=0
203
286
  local cnt_auth_security=0 cnt_list_pagination=0 cnt_prose_steps=0
204
- local cnt_migration_prereq=0
287
+ local cnt_migration_prereq=0 cnt_naming_convention=0
205
288
 
206
289
  echo " Checking test coverage..."
207
290
 
@@ -311,6 +394,21 @@ _validate_and_fix_stories() {
311
394
  fi
312
395
  fi
313
396
 
397
+ # Check 7: Frontend stories consuming APIs need naming convention notes
398
+ # If story is frontend/general AND mentions API/fetch/axios, ensure notes include camelCase guidance
399
+ if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
400
+ local story_desc
401
+ story_desc=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.title + " " + (.acceptanceCriteria // [] | join(" ")) + " " + (.notes // ""))' "$prd_file")
402
+ if echo "$story_desc" | grep -qiE "(api|fetch|axios|endpoint|backend|response)"; then
403
+ local story_notes
404
+ story_notes=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .notes // ""' "$prd_file")
405
+ if ! echo "$story_notes" | grep -qiE "(camelCase|snake_case|naming)"; then
406
+ story_issues+="API consumer needs camelCase transformation note, "
407
+ cnt_naming_convention=$((cnt_naming_convention + 1))
408
+ fi
409
+ fi
410
+ fi
411
+
314
412
  # Track this story if it has issues
315
413
  if [[ -n "$story_issues" ]]; then
316
414
  needs_fix=true
@@ -335,6 +433,7 @@ _validate_and_fix_stories() {
335
433
  [[ $cnt_auth_security -gt 0 ]] && echo " ${cnt_auth_security}x auth: add security criteria"
336
434
  [[ $cnt_list_pagination -gt 0 ]] && echo " ${cnt_list_pagination}x list: add pagination"
337
435
  [[ $cnt_migration_prereq -gt 0 ]] && echo " ${cnt_migration_prereq}x migration: add prerequisites (DB reset)"
436
+ [[ $cnt_naming_convention -gt 0 ]] && echo " ${cnt_naming_convention}x API consumer: add camelCase transformation note"
338
437
 
339
438
  # Check if Claude is available for auto-fix
340
439
  if command -v claude &>/dev/null; then
@@ -375,6 +474,8 @@ RULES:
375
474
  - Accepts ?page=N&limit=N query params
376
475
  7. Migration stories (creating alembic/versions, migrations/, or modifying models) MUST have prerequisites:
377
476
  Example: \"prerequisites\": [{\"name\": \"Reset test DB\", \"command\": \"npm run db:reset:test\", \"when\": \"schema changes\"}]
477
+ 8. Frontend/general stories that consume APIs MUST have notes about naming conventions:
478
+ Example: \"notes\": \"Transform API responses from snake_case to camelCase. Create typed interfaces with camelCase properties and map: const user = { userName: data.user_name }\"
378
479
 
379
480
  CURRENT PRD:
380
481
  $(cat "$prd_file")
@@ -394,11 +495,34 @@ Output ONLY the fixed JSON, no explanation. Start with { and end with }."
394
495
  fixed_prd=$(echo "$raw_response" | sed 's/^```json//; s/^```//; s/```$//')
395
496
  fi
396
497
 
498
+ # Create backup BEFORE any validation/write attempts
499
+ local backup_file="${prd_file}.$(date +%Y%m%d-%H%M%S).bak"
500
+ cp "$prd_file" "$backup_file"
501
+
502
+ # Get original story count for validation
503
+ local orig_story_count
504
+ orig_story_count=$(jq '.stories | length' "$prd_file" 2>/dev/null || echo "0")
505
+
397
506
  # Validate the response is valid JSON with required structure
398
507
  if echo "$fixed_prd" | jq -e '.stories' >/dev/null 2>&1; then
399
- # Timestamped backup (preserves history across multiple fixes)
400
- local backup_file="${prd_file}.$(date +%Y%m%d-%H%M%S).bak"
401
- cp "$prd_file" "$backup_file"
508
+ # Critical: Check story count is preserved (not just that .stories exists)
509
+ local new_story_count
510
+ new_story_count=$(echo "$fixed_prd" | jq '.stories | length' 2>/dev/null || echo "0")
511
+ if [[ "$new_story_count" -lt "$orig_story_count" ]]; then
512
+ print_warning "Fixed PRD has fewer stories ($orig_story_count -> $new_story_count) - keeping original"
513
+ echo " Backup preserved at: $backup_file"
514
+ return 0
515
+ fi
516
+
517
+ # Safety check: ensure we're not writing drastically smaller content
518
+ local orig_size new_size
519
+ orig_size=$(wc -c < "$prd_file" | tr -d ' ')
520
+ new_size=${#fixed_prd}
521
+ if [[ $new_size -lt $((orig_size / 3)) ]]; then
522
+ print_warning "Fixed PRD seems too small ($orig_size -> $new_size bytes) - keeping original"
523
+ echo " Backup preserved at: $backup_file"
524
+ return 0
525
+ fi
402
526
 
403
527
  # Write fixed PRD
404
528
  echo "$fixed_prd" > "$prd_file"
@@ -412,6 +536,7 @@ Output ONLY the fixed JSON, no explanation. Start with { and end with }."
412
536
  fi
413
537
  else
414
538
  print_warning "Could not auto-optimize - continuing with current PRD"
539
+ echo " Backup preserved at: $backup_file"
415
540
  return 0 # Don't fail, just continue
416
541
  fi
417
542
  }
@@ -492,6 +617,19 @@ validate_stories_quick() {
492
617
  issues+="$story_id: migration needs prerequisites, "
493
618
  fi
494
619
  fi
620
+
621
+ # Check 7: Frontend/general stories consuming APIs need naming convention notes
622
+ if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
623
+ local story_desc
624
+ story_desc=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.title + " " + (.acceptanceCriteria // [] | join(" ")) + " " + (.notes // ""))' "$prd_file")
625
+ if echo "$story_desc" | grep -qiE "(api|fetch|axios|endpoint|backend|response)"; then
626
+ local story_notes
627
+ story_notes=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .notes // ""' "$prd_file")
628
+ if ! echo "$story_notes" | grep -qiE "(camelCase|snake_case|naming)"; then
629
+ issues+="$story_id: needs camelCase transformation note, "
630
+ fi
631
+ fi
632
+ fi
495
633
  done <<< "$story_ids"
496
634
 
497
635
  echo "$issues"
package/ralph/prd.sh CHANGED
@@ -265,13 +265,20 @@ ralph_prd_accept() {
265
265
  # - Ensure all stories have passes: false
266
266
  # - Ensure all stories have id and title
267
267
  # - Set status to pending
268
- prd_json=$(echo "$prd_json" | jq '
268
+ local normalized_json
269
+ normalized_json=$(echo "$prd_json" | jq '
269
270
  .feature.status = "pending" |
270
271
  .stories = [.stories[] | . + {passes: (.passes // false)}]
271
272
  ')
272
273
 
274
+ # Safety check: don't save if normalization failed
275
+ if [[ -z "$normalized_json" ]] || ! echo "$normalized_json" | jq -e '.stories' >/dev/null 2>&1; then
276
+ print_error "PRD normalization failed - saving original JSON instead"
277
+ normalized_json="$prd_json"
278
+ fi
279
+
273
280
  # Save
274
- echo "$prd_json" > "$RALPH_DIR/prd.json"
281
+ echo "$normalized_json" > "$RALPH_DIR/prd.json"
275
282
 
276
283
  # Run full validation
277
284
  if ! validate_prd "$RALPH_DIR/prd.json"; then
package/ralph/utils.sh CHANGED
@@ -429,6 +429,12 @@ send_notification() {
429
429
  return 0
430
430
  }
431
431
 
432
+ # Escape special regex characters in a string for use in sed
433
+ # Usage: escaped=$(escape_sed_pattern "http://localhost:8000")
434
+ _escape_sed_pattern() {
435
+ printf '%s' "$1" | sed 's/[.[\/*^$()+?{|]/\\&/g'
436
+ }
437
+
432
438
  # Replace hardcoded paths/URLs with config placeholders
433
439
  # Makes PRDs portable across machines and environments
434
440
  fix_hardcoded_paths() {
@@ -440,6 +446,11 @@ fix_hardcoded_paths() {
440
446
  local prd_content
441
447
  prd_content=$(cat "$prd_file")
442
448
 
449
+ # Safety check - don't proceed if file is empty
450
+ if [[ -z "$prd_content" ]]; then
451
+ return 0
452
+ fi
453
+
443
454
  # Get URLs from config (if available)
444
455
  local backend_url="" frontend_url=""
445
456
  if [[ -f "$config_file" ]]; then
@@ -447,6 +458,9 @@ fix_hardcoded_paths() {
447
458
  frontend_url=$(jq -r '.urls.frontend // .playwright.baseUrl // empty' "$config_file" 2>/dev/null)
448
459
  fi
449
460
 
461
+ # Store original for safety check
462
+ local original_content="$prd_content"
463
+
450
464
  # Check for hardcoded absolute paths (non-portable)
451
465
  if echo "$prd_content" | grep -qE '"/Users/|"/home/|"C:\\|"/var/|"/opt/' ; then
452
466
  echo " Removing hardcoded absolute paths..."
@@ -457,16 +471,20 @@ fix_hardcoded_paths() {
457
471
  fi
458
472
 
459
473
  # Replace hardcoded backend URLs with {config.urls.backend}
460
- if [[ -n "$backend_url" ]] && echo "$prd_content" | grep -qF "\"$backend_url" ; then
474
+ if [[ -n "$backend_url" ]] && echo "$prd_content" | grep -qF "$backend_url" ; then
461
475
  echo " Replacing hardcoded backend URL with {config.urls.backend}..."
462
- prd_content=$(echo "$prd_content" | sed "s|$backend_url|{config.urls.backend}|g")
476
+ local escaped_url
477
+ escaped_url=$(_escape_sed_pattern "$backend_url")
478
+ prd_content=$(echo "$prd_content" | sed "s|$escaped_url|{config.urls.backend}|g")
463
479
  modified=true
464
480
  fi
465
481
 
466
482
  # Replace hardcoded frontend URLs with {config.urls.frontend}
467
- if [[ -n "$frontend_url" ]] && echo "$prd_content" | grep -qF "\"$frontend_url" ; then
483
+ if [[ -n "$frontend_url" ]] && echo "$prd_content" | grep -qF "$frontend_url" ; then
468
484
  echo " Replacing hardcoded frontend URL with {config.urls.frontend}..."
469
- prd_content=$(echo "$prd_content" | sed "s|$frontend_url|{config.urls.frontend}|g")
485
+ local escaped_url
486
+ escaped_url=$(_escape_sed_pattern "$frontend_url")
487
+ prd_content=$(echo "$prd_content" | sed "s|$escaped_url|{config.urls.frontend}|g")
470
488
  modified=true
471
489
  fi
472
490
 
@@ -480,28 +498,54 @@ fix_hardcoded_paths() {
480
498
  fi
481
499
 
482
500
  # Replace common localhost patterns if no config URLs set
501
+ # Note: Use # as delimiter since | appears in regex alternation
483
502
  if [[ -z "$backend_url" ]]; then
484
503
  # Common backend ports: 8000, 8001, 8080, 3001, 4000, 5000
485
504
  if echo "$prd_content" | grep -qE 'http://localhost:(8000|8001|8080|3001|4000|5000)' ; then
486
505
  echo " Replacing hardcoded localhost backend URLs with {config.urls.backend}..."
487
- prd_content=$(echo "$prd_content" | sed -E 's|http://localhost:(8000|8001|8080|3001|4000|5000)|{config.urls.backend}|g')
506
+ prd_content=$(echo "$prd_content" | sed -E 's#http://localhost:(8000|8001|8080|3001|4000|5000)#{config.urls.backend}#g')
488
507
  modified=true
489
508
  fi
490
509
  fi
491
510
 
492
511
  if [[ -z "$frontend_url" ]]; then
493
- # Common frontend ports: 3000, 5173, 4200, 8080
512
+ # Common frontend ports: 3000, 5173, 4200
494
513
  if echo "$prd_content" | grep -qE 'http://localhost:(3000|5173|4200)' ; then
495
514
  echo " Replacing hardcoded localhost frontend URLs with {config.urls.frontend}..."
496
- prd_content=$(echo "$prd_content" | sed -E 's|http://localhost:(3000|5173|4200)|{config.urls.frontend}|g')
515
+ prd_content=$(echo "$prd_content" | sed -E 's#http://localhost:(3000|5173|4200)#{config.urls.frontend}#g')
497
516
  modified=true
498
517
  fi
499
518
  fi
500
519
 
501
- # Write back if modified
520
+ # Write back if modified, but only if content is still valid
502
521
  if [[ "$modified" == "true" ]]; then
522
+ # Safety check: don't write empty or drastically smaller content
523
+ if [[ -z "$prd_content" ]]; then
524
+ print_error "Path replacement resulted in empty content - aborting write"
525
+ return 1
526
+ fi
527
+
528
+ # Validate the result is still valid JSON with stories
529
+ if ! echo "$prd_content" | jq -e '.stories' >/dev/null 2>&1; then
530
+ print_error "Path replacement produced invalid JSON - aborting write"
531
+ return 1
532
+ fi
533
+
534
+ local orig_len=${#original_content}
535
+ local new_len=${#prd_content}
536
+ if [[ $new_len -lt $((orig_len / 2)) ]]; then
537
+ print_error "Path replacement lost too much content ($orig_len -> $new_len bytes) - aborting write"
538
+ return 1
539
+ fi
540
+
541
+ # Create backup before writing
542
+ cp "$prd_file" "${prd_file}.pre-fix.bak"
543
+
503
544
  echo "$prd_content" > "$prd_file"
504
545
  print_success "Paths updated to use config placeholders"
546
+
547
+ # Remove backup on success
548
+ rm -f "${prd_file}.pre-fix.bak"
505
549
  fi
506
550
  }
507
551
 
@@ -29,8 +29,21 @@ run_api_smoke_test() {
29
29
  local endpoints_tested=0
30
30
 
31
31
  # 1. Health endpoint (most important)
32
+ # Check if explicitly disabled (empty string in config)
33
+ local health_endpoint_raw
34
+ health_endpoint_raw=$(jq -r '.api.healthEndpoint' "$RALPH_DIR/config.json" 2>/dev/null)
35
+
32
36
  local health_endpoint
33
- health_endpoint=$(get_config '.api.healthEndpoint' "/health")
37
+ if [[ "$health_endpoint_raw" == "" ]]; then
38
+ # Explicitly disabled
39
+ health_endpoint=""
40
+ elif [[ "$health_endpoint_raw" == "null" ]]; then
41
+ # Not configured, use default
42
+ health_endpoint="/health"
43
+ else
44
+ health_endpoint="$health_endpoint_raw"
45
+ fi
46
+
34
47
  if [[ -n "$health_endpoint" ]]; then
35
48
  if ! _smoke_test_endpoint "$base_url" "$health_endpoint" "health"; then
36
49
  failed=1
@@ -484,6 +484,82 @@ run_fastapi_response_check() {
484
484
  return $failed
485
485
  }
486
486
 
487
+ # Verify naming conventions (snake_case vs camelCase)
488
+ # Uses vibe-check to catch common naming issues in TypeScript
489
+ verify_naming_conventions() {
490
+ local story_type="${1:-general}"
491
+ local naming_log="$RALPH_DIR/last_naming_failure.log"
492
+
493
+ # Skip for backend-only stories (Python uses snake_case correctly)
494
+ if [[ "$story_type" == "backend" ]]; then
495
+ echo " Naming conventions... skipped (backend story)"
496
+ return 0
497
+ fi
498
+
499
+ # Skip if no TypeScript files or vibe-check not available
500
+ if [[ ! -f "tsconfig.json" ]] && [[ ! -f "package.json" ]]; then
501
+ return 0
502
+ fi
503
+
504
+ # Check if vibe-check is available (it's part of agentic-loop)
505
+ local vibe_check_cmd=""
506
+ if command -v vibe-check &>/dev/null; then
507
+ vibe_check_cmd="vibe-check"
508
+ elif command -v npx &>/dev/null && [[ -f "node_modules/.bin/vibe-check" ]]; then
509
+ vibe_check_cmd="npx vibe-check"
510
+ elif [[ -n "${RALPH_LIB:-}" ]] && [[ -f "${RALPH_LIB}/../bin/vibe-check" ]]; then
511
+ vibe_check_cmd="${RALPH_LIB}/../bin/vibe-check"
512
+ else
513
+ # vibe-check not available, skip silently
514
+ return 0
515
+ fi
516
+
517
+ # Clear previous failure log
518
+ rm -f "$naming_log"
519
+
520
+ echo -n " Naming conventions (snake_case)... "
521
+
522
+ # Run vibe-check with snake-case check only
523
+ # Use --fail-on warning to catch all snake_case issues
524
+ local check_output
525
+
526
+ # Build array of directories to check (handles spaces/newlines in paths)
527
+ local -a check_dirs_arr=(".")
528
+ local fe_dirs
529
+ fe_dirs=$(get_frontend_dirs 2>/dev/null || echo "")
530
+ if [[ -n "$fe_dirs" ]]; then
531
+ check_dirs_arr=() # Clear default, use frontend dirs instead
532
+ while IFS= read -r dir; do
533
+ [[ -n "$dir" ]] && check_dirs_arr+=("$dir")
534
+ done <<< "$fe_dirs"
535
+ fi
536
+
537
+ if check_output=$("$vibe_check_cmd" "${check_dirs_arr[@]}" --only snake-case --fail-on warning --format compact 2>&1); then
538
+ print_success "passed"
539
+ return 0
540
+ else
541
+ # Check if there are actual issues or just no files
542
+ if echo "$check_output" | grep -qE "snake_case|snake-case"; then
543
+ print_error "failed"
544
+ echo ""
545
+ echo " Naming convention issues (use camelCase in TypeScript):"
546
+ echo "$check_output" | head -"$MAX_LINT_ERROR_LINES" | sed 's/^/ /'
547
+ {
548
+ echo "Naming convention errors:"
549
+ echo "$check_output"
550
+ echo ""
551
+ echo "Fix: Use camelCase for TypeScript properties and variables."
552
+ echo " snake_case is only acceptable when mapping external API responses."
553
+ } > "$naming_log"
554
+ return 1
555
+ else
556
+ # No snake_case issues found, might be other error
557
+ print_success "passed"
558
+ return 0
559
+ fi
560
+ fi
561
+ }
562
+
487
563
  # Check if a verification step is enabled in config
488
564
  # Values: true, false, "final" (only on last story)
489
565
  check_enabled() {
@@ -555,6 +631,14 @@ run_configured_checks() {
555
631
  run_fastapi_response_check
556
632
  fi
557
633
 
634
+ # Naming convention check (snake_case vs camelCase)
635
+ # Enabled by default for TypeScript projects
636
+ if check_enabled "naming" "true"; then
637
+ if ! verify_naming_conventions "$story_type"; then
638
+ return 1
639
+ fi
640
+ fi
641
+
558
642
  # Run pre-commit hooks if available (catches errors before commit attempt)
559
643
  run_precommit_hooks
560
644
 
@@ -23,7 +23,7 @@
23
23
  },
24
24
  {
25
25
  "id": "sign-004",
26
- "pattern": "Use camelCase for TypeScript/JavaScript variables, functions, and API response fields - convert snake_case from backend to camelCase in frontend",
26
+ "pattern": "ALWAYS transform API responses from snake_case to camelCase at the boundary. Create a transform function: `const user = { userName: data.user_name, createdAt: data.created_at }`. NEVER use snake_case properties directly in TypeScript code like `data.user_name` - always map to camelCase first.",
27
27
  "category": "frontend",
28
28
  "learnedFrom": null,
29
29
  "createdAt": "2026-01-20T00:00:00-08:00"