agentic-loop 3.10.2 → 3.11.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/api.md +496 -0
- package/.claude/commands/aws.md +408 -0
- package/bin/ralph.sh +2 -1
- package/package.json +2 -1
- package/ralph/code-check.sh +307 -0
- package/ralph/loop.sh +80 -27
- package/ralph/prd-check.sh +498 -0
- package/ralph/utils.sh +66 -351
- package/templates/config/elixir.json +1 -1
- package/templates/config/fastmcp.json +1 -1
- package/templates/config/fullstack.json +1 -1
- package/templates/config/go.json +1 -1
- package/templates/config/minimal.json +1 -1
- package/templates/config/node.json +1 -1
- package/templates/config/python.json +1 -1
- package/templates/config/rust.json +1 -1
- package/ralph/verify.sh +0 -106
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# shellcheck shell=bash
|
|
3
|
+
#
|
|
4
|
+
# prd-check.sh - PRD validation and optimization for Ralph
|
|
5
|
+
#
|
|
6
|
+
# ============================================================================
|
|
7
|
+
# OVERVIEW
|
|
8
|
+
# ============================================================================
|
|
9
|
+
# Validates PRD structure and story quality BEFORE the loop starts. Catches
|
|
10
|
+
# issues early (missing test steps, vague requirements) rather than failing
|
|
11
|
+
# 50+ times during execution.
|
|
12
|
+
#
|
|
13
|
+
# This runs once at loop startup, not after each story.
|
|
14
|
+
#
|
|
15
|
+
# ============================================================================
|
|
16
|
+
# WHAT IT CHECKS
|
|
17
|
+
# ============================================================================
|
|
18
|
+
#
|
|
19
|
+
# Structure validation:
|
|
20
|
+
# - Valid JSON syntax
|
|
21
|
+
# - Has .feature.name
|
|
22
|
+
# - Has .stories array (non-empty)
|
|
23
|
+
# - Each story has id and title
|
|
24
|
+
# - Initializes passes=false for new stories
|
|
25
|
+
#
|
|
26
|
+
# Story quality checks (per story type):
|
|
27
|
+
#
|
|
28
|
+
# ALL STORIES:
|
|
29
|
+
# - Has testSteps (not empty)
|
|
30
|
+
# - testSteps are executable commands, not prose
|
|
31
|
+
# Good: "curl -s POST /api/login | jq -e '.token'"
|
|
32
|
+
# Bad: "Verify the user can log in"
|
|
33
|
+
#
|
|
34
|
+
# BACKEND STORIES:
|
|
35
|
+
# - Has curl commands in testSteps (not just "npm test")
|
|
36
|
+
# - Has apiContract with endpoint, request, response
|
|
37
|
+
#
|
|
38
|
+
# FRONTEND STORIES:
|
|
39
|
+
# - Has tsc or playwright in testSteps
|
|
40
|
+
# - Has testUrl for browser verification
|
|
41
|
+
# - Has contextFiles (design specs, etc.)
|
|
42
|
+
#
|
|
43
|
+
# AUTH STORIES (login, register, password):
|
|
44
|
+
# - Has security criteria (bcrypt, sanitize, rate limit)
|
|
45
|
+
#
|
|
46
|
+
# LIST ENDPOINTS (get all, index):
|
|
47
|
+
# - Has pagination criteria (limit, page params)
|
|
48
|
+
#
|
|
49
|
+
# MIGRATION STORIES (alembic, migrations, models):
|
|
50
|
+
# - Has prerequisites array with DB reset command
|
|
51
|
+
# - Prevents infinite retries on schema mismatch errors
|
|
52
|
+
#
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# AUTO-FIX
|
|
55
|
+
# ============================================================================
|
|
56
|
+
# When issues are found, Claude is invoked to fix them automatically:
|
|
57
|
+
#
|
|
58
|
+
# 1. Issues are summarized (e.g., "3x backend: add curl tests")
|
|
59
|
+
# 2. Claude receives the PRD + issues + fix rules
|
|
60
|
+
# 3. Fixed PRD is validated and saved
|
|
61
|
+
# 4. Timestamped backup preserved (prd.json.20240115-143022.bak)
|
|
62
|
+
#
|
|
63
|
+
# If Claude is unavailable or fix fails, loop continues with warnings.
|
|
64
|
+
#
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# CONFIGURATION
|
|
67
|
+
# ============================================================================
|
|
68
|
+
#
|
|
69
|
+
# .checks.requireTests - Warn if no test directory configured
|
|
70
|
+
# .tests.directory - Where tests live (for requireTests check)
|
|
71
|
+
#
|
|
72
|
+
# ============================================================================
|
|
73
|
+
# USAGE
|
|
74
|
+
# ============================================================================
|
|
75
|
+
#
|
|
76
|
+
# source ralph/prd-check.sh
|
|
77
|
+
#
|
|
78
|
+
# # Full validation with auto-fix
|
|
79
|
+
# validate_prd ".ralph/prd.json"
|
|
80
|
+
#
|
|
81
|
+
# # Quick check without auto-fix (returns issues string)
|
|
82
|
+
# issues=$(validate_stories_quick ".ralph/prd.json")
|
|
83
|
+
#
|
|
84
|
+
# ============================================================================
|
|
85
|
+
|
|
86
|
+
# Validate PRD structure and story quality
|
|
87
|
+
# Returns 0 if valid (possibly after auto-fix), 1 if unrecoverable error
|
|
88
|
+
validate_prd() {
|
|
89
|
+
local prd_file="$1"
|
|
90
|
+
|
|
91
|
+
# Check file exists
|
|
92
|
+
if [[ ! -f "$prd_file" ]]; then
|
|
93
|
+
print_error "PRD file not found: $prd_file"
|
|
94
|
+
return 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Check valid JSON
|
|
98
|
+
if ! jq -e . "$prd_file" >/dev/null 2>&1; then
|
|
99
|
+
print_error "prd.json is not valid JSON."
|
|
100
|
+
echo ""
|
|
101
|
+
echo "Fix it manually or regenerate with:"
|
|
102
|
+
echo " /idea 'your feature'"
|
|
103
|
+
echo ""
|
|
104
|
+
return 1
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Check feature.name is set
|
|
108
|
+
local feature_name
|
|
109
|
+
feature_name=$(jq -r '.feature.name // empty' "$prd_file" 2>/dev/null)
|
|
110
|
+
if [[ -z "$feature_name" || "$feature_name" == "null" ]]; then
|
|
111
|
+
print_error "prd.json is missing .feature.name"
|
|
112
|
+
echo ""
|
|
113
|
+
echo "Add a feature name to your PRD or regenerate with:"
|
|
114
|
+
echo " /idea 'your feature'"
|
|
115
|
+
echo ""
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# Check for stories array
|
|
120
|
+
if ! jq -e '.stories' "$prd_file" >/dev/null 2>&1; then
|
|
121
|
+
print_error "prd.json is missing 'stories' array."
|
|
122
|
+
echo ""
|
|
123
|
+
echo "Regenerate with: /idea 'your feature'"
|
|
124
|
+
echo ""
|
|
125
|
+
return 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Check stories is not empty
|
|
129
|
+
local story_count
|
|
130
|
+
story_count=$(jq '.stories | length' "$prd_file" 2>/dev/null || echo "0")
|
|
131
|
+
if [[ "$story_count" == "0" ]]; then
|
|
132
|
+
print_error "prd.json has no stories."
|
|
133
|
+
echo ""
|
|
134
|
+
echo "Regenerate with: /idea 'your feature'"
|
|
135
|
+
echo ""
|
|
136
|
+
return 1
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Check each story has required fields
|
|
140
|
+
local invalid_stories
|
|
141
|
+
invalid_stories=$(jq -r '.stories[] | select(.id == null or .id == "" or .title == null or .title == "") | .id // "unnamed"' "$prd_file" 2>/dev/null)
|
|
142
|
+
if [[ -n "$invalid_stories" ]]; then
|
|
143
|
+
print_error "Some stories are missing required fields (id, title):"
|
|
144
|
+
echo "$invalid_stories" | head -5
|
|
145
|
+
echo ""
|
|
146
|
+
echo "Fix the PRD or regenerate with: /idea 'your feature'"
|
|
147
|
+
echo ""
|
|
148
|
+
return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Check stories have passes field (initialize if missing)
|
|
152
|
+
local missing_passes
|
|
153
|
+
missing_passes=$(jq '[.stories[] | select(.passes == null)] | length' "$prd_file" 2>/dev/null || echo "0")
|
|
154
|
+
if [[ "$missing_passes" != "0" ]]; then
|
|
155
|
+
print_info "Initializing $missing_passes stories with passes=false..."
|
|
156
|
+
update_json "$prd_file" '(.stories[] | select(.passes == null) | .passes) = false'
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# Check if project has tests (from config)
|
|
160
|
+
local config="$RALPH_DIR/config.json"
|
|
161
|
+
if [[ -f "$config" ]]; then
|
|
162
|
+
local require_tests
|
|
163
|
+
require_tests=$(jq -r '.checks.requireTests // true' "$config" 2>/dev/null)
|
|
164
|
+
local test_dir
|
|
165
|
+
test_dir=$(jq -r '.tests.directory // empty' "$config" 2>/dev/null)
|
|
166
|
+
|
|
167
|
+
if [[ "$require_tests" == "true" && -z "$test_dir" ]]; then
|
|
168
|
+
echo ""
|
|
169
|
+
print_warning "No test directory configured in .ralph/config.json"
|
|
170
|
+
echo " Without tests, Ralph can only verify syntax and API responses."
|
|
171
|
+
echo " Import errors and integration issues won't be caught."
|
|
172
|
+
echo ""
|
|
173
|
+
echo " To fix: Add tests, or set in .ralph/config.json:"
|
|
174
|
+
echo " {\"tests\": {\"directory\": \"src\", \"patterns\": \"*.test.ts\"}}"
|
|
175
|
+
echo " To silence: {\"checks\": {\"requireTests\": false}}"
|
|
176
|
+
echo ""
|
|
177
|
+
fi
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# Replace hardcoded paths with config placeholders
|
|
181
|
+
fix_hardcoded_paths "$prd_file" "$config"
|
|
182
|
+
|
|
183
|
+
# Validate and fix individual stories
|
|
184
|
+
_validate_and_fix_stories "$prd_file" || return 1
|
|
185
|
+
|
|
186
|
+
return 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# ============================================================================
|
|
190
|
+
# INTERNAL FUNCTIONS
|
|
191
|
+
# ============================================================================
|
|
192
|
+
|
|
193
|
+
# Validate individual stories and auto-fix with Claude if needed
|
|
194
|
+
_validate_and_fix_stories() {
|
|
195
|
+
local prd_file="$1"
|
|
196
|
+
local needs_fix=false
|
|
197
|
+
local issues=""
|
|
198
|
+
local story_count=0
|
|
199
|
+
|
|
200
|
+
# Issue counters (bash 3.2 compatible - no associative arrays)
|
|
201
|
+
local cnt_no_tests=0 cnt_backend_curl=0 cnt_backend_contract=0
|
|
202
|
+
local cnt_frontend_tsc=0 cnt_frontend_url=0 cnt_frontend_context=0
|
|
203
|
+
local cnt_auth_security=0 cnt_list_pagination=0 cnt_prose_steps=0
|
|
204
|
+
local cnt_migration_prereq=0
|
|
205
|
+
|
|
206
|
+
echo " Checking test coverage..."
|
|
207
|
+
|
|
208
|
+
# Only validate incomplete stories (skip stories that already passed)
|
|
209
|
+
local story_ids
|
|
210
|
+
story_ids=$(jq -r '.stories[] | select(.passes != true) | .id' "$prd_file" 2>/dev/null)
|
|
211
|
+
|
|
212
|
+
while IFS= read -r story_id; do
|
|
213
|
+
[[ -z "$story_id" ]] && continue
|
|
214
|
+
|
|
215
|
+
local story_issues=""
|
|
216
|
+
local story_type
|
|
217
|
+
story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
|
|
218
|
+
local story_title
|
|
219
|
+
story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
|
|
220
|
+
|
|
221
|
+
# Check 1: testSteps quality
|
|
222
|
+
local test_steps
|
|
223
|
+
test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
|
|
224
|
+
|
|
225
|
+
if [[ -z "$test_steps" ]]; then
|
|
226
|
+
story_issues+="no testSteps, "
|
|
227
|
+
cnt_no_tests=$((cnt_no_tests + 1))
|
|
228
|
+
else
|
|
229
|
+
# Check test steps are executable commands, not prose
|
|
230
|
+
# Good: "curl -s POST /api/login | jq -e '.token'"
|
|
231
|
+
# Bad: "Verify the user can log in successfully"
|
|
232
|
+
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
|
|
233
|
+
story_issues+="testSteps look like prose (need executable commands), "
|
|
234
|
+
cnt_prose_steps=$((cnt_prose_steps + 1))
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
# Type-specific checks
|
|
238
|
+
if [[ "$story_type" == "backend" ]]; then
|
|
239
|
+
# Backend must have curl, not just npm test/pytest
|
|
240
|
+
if ! echo "$test_steps" | grep -q "curl "; then
|
|
241
|
+
story_issues+="backend needs curl tests, "
|
|
242
|
+
cnt_backend_curl=$((cnt_backend_curl + 1))
|
|
243
|
+
fi
|
|
244
|
+
elif [[ "$story_type" == "frontend" ]]; then
|
|
245
|
+
# Frontend must have tsc or playwright
|
|
246
|
+
if ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
|
|
247
|
+
story_issues+="frontend needs tsc/playwright tests, "
|
|
248
|
+
cnt_frontend_tsc=$((cnt_frontend_tsc + 1))
|
|
249
|
+
fi
|
|
250
|
+
fi
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
# Check 2: Backend needs apiContract
|
|
254
|
+
if [[ "$story_type" == "backend" ]]; then
|
|
255
|
+
local has_contract
|
|
256
|
+
has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
|
|
257
|
+
if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
|
|
258
|
+
story_issues+="missing apiContract, "
|
|
259
|
+
cnt_backend_contract=$((cnt_backend_contract + 1))
|
|
260
|
+
fi
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# Check 3: Frontend needs testUrl and contextFiles
|
|
264
|
+
if [[ "$story_type" == "frontend" ]]; then
|
|
265
|
+
local has_url
|
|
266
|
+
has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
|
|
267
|
+
if [[ -z "$has_url" || "$has_url" == "null" ]]; then
|
|
268
|
+
story_issues+="missing testUrl, "
|
|
269
|
+
cnt_frontend_url=$((cnt_frontend_url + 1))
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
local context_files
|
|
273
|
+
context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
|
|
274
|
+
if [[ "$context_files" == "0" ]]; then
|
|
275
|
+
story_issues+="missing contextFiles, "
|
|
276
|
+
cnt_frontend_context=$((cnt_frontend_context + 1))
|
|
277
|
+
fi
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# Check 4: Auth stories need security criteria
|
|
281
|
+
if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
|
|
282
|
+
local criteria
|
|
283
|
+
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
284
|
+
if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
|
|
285
|
+
story_issues+="missing security criteria, "
|
|
286
|
+
cnt_auth_security=$((cnt_auth_security + 1))
|
|
287
|
+
fi
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
# Check 5: List endpoints need scale criteria
|
|
291
|
+
# Note: "search" excluded - search endpoints often return single/filtered results
|
|
292
|
+
if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
|
|
293
|
+
local criteria
|
|
294
|
+
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
295
|
+
if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
|
|
296
|
+
story_issues+="missing pagination criteria, "
|
|
297
|
+
cnt_list_pagination=$((cnt_list_pagination + 1))
|
|
298
|
+
fi
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
# Check 6: Migration stories need DB prerequisites
|
|
302
|
+
# If story creates migration files or modifies models, it needs resetDb prerequisite
|
|
303
|
+
local story_files
|
|
304
|
+
story_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.files.create // []) + (.files.modify // []) | join(" ")' "$prd_file")
|
|
305
|
+
if echo "$story_files" | grep -qiE "(alembic/versions|migrations/|\.migration\.|models\.py|models/|schema\.)"; then
|
|
306
|
+
local has_prereq
|
|
307
|
+
has_prereq=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .prerequisites // [] | length' "$prd_file")
|
|
308
|
+
if [[ "$has_prereq" == "0" ]]; then
|
|
309
|
+
story_issues+="migration story needs prerequisites (DB reset), "
|
|
310
|
+
cnt_migration_prereq=$((cnt_migration_prereq + 1))
|
|
311
|
+
fi
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
# Track this story if it has issues
|
|
315
|
+
if [[ -n "$story_issues" ]]; then
|
|
316
|
+
needs_fix=true
|
|
317
|
+
story_count=$((story_count + 1))
|
|
318
|
+
issues+="$story_id: ${story_issues%%, }
|
|
319
|
+
"
|
|
320
|
+
fi
|
|
321
|
+
done <<< "$story_ids"
|
|
322
|
+
|
|
323
|
+
# If issues found, show summary and attempt fix
|
|
324
|
+
if [[ "$needs_fix" == "true" ]]; then
|
|
325
|
+
echo " Optimizing test coverage for $story_count stories..."
|
|
326
|
+
|
|
327
|
+
# Print compact summary (only non-zero counts)
|
|
328
|
+
[[ $cnt_no_tests -gt 0 ]] && echo " ${cnt_no_tests}x missing testSteps"
|
|
329
|
+
[[ $cnt_prose_steps -gt 0 ]] && echo " ${cnt_prose_steps}x testSteps are prose (need executable commands)"
|
|
330
|
+
[[ $cnt_backend_curl -gt 0 ]] && echo " ${cnt_backend_curl}x backend: add curl tests"
|
|
331
|
+
[[ $cnt_backend_contract -gt 0 ]] && echo " ${cnt_backend_contract}x backend: add apiContract"
|
|
332
|
+
[[ $cnt_frontend_tsc -gt 0 ]] && echo " ${cnt_frontend_tsc}x frontend: add tsc/playwright"
|
|
333
|
+
[[ $cnt_frontend_url -gt 0 ]] && echo " ${cnt_frontend_url}x frontend: add testUrl"
|
|
334
|
+
[[ $cnt_frontend_context -gt 0 ]] && echo " ${cnt_frontend_context}x frontend: add contextFiles"
|
|
335
|
+
[[ $cnt_auth_security -gt 0 ]] && echo " ${cnt_auth_security}x auth: add security criteria"
|
|
336
|
+
[[ $cnt_list_pagination -gt 0 ]] && echo " ${cnt_list_pagination}x list: add pagination"
|
|
337
|
+
[[ $cnt_migration_prereq -gt 0 ]] && echo " ${cnt_migration_prereq}x migration: add prerequisites (DB reset)"
|
|
338
|
+
|
|
339
|
+
# Check if Claude is available for auto-fix
|
|
340
|
+
if command -v claude &>/dev/null; then
|
|
341
|
+
_fix_stories_with_claude "$prd_file" "$issues"
|
|
342
|
+
else
|
|
343
|
+
print_warning "Claude CLI not found - run manually to optimize test coverage"
|
|
344
|
+
return 1
|
|
345
|
+
fi
|
|
346
|
+
else
|
|
347
|
+
print_success "Test coverage looks good"
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
return 0
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# Optimize story test coverage using Claude
|
|
354
|
+
_fix_stories_with_claude() {
|
|
355
|
+
local prd_file="$1"
|
|
356
|
+
local issues="$2"
|
|
357
|
+
|
|
358
|
+
local fix_prompt="Enhance test coverage for these stories. Output the COMPLETE updated prd.json.
|
|
359
|
+
|
|
360
|
+
STORIES TO OPTIMIZE:
|
|
361
|
+
$issues
|
|
362
|
+
|
|
363
|
+
RULES:
|
|
364
|
+
1. Backend stories MUST have testSteps with curl commands that hit real endpoints
|
|
365
|
+
Example: curl -s -X POST {config.urls.backend}/api/users -d '...' | jq -e '.id'
|
|
366
|
+
2. Backend stories MUST have apiContract with endpoint, request, response
|
|
367
|
+
3. Frontend stories MUST have testUrl set to {config.urls.frontend}/page
|
|
368
|
+
4. Frontend stories MUST have contextFiles array (include idea file path from originalContext)
|
|
369
|
+
5. Auth stories MUST have security acceptanceCriteria:
|
|
370
|
+
- Passwords hashed with bcrypt (cost 10+)
|
|
371
|
+
- Passwords NEVER in API responses
|
|
372
|
+
- Rate limiting on login attempts
|
|
373
|
+
6. List endpoints MUST have pagination acceptanceCriteria:
|
|
374
|
+
- Returns paginated results (max 100 per page)
|
|
375
|
+
- Accepts ?page=N&limit=N query params
|
|
376
|
+
7. Migration stories (creating alembic/versions, migrations/, or modifying models) MUST have prerequisites:
|
|
377
|
+
Example: \"prerequisites\": [{\"name\": \"Reset test DB\", \"command\": \"npm run db:reset:test\", \"when\": \"schema changes\"}]
|
|
378
|
+
|
|
379
|
+
CURRENT PRD:
|
|
380
|
+
$(cat "$prd_file")
|
|
381
|
+
|
|
382
|
+
Output ONLY the fixed JSON, no explanation. Start with { and end with }."
|
|
383
|
+
|
|
384
|
+
local raw_response
|
|
385
|
+
raw_response=$(echo "$fix_prompt" | run_with_timeout "$CODE_REVIEW_TIMEOUT_SECONDS" claude -p 2>/dev/null)
|
|
386
|
+
|
|
387
|
+
# Extract JSON from response (Claude often wraps in markdown code fences)
|
|
388
|
+
local fixed_prd
|
|
389
|
+
# First strip markdown code fences if present
|
|
390
|
+
fixed_prd=$(echo "$raw_response" | sed 's/^```json//; s/^```$//' | sed -n '/^[[:space:]]*{/,/^[[:space:]]*}[[:space:]]*$/p' | head -1000)
|
|
391
|
+
|
|
392
|
+
# If sed extraction failed, try removing fences and using raw
|
|
393
|
+
if [[ -z "$fixed_prd" ]]; then
|
|
394
|
+
fixed_prd=$(echo "$raw_response" | sed 's/^```json//; s/^```//; s/```$//')
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
# Validate the response is valid JSON with required structure
|
|
398
|
+
if echo "$fixed_prd" | jq -e '.stories' >/dev/null 2>&1; then
|
|
399
|
+
# Timestamped backup (preserves history across multiple fixes)
|
|
400
|
+
local backup_file="${prd_file}.$(date +%Y%m%d-%H%M%S).bak"
|
|
401
|
+
cp "$prd_file" "$backup_file"
|
|
402
|
+
|
|
403
|
+
# Write fixed PRD
|
|
404
|
+
echo "$fixed_prd" > "$prd_file"
|
|
405
|
+
print_success "Test coverage optimized (backup at $backup_file)"
|
|
406
|
+
|
|
407
|
+
# Re-validate to confirm
|
|
408
|
+
local remaining_issues
|
|
409
|
+
remaining_issues=$(validate_stories_quick "$prd_file")
|
|
410
|
+
if [[ -n "$remaining_issues" ]]; then
|
|
411
|
+
echo " Some stories may need manual review"
|
|
412
|
+
fi
|
|
413
|
+
else
|
|
414
|
+
print_warning "Could not auto-optimize - continuing with current PRD"
|
|
415
|
+
return 0 # Don't fail, just continue
|
|
416
|
+
fi
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
# Quick validation without auto-fix (for re-checking after fix)
|
|
420
|
+
# Returns issues string (empty if all good)
|
|
421
|
+
validate_stories_quick() {
|
|
422
|
+
local prd_file="$1"
|
|
423
|
+
local issues=""
|
|
424
|
+
|
|
425
|
+
# Only check incomplete stories
|
|
426
|
+
local story_ids
|
|
427
|
+
story_ids=$(jq -r '.stories[] | select(.passes != true) | .id' "$prd_file" 2>/dev/null)
|
|
428
|
+
|
|
429
|
+
while IFS= read -r story_id; do
|
|
430
|
+
[[ -z "$story_id" ]] && continue
|
|
431
|
+
|
|
432
|
+
local story_type
|
|
433
|
+
story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
|
|
434
|
+
local story_title
|
|
435
|
+
story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
|
|
436
|
+
local test_steps
|
|
437
|
+
test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
|
|
438
|
+
|
|
439
|
+
# Check 1: testSteps quality
|
|
440
|
+
if [[ "$story_type" == "backend" ]] && ! echo "$test_steps" | grep -q "curl "; then
|
|
441
|
+
issues+="$story_id: missing curl tests, "
|
|
442
|
+
fi
|
|
443
|
+
if [[ "$story_type" == "frontend" ]] && ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
|
|
444
|
+
issues+="$story_id: missing tsc/playwright tests, "
|
|
445
|
+
fi
|
|
446
|
+
|
|
447
|
+
# Check 2: Backend needs apiContract
|
|
448
|
+
if [[ "$story_type" == "backend" ]]; then
|
|
449
|
+
local has_contract
|
|
450
|
+
has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
|
|
451
|
+
if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
|
|
452
|
+
issues+="$story_id: missing apiContract, "
|
|
453
|
+
fi
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
# Check 3: Frontend needs testUrl and contextFiles
|
|
457
|
+
if [[ "$story_type" == "frontend" ]]; then
|
|
458
|
+
local has_url
|
|
459
|
+
has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
|
|
460
|
+
[[ -z "$has_url" || "$has_url" == "null" ]] && issues+="$story_id: missing testUrl, "
|
|
461
|
+
|
|
462
|
+
local context_files
|
|
463
|
+
context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
|
|
464
|
+
[[ "$context_files" == "0" ]] && issues+="$story_id: missing contextFiles, "
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
# Check 4: Auth stories need security criteria
|
|
468
|
+
if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
|
|
469
|
+
local criteria
|
|
470
|
+
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
471
|
+
if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
|
|
472
|
+
issues+="$story_id: missing security criteria, "
|
|
473
|
+
fi
|
|
474
|
+
fi
|
|
475
|
+
|
|
476
|
+
# Check 5: List endpoints need scale criteria
|
|
477
|
+
if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
|
|
478
|
+
local criteria
|
|
479
|
+
criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
|
|
480
|
+
if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
|
|
481
|
+
issues+="$story_id: missing pagination criteria, "
|
|
482
|
+
fi
|
|
483
|
+
fi
|
|
484
|
+
|
|
485
|
+
# Check 6: Migration stories need prerequisites
|
|
486
|
+
local story_files
|
|
487
|
+
story_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.files.create // []) + (.files.modify // []) | join(" ")' "$prd_file")
|
|
488
|
+
if echo "$story_files" | grep -qiE "(alembic/versions|migrations/|\.migration\.|models\.py|models/|schema\.)"; then
|
|
489
|
+
local has_prereq
|
|
490
|
+
has_prereq=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .prerequisites // [] | length' "$prd_file")
|
|
491
|
+
if [[ "$has_prereq" == "0" ]]; then
|
|
492
|
+
issues+="$story_id: migration needs prerequisites, "
|
|
493
|
+
fi
|
|
494
|
+
fi
|
|
495
|
+
done <<< "$story_ids"
|
|
496
|
+
|
|
497
|
+
echo "$issues"
|
|
498
|
+
}
|