agentic-loop 3.18.2 → 3.21.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/.claude/commands/tour.md +11 -7
- package/.claude/commands/vibe-help.md +5 -2
- package/.claude/commands/vibe-list.md +17 -2
- package/.claude/skills/prd/SKILL.md +21 -6
- package/.claude/skills/setup-review/SKILL.md +56 -0
- package/.claude/skills/tour/SKILL.md +11 -7
- package/.claude/skills/vibe-help/SKILL.md +2 -1
- package/.claude/skills/vibe-list/SKILL.md +5 -2
- package/.pre-commit-hooks.yaml +8 -0
- package/README.md +4 -0
- package/bin/agentic-loop.sh +7 -0
- package/bin/ralph.sh +35 -0
- package/dist/checks/check-signs-secrets.d.ts +9 -0
- package/dist/checks/check-signs-secrets.d.ts.map +1 -0
- package/dist/checks/check-signs-secrets.js +57 -0
- package/dist/checks/check-signs-secrets.js.map +1 -0
- package/dist/checks/index.d.ts +2 -5
- package/dist/checks/index.d.ts.map +1 -1
- package/dist/checks/index.js +4 -9
- package/dist/checks/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/ralph/hooks/common.sh +47 -0
- package/ralph/hooks/warn-debug.sh +12 -26
- package/ralph/hooks/warn-empty-catch.sh +21 -34
- package/ralph/hooks/warn-secrets.sh +39 -52
- package/ralph/hooks/warn-urls.sh +25 -45
- package/ralph/init.sh +60 -82
- package/ralph/loop.sh +533 -53
- package/ralph/prd-check.sh +177 -236
- package/ralph/prd.sh +5 -2
- package/ralph/setup/quick-setup.sh +2 -16
- package/ralph/setup.sh +68 -80
- package/ralph/signs.sh +8 -0
- package/ralph/uat.sh +2015 -0
- package/ralph/utils.sh +198 -69
- package/ralph/verify/tests.sh +65 -10
- package/templates/PROMPT.md +10 -4
- package/templates/UAT-PROMPT.md +197 -0
- package/templates/config/elixir.json +0 -2
- package/templates/config/fastmcp.json +0 -2
- package/templates/config/fullstack.json +2 -4
- package/templates/config/go.json +0 -2
- package/templates/config/minimal.json +0 -2
- package/templates/config/node.json +0 -2
- package/templates/config/python.json +0 -2
- package/templates/config/rust.json +0 -2
- package/templates/prd-example.json +6 -8
package/ralph/prd-check.sh
CHANGED
|
@@ -197,31 +197,19 @@ validate_prd() {
|
|
|
197
197
|
fi
|
|
198
198
|
fi
|
|
199
199
|
|
|
200
|
-
#
|
|
200
|
+
# Auto-remove deprecated root-level fields (no longer used, safe to strip)
|
|
201
201
|
local deprecated_fields=""
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if jq -e '.originalContext' "$prd_file" >/dev/null 2>&1; then
|
|
209
|
-
deprecated_fields+="originalContext "
|
|
210
|
-
fi
|
|
211
|
-
if jq -e '.testing' "$prd_file" >/dev/null 2>&1; then
|
|
212
|
-
deprecated_fields+="testing "
|
|
213
|
-
fi
|
|
214
|
-
if jq -e '.architecture' "$prd_file" >/dev/null 2>&1; then
|
|
215
|
-
deprecated_fields+="architecture "
|
|
216
|
-
fi
|
|
217
|
-
if jq -e '.testUsers' "$prd_file" >/dev/null 2>&1; then
|
|
218
|
-
deprecated_fields+="testUsers "
|
|
219
|
-
fi
|
|
202
|
+
local deprecated_keys=("techStack" "globalConstraints" "originalContext" "testing" "architecture" "testUsers")
|
|
203
|
+
for key in "${deprecated_keys[@]}"; do
|
|
204
|
+
if jq -e ".$key" "$prd_file" >/dev/null 2>&1; then
|
|
205
|
+
deprecated_fields+="$key "
|
|
206
|
+
fi
|
|
207
|
+
done
|
|
220
208
|
if [[ -n "$deprecated_fields" ]]; then
|
|
221
209
|
echo ""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
210
|
+
print_info "Removing deprecated root-level fields: $deprecated_fields"
|
|
211
|
+
update_json "$prd_file" \
|
|
212
|
+
'del(.techStack, .globalConstraints, .originalContext, .testing, .architecture, .testUsers)'
|
|
225
213
|
fi
|
|
226
214
|
|
|
227
215
|
# Validate API smoke test configuration in background (skip in fast/cached mode)
|
|
@@ -233,11 +221,23 @@ validate_prd() {
|
|
|
233
221
|
api_check_pid=$!
|
|
234
222
|
fi
|
|
235
223
|
|
|
224
|
+
# Validate batch assignments (warn, don't block)
|
|
225
|
+
local batch_errors batch_rc=0
|
|
226
|
+
batch_errors=$(validate_batch_assignments "$prd_file" 2>/dev/null) || batch_rc=$?
|
|
227
|
+
if [[ $batch_rc -ne 0 && -n "$batch_errors" ]]; then
|
|
228
|
+
echo ""
|
|
229
|
+
print_warning "Batch assignment issues:"
|
|
230
|
+
echo "$batch_errors" | while IFS= read -r line; do
|
|
231
|
+
[[ -n "$line" ]] && echo " $line"
|
|
232
|
+
done
|
|
233
|
+
echo ""
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
236
|
# Replace hardcoded paths with config placeholders
|
|
237
237
|
fix_hardcoded_paths "$prd_file" "$config"
|
|
238
238
|
|
|
239
239
|
# Validate and fix individual stories
|
|
240
|
-
# dry_run flag — when "true", skip auto-fix
|
|
240
|
+
# dry_run flag — when "true", report issues but skip auto-fix
|
|
241
241
|
_validate_and_fix_stories "$prd_file" "$dry_run" || return 1
|
|
242
242
|
|
|
243
243
|
# Wait for background API health check and print its output
|
|
@@ -326,6 +326,107 @@ _validate_api_config() {
|
|
|
326
326
|
return 0
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
# Check a single story for common issues
|
|
330
|
+
# Outputs issue strings to stdout (one per line: "issue description")
|
|
331
|
+
# $1: story_id $2: prd_file
|
|
332
|
+
_check_story_issues() {
|
|
333
|
+
local story_id="$1"
|
|
334
|
+
local prd_file="$2"
|
|
335
|
+
|
|
336
|
+
local story_type
|
|
337
|
+
story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
|
|
338
|
+
local story_title
|
|
339
|
+
story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
|
|
340
|
+
local test_steps
|
|
341
|
+
test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
|
|
342
|
+
|
|
343
|
+
# Backend must have curl tests
|
|
344
|
+
if [[ "$story_type" == "backend" ]] && [[ -n "$test_steps" ]] && ! echo "$test_steps" | grep -q "curl "; then
|
|
345
|
+
echo "backend needs curl tests"
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
# Frontend must have tsc or playwright
|
|
349
|
+
if [[ "$story_type" == "frontend" ]] && [[ -n "$test_steps" ]] && ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
|
|
350
|
+
echo "frontend needs tsc/playwright tests"
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
# Backend needs apiContract
|
|
354
|
+
if [[ "$story_type" == "backend" ]]; then
|
|
355
|
+
local has_contract
|
|
356
|
+
has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
|
|
357
|
+
if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
|
|
358
|
+
echo "missing apiContract"
|
|
359
|
+
fi
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
# Frontend needs testUrl, contextFiles, and mcp
|
|
363
|
+
if [[ "$story_type" == "frontend" ]]; then
|
|
364
|
+
local has_url
|
|
365
|
+
has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
|
|
366
|
+
[[ -z "$has_url" || "$has_url" == "null" ]] && echo "missing testUrl"
|
|
367
|
+
|
|
368
|
+
local context_files
|
|
369
|
+
context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
|
|
370
|
+
[[ "$context_files" == "0" ]] && echo "missing contextFiles"
|
|
371
|
+
|
|
372
|
+
local mcp_tools
|
|
373
|
+
mcp_tools=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .mcp // [] | length' "$prd_file")
|
|
374
|
+
[[ "$mcp_tools" == "0" ]] && echo "missing mcp (browser tools)"
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
# Auth stories need security criteria
|
|
378
|
+
if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
|
|
379
|
+
local criteria
|
|
380
|
+
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
381
|
+
if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
|
|
382
|
+
echo "missing security criteria"
|
|
383
|
+
fi
|
|
384
|
+
fi
|
|
385
|
+
|
|
386
|
+
# List endpoints need pagination criteria
|
|
387
|
+
if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
|
|
388
|
+
local criteria
|
|
389
|
+
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
390
|
+
if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
|
|
391
|
+
echo "missing pagination criteria"
|
|
392
|
+
fi
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
# API consumer needs camelCase transformation note
|
|
396
|
+
if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
|
|
397
|
+
local story_desc
|
|
398
|
+
story_desc=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.title + " " + (.acceptanceCriteria // [] | join(" ")) + " " + (.notes // ""))' "$prd_file")
|
|
399
|
+
if echo "$story_desc" | grep -qiE "(api|fetch|axios|endpoint|backend|response)"; then
|
|
400
|
+
local story_notes
|
|
401
|
+
story_notes=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .notes // ""' "$prd_file")
|
|
402
|
+
if ! echo "$story_notes" | grep -qiE "(camelCase|snake_case|naming)"; then
|
|
403
|
+
echo "API consumer needs camelCase transformation note"
|
|
404
|
+
fi
|
|
405
|
+
fi
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
# All testSteps are server-dependent
|
|
409
|
+
if [[ -n "$test_steps" ]]; then
|
|
410
|
+
local has_offline=false has_server=false
|
|
411
|
+
local step_list
|
|
412
|
+
step_list=$(jq -r --arg id "$story_id" \
|
|
413
|
+
'.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
|
|
414
|
+
|
|
415
|
+
while IFS= read -r single_step; do
|
|
416
|
+
[[ -z "$single_step" ]] && continue
|
|
417
|
+
if echo "$single_step" | grep -qE "^(curl |wget |http )"; then
|
|
418
|
+
has_server=true
|
|
419
|
+
else
|
|
420
|
+
has_offline=true
|
|
421
|
+
fi
|
|
422
|
+
done <<< "$step_list"
|
|
423
|
+
|
|
424
|
+
if [[ "$has_server" == "true" && "$has_offline" == "false" ]]; then
|
|
425
|
+
echo "all testSteps need a live server (add offline test: npm test, tsc --noEmit, pytest)"
|
|
426
|
+
fi
|
|
427
|
+
fi
|
|
428
|
+
}
|
|
429
|
+
|
|
329
430
|
# Validate individual stories and auto-fix with Claude if needed
|
|
330
431
|
# $1: prd_file $2: optional "dry_run" — when "true", report issues but skip auto-fix
|
|
331
432
|
_validate_and_fix_stories() {
|
|
@@ -339,7 +440,7 @@ _validate_and_fix_stories() {
|
|
|
339
440
|
local cnt_no_tests=0 cnt_backend_curl=0 cnt_backend_contract=0
|
|
340
441
|
local cnt_frontend_tsc=0 cnt_frontend_url=0 cnt_frontend_context=0 cnt_frontend_mcp=0
|
|
341
442
|
local cnt_auth_security=0 cnt_list_pagination=0 cnt_prose_steps=0
|
|
342
|
-
local cnt_naming_convention=0 cnt_bare_pytest=0
|
|
443
|
+
local cnt_naming_convention=0 cnt_bare_pytest=0 cnt_bare_python=0
|
|
343
444
|
local cnt_server_only=0
|
|
344
445
|
local cnt_custom=0
|
|
345
446
|
|
|
@@ -356,156 +457,64 @@ _validate_and_fix_stories() {
|
|
|
356
457
|
[[ -z "$story_id" ]] && continue
|
|
357
458
|
|
|
358
459
|
local story_issues=""
|
|
359
|
-
local story_type
|
|
360
|
-
story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
|
|
361
|
-
local story_title
|
|
362
|
-
story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
|
|
363
|
-
|
|
364
|
-
# Check 1: testSteps quality
|
|
365
460
|
local test_steps
|
|
366
461
|
test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
|
|
367
462
|
|
|
463
|
+
# Checks unique to full validation: empty testSteps, prose detection, bare pytest
|
|
368
464
|
if [[ -z "$test_steps" ]]; then
|
|
369
465
|
story_issues+="no testSteps, "
|
|
370
466
|
cnt_no_tests=$((cnt_no_tests + 1))
|
|
371
467
|
else
|
|
372
|
-
# Check test steps are executable commands, not prose
|
|
373
|
-
# Good: "curl -s POST /api/login | jq -e '.token'"
|
|
374
|
-
# Bad: "Verify the user can log in successfully"
|
|
375
468
|
if ! echo "$test_steps" | grep -qE "(curl |npm |pytest|go test|cargo test|mix test|rails test|bundle exec|python |node |sh |bash |\| jq)"; then
|
|
376
469
|
story_issues+="testSteps look like prose (need executable commands), "
|
|
377
470
|
cnt_prose_steps=$((cnt_prose_steps + 1))
|
|
378
471
|
fi
|
|
379
472
|
|
|
380
|
-
# Type-specific checks
|
|
381
|
-
if [[ "$story_type" == "backend" ]]; then
|
|
382
|
-
# Backend must have curl, not just npm test/pytest
|
|
383
|
-
if ! echo "$test_steps" | grep -q "curl "; then
|
|
384
|
-
story_issues+="backend needs curl tests, "
|
|
385
|
-
cnt_backend_curl=$((cnt_backend_curl + 1))
|
|
386
|
-
fi
|
|
387
|
-
elif [[ "$story_type" == "frontend" ]]; then
|
|
388
|
-
# Frontend must have tsc or playwright
|
|
389
|
-
if ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
|
|
390
|
-
story_issues+="frontend needs tsc/playwright tests, "
|
|
391
|
-
cnt_frontend_tsc=$((cnt_frontend_tsc + 1))
|
|
392
|
-
fi
|
|
393
|
-
fi
|
|
394
|
-
|
|
395
|
-
# Check for bare pytest/python in projects using uv/poetry/pipenv
|
|
396
473
|
local py_runner
|
|
397
474
|
py_runner=$(detect_python_runner ".")
|
|
398
475
|
if [[ -n "$py_runner" ]]; then
|
|
399
|
-
# Project uses a Python runner - check for bare pytest/python commands
|
|
400
|
-
# Match: "pytest " at start or after space/semicolon, but not preceded by "run "
|
|
401
476
|
if echo "$test_steps" | grep -qE '(^|[; ])pytest ' && ! echo "$test_steps" | grep -qE "(uv run|poetry run|pipenv run) pytest"; then
|
|
402
477
|
story_issues+="use '$py_runner pytest' not bare 'pytest', "
|
|
403
478
|
cnt_bare_pytest=$((cnt_bare_pytest + 1))
|
|
404
479
|
fi
|
|
405
480
|
fi
|
|
406
|
-
fi
|
|
407
|
-
|
|
408
|
-
# Check 2: Backend needs apiContract
|
|
409
|
-
if [[ "$story_type" == "backend" ]]; then
|
|
410
|
-
local has_contract
|
|
411
|
-
has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
|
|
412
|
-
if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
|
|
413
|
-
story_issues+="missing apiContract, "
|
|
414
|
-
cnt_backend_contract=$((cnt_backend_contract + 1))
|
|
415
|
-
fi
|
|
416
|
-
fi
|
|
417
|
-
|
|
418
|
-
# Check 3: Frontend needs testUrl, contextFiles, and mcp
|
|
419
|
-
if [[ "$story_type" == "frontend" ]]; then
|
|
420
|
-
local has_url
|
|
421
|
-
has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
|
|
422
|
-
if [[ -z "$has_url" || "$has_url" == "null" ]]; then
|
|
423
|
-
story_issues+="missing testUrl, "
|
|
424
|
-
cnt_frontend_url=$((cnt_frontend_url + 1))
|
|
425
|
-
fi
|
|
426
|
-
|
|
427
|
-
local context_files
|
|
428
|
-
context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
|
|
429
|
-
if [[ "$context_files" == "0" ]]; then
|
|
430
|
-
story_issues+="missing contextFiles, "
|
|
431
|
-
cnt_frontend_context=$((cnt_frontend_context + 1))
|
|
432
|
-
fi
|
|
433
481
|
|
|
434
|
-
#
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
story_issues+="missing mcp (browser tools), "
|
|
439
|
-
cnt_frontend_mcp=$((cnt_frontend_mcp + 1))
|
|
440
|
-
fi
|
|
441
|
-
fi
|
|
442
|
-
|
|
443
|
-
# Check 4: Auth stories need security criteria
|
|
444
|
-
if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
|
|
445
|
-
local criteria
|
|
446
|
-
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
447
|
-
if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
|
|
448
|
-
story_issues+="missing security criteria, "
|
|
449
|
-
cnt_auth_security=$((cnt_auth_security + 1))
|
|
450
|
-
fi
|
|
451
|
-
fi
|
|
452
|
-
|
|
453
|
-
# Check 5: List endpoints need scale criteria
|
|
454
|
-
# Note: "search" excluded - search endpoints often return single/filtered results
|
|
455
|
-
if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
|
|
456
|
-
local criteria
|
|
457
|
-
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
458
|
-
if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
|
|
459
|
-
story_issues+="missing pagination criteria, "
|
|
460
|
-
cnt_list_pagination=$((cnt_list_pagination + 1))
|
|
461
|
-
fi
|
|
462
|
-
fi
|
|
463
|
-
|
|
464
|
-
# Check 7: Frontend stories consuming APIs need naming convention notes
|
|
465
|
-
# If story is frontend/general AND mentions API/fetch/axios, ensure notes include camelCase guidance
|
|
466
|
-
if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
|
|
467
|
-
local story_desc
|
|
468
|
-
story_desc=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.title + " " + (.acceptanceCriteria // [] | join(" ")) + " " + (.notes // ""))' "$prd_file")
|
|
469
|
-
if echo "$story_desc" | grep -qiE "(api|fetch|axios|endpoint|backend|response)"; then
|
|
470
|
-
local story_notes
|
|
471
|
-
story_notes=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .notes // ""' "$prd_file")
|
|
472
|
-
if ! echo "$story_notes" | grep -qiE "(camelCase|snake_case|naming)"; then
|
|
473
|
-
story_issues+="API consumer needs camelCase transformation note, "
|
|
474
|
-
cnt_naming_convention=$((cnt_naming_convention + 1))
|
|
475
|
-
fi
|
|
476
|
-
fi
|
|
477
|
-
fi
|
|
478
|
-
|
|
479
|
-
# Check 9: Stories where ALL testSteps depend on a live server
|
|
480
|
-
# If every testStep is a curl/wget/httpie command and none are offline
|
|
481
|
-
# (npm test, pytest, tsc, playwright, cargo test, go test, etc.),
|
|
482
|
-
# the story will always fail without a running server.
|
|
483
|
-
if [[ -n "$test_steps" ]]; then
|
|
484
|
-
local has_offline_step=false
|
|
485
|
-
local has_server_step=false
|
|
486
|
-
local step_list
|
|
487
|
-
step_list=$(jq -r --arg id "$story_id" \
|
|
488
|
-
'.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
|
|
489
|
-
|
|
490
|
-
while IFS= read -r single_step; do
|
|
491
|
-
[[ -z "$single_step" ]] && continue
|
|
492
|
-
if echo "$single_step" | grep -qE "^(curl |wget |http )"; then
|
|
493
|
-
has_server_step=true
|
|
482
|
+
# Check for bare 'python' (fails on macOS which only has python3)
|
|
483
|
+
if echo "$test_steps" | grep -qE '(^|[;&| ])python ' && ! echo "$test_steps" | grep -qE "(uv run|poetry run|pipenv run|python3) "; then
|
|
484
|
+
if [[ -n "$py_runner" ]]; then
|
|
485
|
+
story_issues+="use '$py_runner python' not bare 'python', "
|
|
494
486
|
else
|
|
495
|
-
|
|
487
|
+
story_issues+="use 'python3' not bare 'python' (macOS has no 'python'), "
|
|
496
488
|
fi
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if [[ "$has_server_step" == "true" && "$has_offline_step" == "false" ]]; then
|
|
500
|
-
story_issues+="all testSteps need a live server (add offline test: npm test, tsc --noEmit, pytest), "
|
|
501
|
-
cnt_server_only=$((cnt_server_only + 1))
|
|
489
|
+
cnt_bare_python=$((cnt_bare_python + 1))
|
|
502
490
|
fi
|
|
503
491
|
fi
|
|
504
492
|
|
|
493
|
+
# Shared checks (same as validate_stories_quick)
|
|
494
|
+
local shared_issues
|
|
495
|
+
shared_issues=$(_check_story_issues "$story_id" "$prd_file")
|
|
496
|
+
while IFS= read -r issue; do
|
|
497
|
+
[[ -z "$issue" ]] && continue
|
|
498
|
+
story_issues+="$issue, "
|
|
499
|
+
# Count by category for summary display
|
|
500
|
+
case "$issue" in
|
|
501
|
+
"backend needs curl tests") cnt_backend_curl=$((cnt_backend_curl + 1)) ;;
|
|
502
|
+
"frontend needs tsc/playwright tests") cnt_frontend_tsc=$((cnt_frontend_tsc + 1)) ;;
|
|
503
|
+
"missing apiContract") cnt_backend_contract=$((cnt_backend_contract + 1)) ;;
|
|
504
|
+
"missing testUrl") cnt_frontend_url=$((cnt_frontend_url + 1)) ;;
|
|
505
|
+
"missing contextFiles") cnt_frontend_context=$((cnt_frontend_context + 1)) ;;
|
|
506
|
+
"missing mcp (browser tools)") cnt_frontend_mcp=$((cnt_frontend_mcp + 1)) ;;
|
|
507
|
+
"missing security criteria") cnt_auth_security=$((cnt_auth_security + 1)) ;;
|
|
508
|
+
"missing pagination criteria") cnt_list_pagination=$((cnt_list_pagination + 1)) ;;
|
|
509
|
+
"API consumer needs camelCase transformation note") cnt_naming_convention=$((cnt_naming_convention + 1)) ;;
|
|
510
|
+
"all testSteps need a live server"*) cnt_server_only=$((cnt_server_only + 1)) ;;
|
|
511
|
+
esac
|
|
512
|
+
done <<< "$shared_issues"
|
|
513
|
+
|
|
505
514
|
# Snapshot built-in issues before custom checks append
|
|
506
515
|
local builtin_story_issues="$story_issues"
|
|
507
516
|
|
|
508
|
-
#
|
|
517
|
+
# User-defined custom checks (.ralph/checks/prd/)
|
|
509
518
|
if [[ -d ".ralph/checks/prd" ]]; then
|
|
510
519
|
local story_json
|
|
511
520
|
story_json=$(jq --arg id "$story_id" '.stories[] | select(.id==$id)' "$prd_file")
|
|
@@ -546,7 +555,8 @@ _validate_and_fix_stories() {
|
|
|
546
555
|
[[ $cnt_auth_security -gt 0 ]] && echo " ${cnt_auth_security}x auth: add security criteria"
|
|
547
556
|
[[ $cnt_list_pagination -gt 0 ]] && echo " ${cnt_list_pagination}x list: add pagination"
|
|
548
557
|
[[ $cnt_naming_convention -gt 0 ]] && echo " ${cnt_naming_convention}x API consumer: add camelCase transformation note"
|
|
549
|
-
[[ $cnt_bare_pytest -gt 0 ]] && echo " ${cnt_bare_pytest}x use '
|
|
558
|
+
[[ $cnt_bare_pytest -gt 0 ]] && echo " ${cnt_bare_pytest}x use '${py_runner:-python3} pytest' not bare 'pytest'"
|
|
559
|
+
[[ $cnt_bare_python -gt 0 ]] && echo " ${cnt_bare_python}x use 'python3' not bare 'python' (macOS compatibility)"
|
|
550
560
|
[[ $cnt_server_only -gt 0 ]] && echo " ${cnt_server_only}x all testSteps need live server (add offline fallback)"
|
|
551
561
|
[[ $cnt_custom -gt 0 ]] && echo " ${cnt_custom} stories with custom check issues"
|
|
552
562
|
|
|
@@ -662,8 +672,8 @@ _group_issues_by_story() {
|
|
|
662
672
|
}
|
|
663
673
|
|
|
664
674
|
# Apply instant mechanical fixes using jq (no LLM needed)
|
|
665
|
-
# Fixes: missing mcp, bare pytest,
|
|
666
|
-
# server-only testSteps
|
|
675
|
+
# Fixes: missing mcp, bare pytest, bare python (macOS compat), missing camelCase note,
|
|
676
|
+
# missing migration prerequisites, server-only testSteps
|
|
667
677
|
_apply_mechanical_fixes() {
|
|
668
678
|
local prd_file="$1"
|
|
669
679
|
local fixed=0
|
|
@@ -701,6 +711,19 @@ _apply_mechanical_fixes() {
|
|
|
701
711
|
fi
|
|
702
712
|
fi
|
|
703
713
|
|
|
714
|
+
# Fix: Bare python → prefix with runner or replace with python3 (macOS compat)
|
|
715
|
+
local test_steps_for_python
|
|
716
|
+
test_steps_for_python=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join("\n")' "$prd_file")
|
|
717
|
+
if echo "$test_steps_for_python" | grep -qE '(^|[;&| ])python ' && ! echo "$test_steps_for_python" | grep -qE "(uv run|poetry run|pipenv run|python3) "; then
|
|
718
|
+
if [[ -n "$py_runner" ]]; then
|
|
719
|
+
update_json "$prd_file" --arg id "$story_id" --arg runner "$py_runner" \
|
|
720
|
+
'(.stories[] | select(.id==$id) | .testSteps) |= [.[]? | gsub("(?<pre>^|[;&| ])python "; "\(.pre)\($runner) python ")]' && fixed=$((fixed + 1))
|
|
721
|
+
else
|
|
722
|
+
update_json "$prd_file" --arg id "$story_id" \
|
|
723
|
+
'(.stories[] | select(.id==$id) | .testSteps) |= [.[]? | gsub("(?<pre>^|[;&| ])python "; "\(.pre)python3 ")]' && fixed=$((fixed + 1))
|
|
724
|
+
fi
|
|
725
|
+
fi
|
|
726
|
+
|
|
704
727
|
# Fix: Frontend/general API consumer missing camelCase note
|
|
705
728
|
if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
|
|
706
729
|
local story_desc
|
|
@@ -910,100 +933,18 @@ validate_stories_quick() {
|
|
|
910
933
|
local prd_file="$1"
|
|
911
934
|
local issues=""
|
|
912
935
|
|
|
913
|
-
# Only check incomplete stories
|
|
914
936
|
local story_ids
|
|
915
937
|
story_ids=$(jq -r '.stories[] | select(.passes != true) | .id' "$prd_file" 2>/dev/null)
|
|
916
938
|
|
|
917
939
|
while IFS= read -r story_id; do
|
|
918
940
|
[[ -z "$story_id" ]] && continue
|
|
919
941
|
|
|
920
|
-
local
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
# Check 1: testSteps quality
|
|
928
|
-
if [[ "$story_type" == "backend" ]] && ! echo "$test_steps" | grep -q "curl "; then
|
|
929
|
-
issues+="$story_id: missing curl tests, "
|
|
930
|
-
fi
|
|
931
|
-
if [[ "$story_type" == "frontend" ]] && ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
|
|
932
|
-
issues+="$story_id: missing tsc/playwright tests, "
|
|
933
|
-
fi
|
|
934
|
-
|
|
935
|
-
# Check 2: Backend needs apiContract
|
|
936
|
-
if [[ "$story_type" == "backend" ]]; then
|
|
937
|
-
local has_contract
|
|
938
|
-
has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
|
|
939
|
-
if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
|
|
940
|
-
issues+="$story_id: missing apiContract, "
|
|
941
|
-
fi
|
|
942
|
-
fi
|
|
943
|
-
|
|
944
|
-
# Check 3: Frontend needs testUrl, contextFiles, and mcp
|
|
945
|
-
if [[ "$story_type" == "frontend" ]]; then
|
|
946
|
-
local has_url
|
|
947
|
-
has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
|
|
948
|
-
[[ -z "$has_url" || "$has_url" == "null" ]] && issues+="$story_id: missing testUrl, "
|
|
949
|
-
|
|
950
|
-
local context_files
|
|
951
|
-
context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
|
|
952
|
-
[[ "$context_files" == "0" ]] && issues+="$story_id: missing contextFiles, "
|
|
953
|
-
|
|
954
|
-
local mcp_tools
|
|
955
|
-
mcp_tools=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .mcp // [] | length' "$prd_file")
|
|
956
|
-
[[ "$mcp_tools" == "0" ]] && issues+="$story_id: missing mcp, "
|
|
957
|
-
fi
|
|
958
|
-
|
|
959
|
-
# Check 4: Auth stories need security criteria
|
|
960
|
-
if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
|
|
961
|
-
local criteria
|
|
962
|
-
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
963
|
-
if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
|
|
964
|
-
issues+="$story_id: missing security criteria, "
|
|
965
|
-
fi
|
|
966
|
-
fi
|
|
967
|
-
|
|
968
|
-
# Check 5: List endpoints need scale criteria
|
|
969
|
-
if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
|
|
970
|
-
local criteria
|
|
971
|
-
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
972
|
-
if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
|
|
973
|
-
issues+="$story_id: missing pagination criteria, "
|
|
974
|
-
fi
|
|
975
|
-
fi
|
|
976
|
-
|
|
977
|
-
# Check 7: Frontend/general stories consuming APIs need naming convention notes
|
|
978
|
-
if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
|
|
979
|
-
local story_desc
|
|
980
|
-
story_desc=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.title + " " + (.acceptanceCriteria // [] | join(" ")) + " " + (.notes // ""))' "$prd_file")
|
|
981
|
-
if echo "$story_desc" | grep -qiE "(api|fetch|axios|endpoint|backend|response)"; then
|
|
982
|
-
local story_notes
|
|
983
|
-
story_notes=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .notes // ""' "$prd_file")
|
|
984
|
-
if ! echo "$story_notes" | grep -qiE "(camelCase|snake_case|naming)"; then
|
|
985
|
-
issues+="$story_id: needs camelCase transformation note, "
|
|
986
|
-
fi
|
|
987
|
-
fi
|
|
988
|
-
fi
|
|
989
|
-
|
|
990
|
-
# Check 8: All testSteps are server-dependent
|
|
991
|
-
if [[ -n "$test_steps" ]]; then
|
|
992
|
-
local has_offline=false has_server=false
|
|
993
|
-
local steps
|
|
994
|
-
steps=$(jq -r --arg id "$story_id" \
|
|
995
|
-
'.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
|
|
996
|
-
while IFS= read -r s; do
|
|
997
|
-
[[ -z "$s" ]] && continue
|
|
998
|
-
if echo "$s" | grep -qE "^(curl |wget |http )"; then
|
|
999
|
-
has_server=true
|
|
1000
|
-
else
|
|
1001
|
-
has_offline=true
|
|
1002
|
-
fi
|
|
1003
|
-
done <<< "$steps"
|
|
1004
|
-
[[ "$has_server" == "true" && "$has_offline" == "false" ]] && \
|
|
1005
|
-
issues+="$story_id: all testSteps need live server, "
|
|
1006
|
-
fi
|
|
942
|
+
local story_issues
|
|
943
|
+
story_issues=$(_check_story_issues "$story_id" "$prd_file")
|
|
944
|
+
while IFS= read -r issue; do
|
|
945
|
+
[[ -z "$issue" ]] && continue
|
|
946
|
+
issues+="$story_id: $issue, "
|
|
947
|
+
done <<< "$story_issues"
|
|
1007
948
|
done <<< "$story_ids"
|
|
1008
949
|
|
|
1009
950
|
echo "$issues"
|
package/ralph/prd.sh
CHANGED
|
@@ -69,9 +69,10 @@ ralph_prd() {
|
|
|
69
69
|
echo "Claude will ask clarifying questions to refine the PRD."
|
|
70
70
|
echo ""
|
|
71
71
|
|
|
72
|
-
# Get
|
|
72
|
+
# Get frontend URL from config
|
|
73
73
|
local test_url_base
|
|
74
|
-
test_url_base=$(get_config
|
|
74
|
+
test_url_base=$(get_config '.urls.frontend' "")
|
|
75
|
+
[[ -z "$test_url_base" ]] && test_url_base=$(get_config '.playwright.baseUrl' "http://localhost:3000")
|
|
75
76
|
|
|
76
77
|
# Create the PRD generation prompt using printf to avoid heredoc issues
|
|
77
78
|
local prompt_file
|
|
@@ -130,6 +131,8 @@ ralph_prd() {
|
|
|
130
131
|
printf '%s\n' "GOOD: cd frontend && npx tsc --noEmit" >> "$prompt_file"
|
|
131
132
|
printf '%s\n' "GOOD: npx playwright test tests/e2e/dashboard.spec.ts" >> "$prompt_file"
|
|
132
133
|
printf '%s\n' "GOOD: npx playwright test --grep 'login flow'" >> "$prompt_file"
|
|
134
|
+
printf '%s\n' "GOOD: python3 -m pytest tests/ (use python3, NOT python — macOS has no 'python')" >> "$prompt_file"
|
|
135
|
+
printf '%s\n' "GOOD: uv run pytest tests/ (when project uses uv)" >> "$prompt_file"
|
|
133
136
|
printf '\n%s\n' "For UI/visual checks, use Playwright tests that can verify:" >> "$prompt_file"
|
|
134
137
|
printf '%s\n' "- Element visibility and positioning" >> "$prompt_file"
|
|
135
138
|
printf '%s\n' "- Console errors (no errors in DevTools)" >> "$prompt_file"
|
|
@@ -96,22 +96,8 @@ show_completion_with_tour() {
|
|
|
96
96
|
echo ""
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
detect_project_type
|
|
100
|
-
|
|
101
|
-
[[ -d "frontend" && -d "core" ]] && echo "django-react" && return
|
|
102
|
-
# Django alone
|
|
103
|
-
[[ -f "manage.py" ]] && echo "django" && return
|
|
104
|
-
# Rust
|
|
105
|
-
[[ -f "Cargo.toml" ]] && echo "rust" && return
|
|
106
|
-
# Go
|
|
107
|
-
[[ -f "go.mod" ]] && echo "go" && return
|
|
108
|
-
# Python
|
|
109
|
-
[[ -f "pyproject.toml" || -f "requirements.txt" ]] && echo "python" && return
|
|
110
|
-
# Node/TypeScript
|
|
111
|
-
[[ -f "package.json" ]] && echo "node" && return
|
|
112
|
-
# Minimal/unknown
|
|
113
|
-
echo "minimal"
|
|
114
|
-
}
|
|
99
|
+
# Source shared detect_project_type from utils.sh
|
|
100
|
+
source "$VIBE_ROOT/ralph/utils.sh"
|
|
115
101
|
|
|
116
102
|
install_claude_skills() {
|
|
117
103
|
echo -e " ${CYAN}Installing Claude skills...${NC}"
|