agentic-loop 3.7.3 → 3.8.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? (prevent 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.8.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/utils.sh CHANGED
@@ -509,29 +509,212 @@ 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
+ # Validate and fix individual stories
513
+ validate_and_fix_stories "$prd_file" || return 1
514
+
515
+ return 0
516
+ }
517
+
518
+ # Validate individual stories and auto-fix with Claude if needed
519
+ # Checks: testSteps quality, apiContract, testUrl, contextFiles, security, scale
520
+ validate_and_fix_stories() {
521
+ local prd_file="$1"
522
+ local needs_fix=false
523
+ local issues=""
524
+
525
+ echo " Validating story quality..."
526
+
527
+ # Get all story IDs
528
+ local story_ids
529
+ story_ids=$(jq -r '.stories[].id' "$prd_file" 2>/dev/null)
530
+
531
+ while IFS= read -r story_id; do
532
+ [[ -z "$story_id" ]] && continue
533
+
534
+ local story_issues=""
535
+ local story_type
536
+ story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
537
+ local story_title
538
+ story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
539
+
540
+ # Check 1: testSteps quality
541
+ local test_steps
542
+ test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
543
+
544
+ if [[ -z "$test_steps" ]]; then
545
+ story_issues+="no testSteps, "
546
+ elif [[ "$story_type" == "backend" ]]; then
547
+ # Backend must have curl, not just npm test/pytest
548
+ if ! echo "$test_steps" | grep -q "curl "; then
549
+ story_issues+="backend needs curl tests (npm test alone uses mocks), "
550
+ fi
551
+ elif [[ "$story_type" == "frontend" ]]; then
552
+ # Frontend must have tsc or playwright
553
+ if ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
554
+ story_issues+="frontend needs tsc --noEmit or playwright tests, "
555
+ fi
556
+ fi
557
+
558
+ # Check 2: Backend needs apiContract
559
+ if [[ "$story_type" == "backend" ]]; then
560
+ local has_contract
561
+ has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
562
+ if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
563
+ story_issues+="backend missing apiContract, "
564
+ fi
565
+ fi
566
+
567
+ # Check 3: Frontend needs testUrl and contextFiles
568
+ if [[ "$story_type" == "frontend" ]]; then
569
+ local has_url
570
+ has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
571
+ if [[ -z "$has_url" || "$has_url" == "null" ]]; then
572
+ story_issues+="frontend missing testUrl, "
573
+ fi
574
+
575
+ local context_files
576
+ context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
577
+ if [[ "$context_files" == "0" ]]; then
578
+ story_issues+="frontend missing contextFiles (idea file + styleguide), "
579
+ fi
580
+ fi
581
+
582
+ # Check 4: Auth stories need security criteria
583
+ if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
584
+ local criteria
585
+ criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
586
+ if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
587
+ story_issues+="auth story missing security criteria (password hashing/rate limiting), "
588
+ fi
589
+ fi
590
+
591
+ # Check 5: List endpoints need scale criteria
592
+ if echo "$story_title" | grep -qiE "(list|get all|fetch all|index|search)"; then
593
+ local criteria
594
+ criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
595
+ if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
596
+ story_issues+="list endpoint missing pagination criteria, "
597
+ fi
598
+ fi
599
+
600
+ # Report issues for this story
601
+ if [[ -n "$story_issues" ]]; then
602
+ needs_fix=true
603
+ issues+="$story_id: ${story_issues%%, }
604
+ "
605
+ fi
606
+ done <<< "$story_ids"
607
+
608
+ # If issues found, attempt to fix with Claude
609
+ if [[ "$needs_fix" == "true" ]]; then
610
+ print_warning "Story quality issues found:"
611
+ echo "$issues" | while IFS= read -r line; do
612
+ [[ -n "$line" ]] && echo " $line"
526
613
  done
527
614
  echo ""
528
- echo "Grep verifies code exists, not that it works. Add curl/playwright tests."
529
- echo ""
615
+
616
+ # Check if Claude is available for auto-fix
617
+ if command -v claude &>/dev/null; then
618
+ echo " Attempting auto-fix with Claude..."
619
+ fix_stories_with_claude "$prd_file" "$issues"
620
+ else
621
+ echo " Claude CLI not found - fix these issues manually or regenerate PRD."
622
+ echo ""
623
+ return 1
624
+ fi
625
+ else
626
+ print_success "All stories validated"
530
627
  fi
531
628
 
532
629
  return 0
533
630
  }
534
631
 
632
+ # Fix story issues using Claude
633
+ fix_stories_with_claude() {
634
+ local prd_file="$1"
635
+ local issues="$2"
636
+
637
+ local fix_prompt="Fix the following issues in this PRD. Output the COMPLETE fixed prd.json.
638
+
639
+ ISSUES FOUND:
640
+ $issues
641
+
642
+ RULES FOR FIXING:
643
+ 1. Backend stories MUST have testSteps with curl commands that hit real endpoints
644
+ Example: curl -s -X POST {config.urls.backend}/api/users -d '...' | jq -e '.id'
645
+ 2. Backend stories MUST have apiContract with endpoint, request, response
646
+ 3. Frontend stories MUST have testUrl set to {config.urls.frontend}/page
647
+ 4. Frontend stories MUST have contextFiles array (include idea file path from originalContext)
648
+ 5. Auth stories MUST have security acceptanceCriteria:
649
+ - Passwords hashed with bcrypt (cost 10+)
650
+ - Passwords NEVER in API responses
651
+ - Rate limiting on login attempts
652
+ 6. List endpoints MUST have pagination acceptanceCriteria:
653
+ - Returns paginated results (max 100 per page)
654
+ - Accepts ?page=N&limit=N query params
655
+
656
+ CURRENT PRD:
657
+ $(cat "$prd_file")
658
+
659
+ Output ONLY the fixed JSON, no explanation."
660
+
661
+ local fixed_prd
662
+ fixed_prd=$(echo "$fix_prompt" | claude -p 2>/dev/null)
663
+
664
+ # Validate the response is valid JSON
665
+ if echo "$fixed_prd" | jq -e . >/dev/null 2>&1; then
666
+ # Backup original
667
+ cp "$prd_file" "${prd_file}.bak"
668
+
669
+ # Write fixed PRD
670
+ echo "$fixed_prd" > "$prd_file"
671
+ print_success "PRD auto-fixed (backup at ${prd_file}.bak)"
672
+
673
+ # Re-validate to confirm fixes
674
+ echo " Re-validating..."
675
+ local remaining_issues
676
+ remaining_issues=$(validate_stories_quick "$prd_file")
677
+ if [[ -n "$remaining_issues" ]]; then
678
+ print_warning "Some issues remain - may need manual fixes"
679
+ else
680
+ print_success "All issues resolved"
681
+ fi
682
+ else
683
+ print_error "Claude returned invalid JSON - fix manually"
684
+ return 1
685
+ fi
686
+ }
687
+
688
+ # Quick validation without auto-fix (for re-checking after fix)
689
+ validate_stories_quick() {
690
+ local prd_file="$1"
691
+ local issues=""
692
+
693
+ local story_ids
694
+ story_ids=$(jq -r '.stories[].id' "$prd_file" 2>/dev/null)
695
+
696
+ while IFS= read -r story_id; do
697
+ [[ -z "$story_id" ]] && continue
698
+
699
+ local story_type
700
+ story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
701
+ local test_steps
702
+ test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
703
+
704
+ if [[ "$story_type" == "backend" ]] && ! echo "$test_steps" | grep -q "curl "; then
705
+ issues+="$story_id: still missing curl tests, "
706
+ fi
707
+
708
+ if [[ "$story_type" == "frontend" ]]; then
709
+ local has_url
710
+ has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
711
+ [[ -z "$has_url" ]] && issues+="$story_id: still missing testUrl, "
712
+ fi
713
+ done <<< "$story_ids"
714
+
715
+ echo "$issues"
716
+ }
717
+
535
718
  # Detect Python runner (uv, poetry, pipenv, or plain python)
536
719
  detect_python_runner() {
537
720
  local search_dir="${1:-.}"
@@ -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
  ]