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 +44 -42
- package/dist/checks/check-snake-case-ts.d.ts.map +1 -1
- package/dist/checks/check-snake-case-ts.js +28 -0
- package/dist/checks/check-snake-case-ts.js.map +1 -1
- package/package.json +1 -1
- package/ralph/init.sh +24 -0
- package/ralph/loop.sh +15 -0
- package/ralph/prd-check.sh +142 -4
- package/ralph/prd.sh +9 -2
- package/ralph/utils.sh +52 -8
- package/ralph/verify/api.sh +14 -1
- package/ralph/verify/lint.sh +84 -0
- package/templates/signs.json +1 -1
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
**
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
- [
|
|
84
|
-
- [
|
|
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,
|
|
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":"AAAA;;GAEG;AAIH,MAAM,CAAC,MAAM,gBAAgB,GAAS;IACpC,EAAE,EAAE,YAAY;IAChB,IAAI,EAAE,kBAAkB;IACxB,WAAW,EAAE,qEAAqE;IAClF,QAAQ,EAAE,SAAS;IACnB,SAAS,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC;IAEtC,KAAK,CAAC,OAAoB;QACxB,MAAM,OAAO,GAAiB,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAE1C,wDAAwD;QACxD,IAAI,iBAAiB,GAAG,KAAK,CAAC;QAC9B,IAAI,UAAU,GAAG,CAAC,CAAC;QAEnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YAEtB,gBAAgB;YAChB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAChE,SAAS;YACX,CAAC;YAED,iCAAiC;YACjC,IAAI,4CAA4C,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC5D,iBAAiB,GAAG,IAAI,CAAC;gBACzB,UAAU,GAAG,CAAC,CAAC;YACjB,CAAC;YAED,oBAAoB;YACpB,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;YACpD,MAAM,WAAW,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;YAErD,IAAI,iBAAiB,EAAE,CAAC;gBACtB,UAAU,IAAI,UAAU,GAAG,WAAW,CAAC;gBAEvC,kCAAkC;gBAClC,qDAAqD;gBACrD,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAC;gBAE7F,IAAI,aAAa,EAAE,CAAC;oBAClB,MAAM,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;oBACtC,MAAM,aAAa,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC;oBAEhD,OAAO,CAAC,IAAI,CAAC;wBACX,IAAI,EAAE,OAAO;wBACb,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;wBAClC,OAAO,EAAE,aAAa,YAAY,iDAAiD,aAAa,GAAG;wBACnG,QAAQ,EAAE,SAAS;wBACnB,MAAM,EAAE,qBAAqB;wBAC7B,GAAG,EAAE,aAAa;qBACnB,CAAC,CAAC;gBACL,CAAC;gBAED,wBAAwB;gBACxB,IAAI,UAAU,IAAI,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;oBACvC,iBAAiB,GAAG,KAAK,CAAC;gBAC5B,CAAC;YACH,CAAC;YAED,uEAAuE;YACvE,8DAA8D;YAC9D,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;YAClE,IAAI,aAAa,EAAE,CAAC;gBAClB,MAAM,KAAK,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;oBAClD,IAAI,iCAAiC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;wBACrD,OAAO,CAAC,IAAI,CAAC;4BACX,IAAI,EAAE,OAAO;4BACb,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;4BAC9B,OAAO,EAAE,0BAA0B,QAAQ,0DAA0D;4BACrG,QAAQ,EAAE,MAAM;4BAChB,MAAM,EAAE,wBAAwB;yBACjC,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;CACF,CAAC;AAEF,SAAS,WAAW,CAAC,SAAiB;IACpC,OAAO,SAAS,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;AAC7E,CAAC"}
|
|
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
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
|
package/ralph/prd-check.sh
CHANGED
|
@@ -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
|
-
#
|
|
400
|
-
local
|
|
401
|
-
|
|
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
|
-
|
|
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 "$
|
|
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 "
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/ralph/verify/api.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
package/ralph/verify/lint.sh
CHANGED
|
@@ -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
|
|
package/templates/signs.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"id": "sign-004",
|
|
26
|
-
"pattern": "
|
|
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"
|