agentic-loop 3.7.3 → 3.9.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/idea.md +38 -10
- package/.claude/commands/prd.md +39 -18
- package/package.json +1 -1
- package/ralph/init.sh +83 -0
- package/ralph/utils.sh +274 -16
- package/templates/config/elixir.json +6 -0
- package/templates/config/fastmcp.json +6 -0
- package/templates/config/fullstack.json +6 -0
- package/templates/config/go.json +6 -0
- package/templates/config/minimal.json +6 -0
- package/templates/config/node.json +6 -0
- package/templates/config/python.json +6 -0
- package/templates/config/rust.json +6 -0
- package/templates/prd-example.json +76 -13
package/.claude/commands/idea.md
CHANGED
|
@@ -44,13 +44,25 @@ Help the user flesh out the idea through conversation:
|
|
|
44
44
|
|
|
45
45
|
1. **Understand the goal** - What problem does this solve? Who benefits?
|
|
46
46
|
2. **Explore the codebase** - Use Glob/Grep/Read to understand what exists and what patterns to follow
|
|
47
|
-
3. **Ask clarifying questions**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
3. **Ask clarifying questions** about:
|
|
48
|
+
|
|
49
|
+
**Scope & UX:**
|
|
50
|
+
- What's in scope vs out of scope?
|
|
51
|
+
- What does the user see/do? (ask for mockup if UI)
|
|
52
|
+
- What are the edge cases?
|
|
53
|
+
|
|
54
|
+
**Security (IMPORTANT - ask if feature involves):**
|
|
55
|
+
- Authentication: Who can access this? Login required?
|
|
56
|
+
- Passwords: How stored? (must be hashed, never plain text)
|
|
57
|
+
- User input: What validation needed? (SQL injection, XSS, command injection)
|
|
58
|
+
- Sensitive data: What should NEVER be in API responses?
|
|
59
|
+
- Rate limiting: Should this be rate limited? (login attempts, API calls)
|
|
60
|
+
|
|
61
|
+
**Scale (IMPORTANT - ask if feature involves lists/data):**
|
|
62
|
+
- How many items expected? (10s, 1000s, millions?)
|
|
63
|
+
- Pagination needed? What's the max per page?
|
|
64
|
+
- Caching needed? How fresh must data be?
|
|
65
|
+
- Database indexes: What will be queried/sorted frequently?
|
|
54
66
|
|
|
55
67
|
### Step 3: Summarize Before Writing
|
|
56
68
|
|
|
@@ -107,9 +119,25 @@ Once the user confirms, write the idea file:
|
|
|
107
119
|
### Do NOT Create
|
|
108
120
|
- List things that already exist (avoid duplication)
|
|
109
121
|
|
|
110
|
-
##
|
|
111
|
-
-
|
|
112
|
-
-
|
|
122
|
+
## Security Requirements
|
|
123
|
+
- **Authentication**: Who can access? Login required?
|
|
124
|
+
- **Password handling**: Must be hashed with bcrypt (cost 10+), never in responses
|
|
125
|
+
- **Input validation**: What must be validated/sanitized?
|
|
126
|
+
- **Rate limiting**: What should be rate limited?
|
|
127
|
+
- **Sensitive data**: What must NEVER appear in logs/responses?
|
|
128
|
+
|
|
129
|
+
## Scale Requirements
|
|
130
|
+
- **Expected volume**: How many users/items/requests?
|
|
131
|
+
- **Pagination**: Max items per page (recommend 100)
|
|
132
|
+
- **Caching**: What can be cached? For how long?
|
|
133
|
+
- **Database**: What indexes are needed?
|
|
134
|
+
|
|
135
|
+
## UI Mockup (if applicable)
|
|
136
|
+
```
|
|
137
|
+
┌─────────────────────────────────┐
|
|
138
|
+
│ [ASCII mockup of the UI] │
|
|
139
|
+
└─────────────────────────────────┘
|
|
140
|
+
```
|
|
113
141
|
|
|
114
142
|
## Open Questions
|
|
115
143
|
- Any unresolved decisions
|
package/.claude/commands/prd.md
CHANGED
|
@@ -124,29 +124,50 @@ Write the initial PRD to `.ralph/prd.json`:
|
|
|
124
124
|
cat .ralph/prd.json
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
For EACH story,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
For EACH story, check:
|
|
128
|
+
|
|
129
|
+
#### 6a. Testability
|
|
130
|
+
- ❌ `grep -q 'function' file.py` → Only checks code exists, not behavior
|
|
131
|
+
- ❌ `test -f src/component.tsx` → Only checks file exists
|
|
132
|
+
- ❌ `npm test` alone for backend → Mocks can pass without real behavior
|
|
133
|
+
- ✅ `curl ... | jq -e` → Tests actual API response
|
|
134
|
+
- ✅ `npx playwright test` → Real browser tests
|
|
135
|
+
- ✅ `npx tsc --noEmit` → Real type checking
|
|
136
|
+
|
|
137
|
+
#### 6b. Dependencies
|
|
138
|
+
- Can this story's tests pass given prior stories completed?
|
|
139
|
+
- If TASK-003 needs a user, does TASK-001/002 create one?
|
|
140
|
+
|
|
141
|
+
#### 6c. Security (for auth/input stories)
|
|
142
|
+
Does acceptanceCriteria include:
|
|
143
|
+
- Password handling → "Passwords hashed with bcrypt (cost 10+)"
|
|
144
|
+
- Auth responses → "Password/tokens NEVER in response body"
|
|
145
|
+
- User input → "Input sanitized to prevent SQL injection/XSS"
|
|
146
|
+
- Login endpoints → "Rate limited to N attempts per minute"
|
|
147
|
+
- Token expiry → "JWT expires after N hours"
|
|
148
|
+
|
|
149
|
+
#### 6d. Scale (for list/data stories)
|
|
150
|
+
Does acceptanceCriteria include:
|
|
151
|
+
- List endpoints → "Returns paginated results (max 100 per page)"
|
|
152
|
+
- Query params → "Accepts ?page=N&limit=N"
|
|
153
|
+
- Large datasets → "Database query uses index on [column]"
|
|
154
|
+
|
|
155
|
+
#### 6e. Context (for frontend stories)
|
|
156
|
+
- Does `contextFiles` include the idea file (has ASCII mockups)?
|
|
157
|
+
- Does `contextFiles` include styleguide (if exists)?
|
|
158
|
+
- Is `testUrl` set?
|
|
140
159
|
|
|
141
160
|
**Fix any issues you find:**
|
|
142
161
|
|
|
143
162
|
| Problem | Fix |
|
|
144
163
|
|---------|-----|
|
|
145
|
-
| testSteps use grep/test only | Replace with curl,
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
164
|
+
| testSteps use grep/test only | Replace with curl, playwright |
|
|
165
|
+
| Backend story has only `npm test` | Add curl commands that hit real endpoints |
|
|
166
|
+
| Story depends on something not created | Reorder or add missing dependency |
|
|
167
|
+
| Auth story missing security criteria | Add password hashing, rate limiting to acceptanceCriteria |
|
|
168
|
+
| List endpoint missing pagination | Add pagination criteria to acceptanceCriteria |
|
|
169
|
+
| Frontend missing contextFiles | Add idea file + styleguide paths |
|
|
170
|
+
| Frontend missing testUrl | Add URL from config |
|
|
150
171
|
|
|
151
172
|
### Step 7: Reorder if Needed
|
|
152
173
|
|
package/package.json
CHANGED
package/ralph/init.sh
CHANGED
|
@@ -496,6 +496,89 @@ auto_configure_project() {
|
|
|
496
496
|
done
|
|
497
497
|
fi
|
|
498
498
|
|
|
499
|
+
# 7. Detect test directory and patterns
|
|
500
|
+
local test_dir=""
|
|
501
|
+
local test_patterns=""
|
|
502
|
+
|
|
503
|
+
# Check for common test directories
|
|
504
|
+
for dir in "tests" "test" "__tests__" "spec" \
|
|
505
|
+
"src/__tests__" "src/test" \
|
|
506
|
+
"apps/api/tests" "apps/web/tests" \
|
|
507
|
+
"backend/tests" "frontend/tests"; do
|
|
508
|
+
if [[ -d "$dir" ]]; then
|
|
509
|
+
test_dir="$dir"
|
|
510
|
+
break
|
|
511
|
+
fi
|
|
512
|
+
done
|
|
513
|
+
|
|
514
|
+
# If no directory found, check for colocated test files
|
|
515
|
+
if [[ -z "$test_dir" ]]; then
|
|
516
|
+
# Look for test files anywhere (colocated pattern)
|
|
517
|
+
local test_file=""
|
|
518
|
+
test_file=$(find . -type f \( \
|
|
519
|
+
-name "*_test.py" -o -name "test_*.py" -o \
|
|
520
|
+
-name "*.test.ts" -o -name "*.test.js" -o -name "*.test.tsx" -o \
|
|
521
|
+
-name "*.spec.ts" -o -name "*.spec.js" -o \
|
|
522
|
+
-name "*_test.go" -o -name "*_test.rs" \
|
|
523
|
+
\) -not -path "*/node_modules/*" -not -path "*/.venv/*" -not -path "*/venv/*" \
|
|
524
|
+
-not -path "*/.git/*" -not -path "*/dist/*" -not -path "*/build/*" \
|
|
525
|
+
-not -path "*/__pycache__/*" -not -path "*/coverage/*" 2>/dev/null | head -1 || true)
|
|
526
|
+
|
|
527
|
+
if [[ -n "$test_file" ]]; then
|
|
528
|
+
# Tests are colocated with source (e.g., src/component.test.ts)
|
|
529
|
+
test_dir="src"
|
|
530
|
+
[[ ! -d "src" ]] && test_dir="."
|
|
531
|
+
fi
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
# Detect test patterns based on project type (combine for mixed projects)
|
|
535
|
+
test_patterns=""
|
|
536
|
+
if [[ -f "pyproject.toml" || -f "requirements.txt" ]]; then
|
|
537
|
+
test_patterns="*_test.py,test_*.py"
|
|
538
|
+
fi
|
|
539
|
+
if [[ -f "package.json" ]]; then
|
|
540
|
+
[[ -n "$test_patterns" ]] && test_patterns+=","
|
|
541
|
+
test_patterns+="*.test.ts,*.test.tsx,*.test.js,*.spec.ts,*.spec.tsx,*.spec.js"
|
|
542
|
+
fi
|
|
543
|
+
if [[ -f "go.mod" ]]; then
|
|
544
|
+
[[ -n "$test_patterns" ]] && test_patterns+=","
|
|
545
|
+
test_patterns+="*_test.go"
|
|
546
|
+
fi
|
|
547
|
+
if [[ -f "Cargo.toml" ]]; then
|
|
548
|
+
[[ -n "$test_patterns" ]] && test_patterns+=","
|
|
549
|
+
test_patterns+="*_test.rs"
|
|
550
|
+
fi
|
|
551
|
+
if [[ -f "mix.exs" ]]; then
|
|
552
|
+
[[ -n "$test_patterns" ]] && test_patterns+=","
|
|
553
|
+
test_patterns+="*_test.exs"
|
|
554
|
+
fi
|
|
555
|
+
|
|
556
|
+
if [[ -n "$test_dir" ]]; then
|
|
557
|
+
if ! jq -e '.tests.directory' "$tmpfile" >/dev/null 2>&1 || [[ "$(jq -r '.tests.directory' "$tmpfile")" == "" ]]; then
|
|
558
|
+
jq --arg dir "$test_dir" --arg patterns "$test_patterns" \
|
|
559
|
+
'.tests.directory = $dir | .tests.patterns = $patterns' \
|
|
560
|
+
"$tmpfile" > "${tmpfile}.new" && mv "${tmpfile}.new" "$tmpfile"
|
|
561
|
+
echo " Auto-detected tests.directory: $test_dir"
|
|
562
|
+
updated=true
|
|
563
|
+
fi
|
|
564
|
+
else
|
|
565
|
+
# No tests found - check if warning is suppressed
|
|
566
|
+
local require_tests
|
|
567
|
+
require_tests=$(jq -r '.checks.requireTests // true' "$tmpfile" 2>/dev/null)
|
|
568
|
+
|
|
569
|
+
if [[ "$require_tests" == "true" ]]; then
|
|
570
|
+
echo ""
|
|
571
|
+
print_warning "No test directory or test files found."
|
|
572
|
+
echo " Without tests, Ralph can only verify syntax and API responses."
|
|
573
|
+
echo " Import errors and integration issues won't be caught."
|
|
574
|
+
echo ""
|
|
575
|
+
echo " To fix: Add tests, or set in .ralph/config.json:"
|
|
576
|
+
echo " {\"tests\": {\"directory\": \"src\", \"patterns\": \"*.test.ts\"}}"
|
|
577
|
+
echo " To silence: {\"checks\": {\"requireTests\": false}}"
|
|
578
|
+
echo ""
|
|
579
|
+
fi
|
|
580
|
+
fi
|
|
581
|
+
|
|
499
582
|
# Save if updated
|
|
500
583
|
if [[ "$updated" == "true" ]]; then
|
|
501
584
|
mv "$tmpfile" "$config"
|
package/ralph/utils.sh
CHANGED
|
@@ -509,29 +509,287 @@ validate_prd() {
|
|
|
509
509
|
print_warning "PRD is missing feature name (will show as 'unnamed')"
|
|
510
510
|
fi
|
|
511
511
|
|
|
512
|
-
# Check
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
.
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
512
|
+
# Check if project has tests (from config)
|
|
513
|
+
local config="$RALPH_DIR/config.json"
|
|
514
|
+
if [[ -f "$config" ]]; then
|
|
515
|
+
local require_tests
|
|
516
|
+
require_tests=$(jq -r '.checks.requireTests // true' "$config" 2>/dev/null)
|
|
517
|
+
local test_dir
|
|
518
|
+
test_dir=$(jq -r '.tests.directory // empty' "$config" 2>/dev/null)
|
|
519
|
+
|
|
520
|
+
if [[ "$require_tests" == "true" && -z "$test_dir" ]]; then
|
|
521
|
+
echo ""
|
|
522
|
+
print_warning "No test directory configured in .ralph/config.json"
|
|
523
|
+
echo " Without tests, Ralph can only verify syntax and API responses."
|
|
524
|
+
echo " Import errors and integration issues won't be caught."
|
|
525
|
+
echo ""
|
|
526
|
+
echo " To fix: Add tests, or set in .ralph/config.json:"
|
|
527
|
+
echo " {\"tests\": {\"directory\": \"src\", \"patterns\": \"*.test.ts\"}}"
|
|
528
|
+
echo " To silence: {\"checks\": {\"requireTests\": false}}"
|
|
529
|
+
echo ""
|
|
530
|
+
fi
|
|
531
|
+
fi
|
|
532
|
+
|
|
533
|
+
# Validate and fix individual stories
|
|
534
|
+
validate_and_fix_stories "$prd_file" || return 1
|
|
535
|
+
|
|
536
|
+
return 0
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
# Validate individual stories and auto-fix with Claude if needed
|
|
540
|
+
# Checks: testSteps quality, apiContract, testUrl, contextFiles, security, scale
|
|
541
|
+
validate_and_fix_stories() {
|
|
542
|
+
local prd_file="$1"
|
|
543
|
+
local needs_fix=false
|
|
544
|
+
local issues=""
|
|
545
|
+
|
|
546
|
+
echo " Validating story quality..."
|
|
547
|
+
|
|
548
|
+
# Get all story IDs
|
|
549
|
+
local story_ids
|
|
550
|
+
story_ids=$(jq -r '.stories[].id' "$prd_file" 2>/dev/null)
|
|
551
|
+
|
|
552
|
+
while IFS= read -r story_id; do
|
|
553
|
+
[[ -z "$story_id" ]] && continue
|
|
554
|
+
|
|
555
|
+
local story_issues=""
|
|
556
|
+
local story_type
|
|
557
|
+
story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
|
|
558
|
+
local story_title
|
|
559
|
+
story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
|
|
560
|
+
|
|
561
|
+
# Check 1: testSteps quality
|
|
562
|
+
local test_steps
|
|
563
|
+
test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
|
|
564
|
+
|
|
565
|
+
if [[ -z "$test_steps" ]]; then
|
|
566
|
+
story_issues+="no testSteps, "
|
|
567
|
+
elif [[ "$story_type" == "backend" ]]; then
|
|
568
|
+
# Backend must have curl, not just npm test/pytest
|
|
569
|
+
if ! echo "$test_steps" | grep -q "curl "; then
|
|
570
|
+
story_issues+="backend needs curl tests (npm test alone uses mocks), "
|
|
571
|
+
fi
|
|
572
|
+
elif [[ "$story_type" == "frontend" ]]; then
|
|
573
|
+
# Frontend must have tsc or playwright
|
|
574
|
+
if ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
|
|
575
|
+
story_issues+="frontend needs tsc --noEmit or playwright tests, "
|
|
576
|
+
fi
|
|
577
|
+
fi
|
|
578
|
+
|
|
579
|
+
# Check 2: Backend needs apiContract
|
|
580
|
+
if [[ "$story_type" == "backend" ]]; then
|
|
581
|
+
local has_contract
|
|
582
|
+
has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
|
|
583
|
+
if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
|
|
584
|
+
story_issues+="backend missing apiContract, "
|
|
585
|
+
fi
|
|
586
|
+
fi
|
|
587
|
+
|
|
588
|
+
# Check 3: Frontend needs testUrl and contextFiles
|
|
589
|
+
if [[ "$story_type" == "frontend" ]]; then
|
|
590
|
+
local has_url
|
|
591
|
+
has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
|
|
592
|
+
if [[ -z "$has_url" || "$has_url" == "null" ]]; then
|
|
593
|
+
story_issues+="frontend missing testUrl, "
|
|
594
|
+
fi
|
|
595
|
+
|
|
596
|
+
local context_files
|
|
597
|
+
context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
|
|
598
|
+
if [[ "$context_files" == "0" ]]; then
|
|
599
|
+
story_issues+="frontend missing contextFiles (idea file + styleguide), "
|
|
600
|
+
fi
|
|
601
|
+
fi
|
|
602
|
+
|
|
603
|
+
# Check 4: Auth stories need security criteria
|
|
604
|
+
if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
|
|
605
|
+
local criteria
|
|
606
|
+
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
607
|
+
if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
|
|
608
|
+
story_issues+="auth story missing security criteria (password hashing/rate limiting), "
|
|
609
|
+
fi
|
|
610
|
+
fi
|
|
611
|
+
|
|
612
|
+
# Check 5: List endpoints need scale criteria
|
|
613
|
+
# Note: "search" excluded - search endpoints often return single/filtered results
|
|
614
|
+
if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
|
|
615
|
+
local criteria
|
|
616
|
+
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
617
|
+
if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
|
|
618
|
+
story_issues+="list endpoint missing pagination criteria, "
|
|
619
|
+
fi
|
|
620
|
+
fi
|
|
621
|
+
|
|
622
|
+
# Report issues for this story
|
|
623
|
+
if [[ -n "$story_issues" ]]; then
|
|
624
|
+
needs_fix=true
|
|
625
|
+
issues+="$story_id: ${story_issues%%, }
|
|
626
|
+
"
|
|
627
|
+
fi
|
|
628
|
+
done <<< "$story_ids"
|
|
629
|
+
|
|
630
|
+
# If issues found, attempt to fix with Claude
|
|
631
|
+
if [[ "$needs_fix" == "true" ]]; then
|
|
632
|
+
print_warning "Story quality issues found:"
|
|
633
|
+
echo "$issues" | while IFS= read -r line; do
|
|
634
|
+
[[ -n "$line" ]] && echo " $line"
|
|
526
635
|
done
|
|
527
636
|
echo ""
|
|
528
|
-
|
|
529
|
-
|
|
637
|
+
|
|
638
|
+
# Check if Claude is available for auto-fix
|
|
639
|
+
if command -v claude &>/dev/null; then
|
|
640
|
+
echo " Attempting auto-fix with Claude..."
|
|
641
|
+
fix_stories_with_claude "$prd_file" "$issues"
|
|
642
|
+
else
|
|
643
|
+
echo " Claude CLI not found - fix these issues manually or regenerate PRD."
|
|
644
|
+
echo ""
|
|
645
|
+
return 1
|
|
646
|
+
fi
|
|
647
|
+
else
|
|
648
|
+
print_success "All stories validated"
|
|
530
649
|
fi
|
|
531
650
|
|
|
532
651
|
return 0
|
|
533
652
|
}
|
|
534
653
|
|
|
654
|
+
# Fix story issues using Claude
|
|
655
|
+
fix_stories_with_claude() {
|
|
656
|
+
local prd_file="$1"
|
|
657
|
+
local issues="$2"
|
|
658
|
+
|
|
659
|
+
local fix_prompt="Fix the following issues in this PRD. Output the COMPLETE fixed prd.json.
|
|
660
|
+
|
|
661
|
+
ISSUES FOUND:
|
|
662
|
+
$issues
|
|
663
|
+
|
|
664
|
+
RULES FOR FIXING:
|
|
665
|
+
1. Backend stories MUST have testSteps with curl commands that hit real endpoints
|
|
666
|
+
Example: curl -s -X POST {config.urls.backend}/api/users -d '...' | jq -e '.id'
|
|
667
|
+
2. Backend stories MUST have apiContract with endpoint, request, response
|
|
668
|
+
3. Frontend stories MUST have testUrl set to {config.urls.frontend}/page
|
|
669
|
+
4. Frontend stories MUST have contextFiles array (include idea file path from originalContext)
|
|
670
|
+
5. Auth stories MUST have security acceptanceCriteria:
|
|
671
|
+
- Passwords hashed with bcrypt (cost 10+)
|
|
672
|
+
- Passwords NEVER in API responses
|
|
673
|
+
- Rate limiting on login attempts
|
|
674
|
+
6. List endpoints MUST have pagination acceptanceCriteria:
|
|
675
|
+
- Returns paginated results (max 100 per page)
|
|
676
|
+
- Accepts ?page=N&limit=N query params
|
|
677
|
+
|
|
678
|
+
CURRENT PRD:
|
|
679
|
+
$(cat "$prd_file")
|
|
680
|
+
|
|
681
|
+
Output ONLY the fixed JSON, no explanation. Start with { and end with }."
|
|
682
|
+
|
|
683
|
+
local raw_response
|
|
684
|
+
raw_response=$(echo "$fix_prompt" | run_with_timeout "$CODE_REVIEW_TIMEOUT_SECONDS" claude -p 2>/dev/null)
|
|
685
|
+
|
|
686
|
+
# Extract JSON from response (Claude sometimes adds text before/after)
|
|
687
|
+
local fixed_prd
|
|
688
|
+
fixed_prd=$(echo "$raw_response" | sed -n '/^[[:space:]]*{/,/^[[:space:]]*}[[:space:]]*$/p' | head -1000)
|
|
689
|
+
|
|
690
|
+
# If sed extraction failed, try the raw response
|
|
691
|
+
if [[ -z "$fixed_prd" ]]; then
|
|
692
|
+
fixed_prd="$raw_response"
|
|
693
|
+
fi
|
|
694
|
+
|
|
695
|
+
# Validate the response is valid JSON with required structure
|
|
696
|
+
if echo "$fixed_prd" | jq -e '.stories' >/dev/null 2>&1; then
|
|
697
|
+
# Timestamped backup (preserves history across multiple fixes)
|
|
698
|
+
local backup_file="${prd_file}.$(date +%Y%m%d-%H%M%S).bak"
|
|
699
|
+
cp "$prd_file" "$backup_file"
|
|
700
|
+
|
|
701
|
+
# Write fixed PRD
|
|
702
|
+
echo "$fixed_prd" > "$prd_file"
|
|
703
|
+
print_success "PRD auto-fixed (backup at $backup_file)"
|
|
704
|
+
|
|
705
|
+
# Re-validate to confirm fixes
|
|
706
|
+
echo " Re-validating..."
|
|
707
|
+
local remaining_issues
|
|
708
|
+
remaining_issues=$(validate_stories_quick "$prd_file")
|
|
709
|
+
if [[ -n "$remaining_issues" ]]; then
|
|
710
|
+
print_warning "Some issues remain - may need manual fixes:"
|
|
711
|
+
echo "$remaining_issues" | tr ',' '\n' | while IFS= read -r line; do
|
|
712
|
+
[[ -n "$line" ]] && echo " $line"
|
|
713
|
+
done
|
|
714
|
+
else
|
|
715
|
+
print_success "All issues resolved"
|
|
716
|
+
fi
|
|
717
|
+
else
|
|
718
|
+
print_error "Claude returned invalid JSON - fix manually"
|
|
719
|
+
echo " Response preview: $(echo "$raw_response" | head -3)"
|
|
720
|
+
return 1
|
|
721
|
+
fi
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
# Quick validation without auto-fix (for re-checking after fix)
|
|
725
|
+
# Checks all the same things as validate_and_fix_stories() but returns issues string
|
|
726
|
+
validate_stories_quick() {
|
|
727
|
+
local prd_file="$1"
|
|
728
|
+
local issues=""
|
|
729
|
+
|
|
730
|
+
local story_ids
|
|
731
|
+
story_ids=$(jq -r '.stories[].id' "$prd_file" 2>/dev/null)
|
|
732
|
+
|
|
733
|
+
while IFS= read -r story_id; do
|
|
734
|
+
[[ -z "$story_id" ]] && continue
|
|
735
|
+
|
|
736
|
+
local story_type
|
|
737
|
+
story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
|
|
738
|
+
local story_title
|
|
739
|
+
story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
|
|
740
|
+
local test_steps
|
|
741
|
+
test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
|
|
742
|
+
|
|
743
|
+
# Check 1: testSteps quality
|
|
744
|
+
if [[ "$story_type" == "backend" ]] && ! echo "$test_steps" | grep -q "curl "; then
|
|
745
|
+
issues+="$story_id: missing curl tests, "
|
|
746
|
+
fi
|
|
747
|
+
if [[ "$story_type" == "frontend" ]] && ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
|
|
748
|
+
issues+="$story_id: missing tsc/playwright tests, "
|
|
749
|
+
fi
|
|
750
|
+
|
|
751
|
+
# Check 2: Backend needs apiContract
|
|
752
|
+
if [[ "$story_type" == "backend" ]]; then
|
|
753
|
+
local has_contract
|
|
754
|
+
has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
|
|
755
|
+
if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
|
|
756
|
+
issues+="$story_id: missing apiContract, "
|
|
757
|
+
fi
|
|
758
|
+
fi
|
|
759
|
+
|
|
760
|
+
# Check 3: Frontend needs testUrl and contextFiles
|
|
761
|
+
if [[ "$story_type" == "frontend" ]]; then
|
|
762
|
+
local has_url
|
|
763
|
+
has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
|
|
764
|
+
[[ -z "$has_url" || "$has_url" == "null" ]] && issues+="$story_id: missing testUrl, "
|
|
765
|
+
|
|
766
|
+
local context_files
|
|
767
|
+
context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
|
|
768
|
+
[[ "$context_files" == "0" ]] && issues+="$story_id: missing contextFiles, "
|
|
769
|
+
fi
|
|
770
|
+
|
|
771
|
+
# Check 4: Auth stories need security criteria
|
|
772
|
+
if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
|
|
773
|
+
local criteria
|
|
774
|
+
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
775
|
+
if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
|
|
776
|
+
issues+="$story_id: missing security criteria, "
|
|
777
|
+
fi
|
|
778
|
+
fi
|
|
779
|
+
|
|
780
|
+
# Check 5: List endpoints need scale criteria
|
|
781
|
+
if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
|
|
782
|
+
local criteria
|
|
783
|
+
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
784
|
+
if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
|
|
785
|
+
issues+="$story_id: missing pagination criteria, "
|
|
786
|
+
fi
|
|
787
|
+
fi
|
|
788
|
+
done <<< "$story_ids"
|
|
789
|
+
|
|
790
|
+
echo "$issues"
|
|
791
|
+
}
|
|
792
|
+
|
|
535
793
|
# Detect Python runner (uv, poetry, pipenv, or plain python)
|
|
536
794
|
detect_python_runner() {
|
|
537
795
|
local search_dir="${1:-.}"
|
|
@@ -39,9 +39,15 @@
|
|
|
39
39
|
"typecheck": false,
|
|
40
40
|
"build": true,
|
|
41
41
|
"test": true,
|
|
42
|
+
"requireTests": true,
|
|
42
43
|
"fastapi": false
|
|
43
44
|
},
|
|
44
45
|
|
|
46
|
+
"tests": {
|
|
47
|
+
"directory": "test",
|
|
48
|
+
"patterns": "*_test.exs"
|
|
49
|
+
},
|
|
50
|
+
|
|
45
51
|
"api": {
|
|
46
52
|
"baseUrl": "http://localhost:4000",
|
|
47
53
|
"healthEndpoint": "/api/health",
|
|
@@ -40,9 +40,15 @@
|
|
|
40
40
|
"typecheck": true,
|
|
41
41
|
"build": false,
|
|
42
42
|
"test": true,
|
|
43
|
+
"requireTests": true,
|
|
43
44
|
"fastmcp": true
|
|
44
45
|
},
|
|
45
46
|
|
|
47
|
+
"tests": {
|
|
48
|
+
"directory": "tests",
|
|
49
|
+
"patterns": "*_test.py,test_*.py"
|
|
50
|
+
},
|
|
51
|
+
|
|
46
52
|
"api": {
|
|
47
53
|
"baseUrl": "http://localhost:8000",
|
|
48
54
|
"healthEndpoint": "/health",
|
|
@@ -41,9 +41,15 @@
|
|
|
41
41
|
"typecheck": true,
|
|
42
42
|
"build": "final",
|
|
43
43
|
"test": true,
|
|
44
|
+
"requireTests": true,
|
|
44
45
|
"fastapi": true
|
|
45
46
|
},
|
|
46
47
|
|
|
48
|
+
"tests": {
|
|
49
|
+
"directory": "tests",
|
|
50
|
+
"patterns": "*.test.ts,*.test.tsx,*_test.py,test_*.py"
|
|
51
|
+
},
|
|
52
|
+
|
|
47
53
|
"api": {
|
|
48
54
|
"baseUrl": "http://localhost:8000",
|
|
49
55
|
"healthEndpoint": "/api/health",
|
package/templates/config/go.json
CHANGED
|
@@ -39,9 +39,15 @@
|
|
|
39
39
|
"typecheck": false,
|
|
40
40
|
"build": true,
|
|
41
41
|
"test": true,
|
|
42
|
+
"requireTests": true,
|
|
42
43
|
"fastapi": false
|
|
43
44
|
},
|
|
44
45
|
|
|
46
|
+
"tests": {
|
|
47
|
+
"directory": ".",
|
|
48
|
+
"patterns": "*_test.go"
|
|
49
|
+
},
|
|
50
|
+
|
|
45
51
|
"api": {
|
|
46
52
|
"baseUrl": "http://localhost:8080",
|
|
47
53
|
"healthEndpoint": "/health",
|
|
@@ -39,9 +39,15 @@
|
|
|
39
39
|
"typecheck": true,
|
|
40
40
|
"build": "final",
|
|
41
41
|
"test": true,
|
|
42
|
+
"requireTests": false,
|
|
42
43
|
"fastapi": false
|
|
43
44
|
},
|
|
44
45
|
|
|
46
|
+
"tests": {
|
|
47
|
+
"directory": "",
|
|
48
|
+
"patterns": ""
|
|
49
|
+
},
|
|
50
|
+
|
|
45
51
|
"api": {
|
|
46
52
|
"baseUrl": "http://localhost:3000",
|
|
47
53
|
"healthEndpoint": "/health",
|
|
@@ -39,9 +39,15 @@
|
|
|
39
39
|
"typecheck": true,
|
|
40
40
|
"build": "final",
|
|
41
41
|
"test": true,
|
|
42
|
+
"requireTests": true,
|
|
42
43
|
"fastapi": false
|
|
43
44
|
},
|
|
44
45
|
|
|
46
|
+
"tests": {
|
|
47
|
+
"directory": "tests",
|
|
48
|
+
"patterns": "*.test.ts,*.test.tsx,*.test.js,*.spec.ts,*.spec.tsx,*.spec.js"
|
|
49
|
+
},
|
|
50
|
+
|
|
45
51
|
"api": {
|
|
46
52
|
"baseUrl": "http://localhost:3000",
|
|
47
53
|
"healthEndpoint": "/api/health",
|
|
@@ -39,9 +39,15 @@
|
|
|
39
39
|
"typecheck": false,
|
|
40
40
|
"build": false,
|
|
41
41
|
"test": true,
|
|
42
|
+
"requireTests": true,
|
|
42
43
|
"fastapi": true
|
|
43
44
|
},
|
|
44
45
|
|
|
46
|
+
"tests": {
|
|
47
|
+
"directory": "tests",
|
|
48
|
+
"patterns": "*_test.py,test_*.py"
|
|
49
|
+
},
|
|
50
|
+
|
|
45
51
|
"api": {
|
|
46
52
|
"baseUrl": "http://localhost:8000",
|
|
47
53
|
"healthEndpoint": "/api/health",
|
|
@@ -39,9 +39,15 @@
|
|
|
39
39
|
"typecheck": false,
|
|
40
40
|
"build": true,
|
|
41
41
|
"test": true,
|
|
42
|
+
"requireTests": true,
|
|
42
43
|
"fastapi": false
|
|
43
44
|
},
|
|
44
45
|
|
|
46
|
+
"tests": {
|
|
47
|
+
"directory": "tests",
|
|
48
|
+
"patterns": "*_test.rs"
|
|
49
|
+
},
|
|
50
|
+
|
|
45
51
|
"api": {
|
|
46
52
|
"baseUrl": "http://localhost:8080",
|
|
47
53
|
"healthEndpoint": "/health",
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
|
|
27
27
|
"globalConstraints": [
|
|
28
28
|
"All API calls must have error handling",
|
|
29
|
-
"Use existing UI components from src/components/ui"
|
|
29
|
+
"Use existing UI components from src/components/ui",
|
|
30
|
+
"Never store passwords in plain text",
|
|
31
|
+
"Sanitize all user input before database operations"
|
|
30
32
|
],
|
|
31
33
|
|
|
32
34
|
"metadata": {
|
|
@@ -51,13 +53,17 @@
|
|
|
51
53
|
|
|
52
54
|
"acceptanceCriteria": [
|
|
53
55
|
"POST /api/users creates a new user with email and password",
|
|
54
|
-
"Returns 201 with user id and email (
|
|
55
|
-
"Returns 400 if email already exists"
|
|
56
|
+
"Returns 201 with user id and email (password NEVER in response)",
|
|
57
|
+
"Returns 400 if email already exists",
|
|
58
|
+
"Passwords hashed with bcrypt (cost factor 10+) before storing",
|
|
59
|
+
"Email validated for format before insert",
|
|
60
|
+
"Input sanitized to prevent SQL injection"
|
|
56
61
|
],
|
|
57
62
|
|
|
58
63
|
"errorHandling": [
|
|
59
64
|
"Duplicate email returns {error: 'Email already registered'}",
|
|
60
|
-
"Invalid email returns {error: 'Invalid email format'}"
|
|
65
|
+
"Invalid email returns {error: 'Invalid email format'}",
|
|
66
|
+
"Missing fields returns {error: 'Email and password required'}"
|
|
61
67
|
],
|
|
62
68
|
|
|
63
69
|
"testing": {
|
|
@@ -69,9 +75,9 @@
|
|
|
69
75
|
},
|
|
70
76
|
|
|
71
77
|
"testSteps": [
|
|
72
|
-
"curl -s -X POST {config.urls.backend}/api/users -H 'Content-Type: application/json' -d '{\"email\":\"test@example.com\",\"password\":\"secret123\"}' | jq -e '.id and .email'",
|
|
73
|
-
"curl -s -X POST {config.urls.backend}/api/users -H 'Content-Type: application/json' -d '{\"email\":\"test@example.com\",\"password\":\"secret123\"}' | jq -e '.error'",
|
|
74
|
-
"
|
|
78
|
+
"curl -s -X POST {config.urls.backend}/api/users -H 'Content-Type: application/json' -d '{\"email\":\"test@example.com\",\"password\":\"secret123\"}' | jq -e '.id and .email and (has(\"password\") | not)'",
|
|
79
|
+
"curl -s -X POST {config.urls.backend}/api/users -H 'Content-Type: application/json' -d '{\"email\":\"test@example.com\",\"password\":\"secret123\"}' | jq -e '.error == \"Email already registered\"'",
|
|
80
|
+
"curl -s -X POST {config.urls.backend}/api/users -H 'Content-Type: application/json' -d '{\"email\":\"invalid\",\"password\":\"x\"}' | jq -e '.error'"
|
|
75
81
|
],
|
|
76
82
|
|
|
77
83
|
"apiContract": {
|
|
@@ -80,7 +86,7 @@
|
|
|
80
86
|
"response": {"id": "string", "email": "string"}
|
|
81
87
|
},
|
|
82
88
|
|
|
83
|
-
"notes": "
|
|
89
|
+
"notes": "SECURITY: Use bcrypt with cost 10+. Never log passwords. Validate email format server-side even if validated client-side.",
|
|
84
90
|
"dependsOn": []
|
|
85
91
|
},
|
|
86
92
|
{
|
|
@@ -97,15 +103,20 @@
|
|
|
97
103
|
},
|
|
98
104
|
|
|
99
105
|
"acceptanceCriteria": [
|
|
100
|
-
"Form has email and password fields",
|
|
106
|
+
"Form has email and password fields with proper input types",
|
|
107
|
+
"Password field uses type='password' (masked input)",
|
|
101
108
|
"Submit button calls POST /api/users",
|
|
102
109
|
"Shows success message on 201 response",
|
|
103
|
-
"Shows error message on 400 response"
|
|
110
|
+
"Shows error message on 400 response",
|
|
111
|
+
"Client-side validation before submit (email format, password length)",
|
|
112
|
+
"Disable submit button while request in flight (prevent double-submit)",
|
|
113
|
+
"Form matches mockup in docs/ideas/auth.md"
|
|
104
114
|
],
|
|
105
115
|
|
|
106
116
|
"errorHandling": [
|
|
107
117
|
"Network error shows 'Unable to connect' message",
|
|
108
|
-
"Validation errors display inline"
|
|
118
|
+
"Validation errors display inline below each field",
|
|
119
|
+
"Server errors display at form level"
|
|
109
120
|
],
|
|
110
121
|
|
|
111
122
|
"testing": {
|
|
@@ -119,15 +130,67 @@
|
|
|
119
130
|
|
|
120
131
|
"testSteps": [
|
|
121
132
|
"npx tsc --noEmit",
|
|
122
|
-
"npm test -- --testPathPattern=RegisterForm",
|
|
123
133
|
"npx playwright test tests/e2e/register.spec.ts"
|
|
124
134
|
],
|
|
125
135
|
|
|
126
136
|
"testUrl": "{config.urls.frontend}/register",
|
|
127
137
|
|
|
138
|
+
"contextFiles": [
|
|
139
|
+
"docs/ideas/auth.md",
|
|
140
|
+
"src/styles/styleguide.html"
|
|
141
|
+
],
|
|
142
|
+
|
|
128
143
|
"mcp": ["playwright", "devtools"],
|
|
129
144
|
|
|
130
|
-
"notes": "Use existing Button and Input components from ui folder.",
|
|
145
|
+
"notes": "IMPORTANT: Reference the ASCII mockup in docs/ideas/auth.md for layout. Use existing Button and Input components from ui folder per styleguide.",
|
|
146
|
+
"dependsOn": ["TASK-001"]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "TASK-003",
|
|
150
|
+
"type": "backend",
|
|
151
|
+
"title": "List users endpoint with pagination",
|
|
152
|
+
"priority": 3,
|
|
153
|
+
"passes": false,
|
|
154
|
+
|
|
155
|
+
"files": {
|
|
156
|
+
"create": [],
|
|
157
|
+
"modify": ["src/api/users.ts"],
|
|
158
|
+
"reuse": ["src/db/client.ts"]
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
"acceptanceCriteria": [
|
|
162
|
+
"GET /api/users returns paginated list of users",
|
|
163
|
+
"Accepts ?page=1&limit=20 query params",
|
|
164
|
+
"Default limit is 20, max limit is 100",
|
|
165
|
+
"Returns {data: [...], total: N, page: N, limit: N}",
|
|
166
|
+
"Passwords NEVER included in response",
|
|
167
|
+
"Results ordered by created_at desc",
|
|
168
|
+
"Database query uses index on created_at"
|
|
169
|
+
],
|
|
170
|
+
|
|
171
|
+
"errorHandling": [
|
|
172
|
+
"Invalid page/limit returns 400 with error message",
|
|
173
|
+
"limit > 100 returns 400 'Limit cannot exceed 100'"
|
|
174
|
+
],
|
|
175
|
+
|
|
176
|
+
"testing": {
|
|
177
|
+
"types": ["integration"],
|
|
178
|
+
"approach": "TDD",
|
|
179
|
+
"files": {}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
"testSteps": [
|
|
183
|
+
"curl -s '{config.urls.backend}/api/users?page=1&limit=10' | jq -e '.data and .total and .page and .limit'",
|
|
184
|
+
"curl -s '{config.urls.backend}/api/users?limit=200' | jq -e '.error'"
|
|
185
|
+
],
|
|
186
|
+
|
|
187
|
+
"apiContract": {
|
|
188
|
+
"endpoint": "GET /api/users",
|
|
189
|
+
"request": {"page": "number (optional)", "limit": "number (optional)"},
|
|
190
|
+
"response": {"data": "User[]", "total": "number", "page": "number", "limit": "number"}
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
"notes": "SCALE: Always paginate list endpoints. Enforce max limit to prevent memory issues. Add database index for sort column.",
|
|
131
194
|
"dependsOn": ["TASK-001"]
|
|
132
195
|
}
|
|
133
196
|
]
|