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.
@@ -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** - Up to 5 questions about:
48
- - Scope boundaries (what's in/out)
49
- - User experience (what does the user see/do)
50
- - Edge cases (what could go wrong)
51
- - Dependencies (what does this touch)
52
- - Security/permissions (who can do what)
53
- - Scale (how many users/items/requests?)
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
- ## Technical Notes
111
- - Dependencies
112
- - Security considerations
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
@@ -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, ask yourself:
128
-
129
- 1. **"Is this testable?"** - Can the testSteps actually run?
130
- - ❌ `grep -q 'function' file.py` → Only checks code exists, not behavior
131
- - ❌ `test -f src/component.tsx` → Only checks file exists
132
- - ❌ "Visit the page and verify"Not executable
133
- - ✅ `curl ... | jq -e` → Tests actual API response
134
- - ✅ `npm test` / `pytest` Runs real tests
135
- - ✅ `npx playwright test` → Runs real tests
136
-
137
- 2. **"Is this passable?"** - Given prior stories completed, can this story's tests pass?
138
- - If TASK-003 needs a user to exist, does TASK-001 or TASK-002 create one?
139
- - If TASK-004 tests a login flow, does a prior story create the auth endpoint?
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 backendMocks 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, pytest, npm test, playwright |
146
- | Story depends on something not yet created | Reorder stories or add missing dependency story |
147
- | testSteps would pass on current code | Strengthen tests to verify NEW behavior |
148
- | No testSteps for backend story | Add `curl -s {config.urls.backend}/endpoint \| jq -e '.field'` |
149
- | No testSteps for frontend story | Add `npx tsc --noEmit` + `npm test` |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-loop",
3
- "version": "3.7.3",
3
+ "version": "3.9.0",
4
4
  "description": "Autonomous AI coding loop - PRD-driven development with Claude Code",
5
5
  "author": "Allie Jones <allie@allthrive.ai>",
6
6
  "license": "MIT",
package/ralph/init.sh CHANGED
@@ -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 for grep-only testSteps (the #1 cause of false passes)
513
- # Matches: grep, test -f/-e/-d, [ -f file ], [[ -f file ]]
514
- local grep_only_stories
515
- grep_only_stories=$(jq -r '
516
- .stories[] |
517
- select(.testSteps != null and (.testSteps | length > 0)) |
518
- select(.testSteps | all(test("^(grep|test\\s+-[fed]|\\[\\[?\\s+-[fed])"; "x"))) |
519
- .id
520
- ' "$prd_file" 2>/dev/null)
521
-
522
- if [[ -n "$grep_only_stories" ]]; then
523
- print_warning "These stories have grep-only testSteps (may cause false passes):"
524
- echo "$grep_only_stories" | while read -r story_id; do
525
- [[ -n "$story_id" ]] && echo " - $story_id"
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
- echo "Grep verifies code exists, not that it works. Add curl/playwright tests."
529
- echo ""
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",
@@ -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 (no password in response)",
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
- "npm test -- --testPathPattern=users"
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": "Hash passwords with bcrypt before storing.",
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
  ]