codeharness 0.9.0 → 0.10.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/dist/index.js +3 -3
- package/package.json +1 -1
- package/ralph/AGENTS.md +17 -7
- package/ralph/drivers/claude-code.sh +3 -0
- package/ralph/harness_status.sh +0 -9
- package/ralph/ralph.sh +97 -16
- package/ralph/verify_gates.sh +0 -31
- package/ralph/doc_gardener.sh +0 -352
- package/ralph/retro.sh +0 -298
package/dist/index.js
CHANGED
|
@@ -1348,7 +1348,7 @@ function importStoriesToBeads(stories, opts, beadsFns) {
|
|
|
1348
1348
|
}
|
|
1349
1349
|
|
|
1350
1350
|
// src/commands/init.ts
|
|
1351
|
-
var HARNESS_VERSION = true ? "0.
|
|
1351
|
+
var HARNESS_VERSION = true ? "0.10.0" : "0.0.0-dev";
|
|
1352
1352
|
function getStackLabel(stack) {
|
|
1353
1353
|
if (stack === "nodejs") return "Node.js (package.json)";
|
|
1354
1354
|
if (stack === "python") return "Python";
|
|
@@ -2408,7 +2408,7 @@ function buildSpawnArgs(opts) {
|
|
|
2408
2408
|
return args;
|
|
2409
2409
|
}
|
|
2410
2410
|
function registerRunCommand(program) {
|
|
2411
|
-
program.command("run").description("Execute the autonomous coding loop").option("--max-iterations <n>", "Maximum loop iterations", "50").option("--timeout <seconds>", "Total loop timeout in seconds", "14400").option("--iteration-timeout <minutes>", "Per-iteration timeout in minutes", "
|
|
2411
|
+
program.command("run").description("Execute the autonomous coding loop").option("--max-iterations <n>", "Maximum loop iterations", "50").option("--timeout <seconds>", "Total loop timeout in seconds", "14400").option("--iteration-timeout <minutes>", "Per-iteration timeout in minutes", "30").option("--live", "Show live output streaming", false).option("--calls <n>", "Max API calls per hour", "100").option("--max-story-retries <n>", "Max retries per story before flagging", "3").action(async (options, cmd) => {
|
|
2412
2412
|
const globalOpts = cmd.optsWithGlobals();
|
|
2413
2413
|
const isJson = !!globalOpts.json;
|
|
2414
2414
|
const outputOpts = { json: isJson };
|
|
@@ -6783,7 +6783,7 @@ function registerGithubImportCommand(program) {
|
|
|
6783
6783
|
}
|
|
6784
6784
|
|
|
6785
6785
|
// src/index.ts
|
|
6786
|
-
var VERSION = true ? "0.
|
|
6786
|
+
var VERSION = true ? "0.10.0" : "0.0.0-dev";
|
|
6787
6787
|
function createProgram() {
|
|
6788
6788
|
const program = new Command();
|
|
6789
6789
|
program.name("codeharness").description("Makes autonomous coding agents produce software that actually works").version(VERSION).option("--json", "Output in machine-readable JSON format");
|
package/package.json
CHANGED
package/ralph/AGENTS.md
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
# ralph/
|
|
2
2
|
|
|
3
|
-
Vendored autonomous execution loop. Spawns fresh Claude Code instances per iteration with verification gates, circuit breaker protection, and crash recovery.
|
|
3
|
+
Vendored autonomous execution loop. Spawns fresh Claude Code instances per iteration with verification gates, circuit breaker protection, and crash recovery. Each iteration runs `/harness-run` which owns story lifecycle, verification, and session retrospective.
|
|
4
4
|
|
|
5
5
|
## Key Files
|
|
6
6
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|------|---------|
|
|
9
|
-
| ralph.sh | Core loop — iteration,
|
|
10
|
-
| bridge.sh | BMAD→Ralph task bridge — converts epics to progress.json |
|
|
9
|
+
| ralph.sh | Core loop — iteration, retry tracking, progress reporting, termination |
|
|
10
|
+
| bridge.sh | BMAD→Ralph task bridge — converts epics to progress.json (legacy) |
|
|
11
11
|
| verify_gates.sh | Per-story verification gate checks (4 gates) |
|
|
12
|
-
| drivers/claude-code.sh | Claude Code instance lifecycle
|
|
12
|
+
| drivers/claude-code.sh | Claude Code instance lifecycle, allowed tools, command building |
|
|
13
|
+
| harness_status.sh | Sprint status display via CLI |
|
|
13
14
|
| lib/date_utils.sh | Cross-platform date/timestamp utilities |
|
|
14
15
|
| lib/timeout_utils.sh | Cross-platform timeout command detection |
|
|
15
16
|
| lib/circuit_breaker.sh | Stagnation detection (CLOSED→HALF_OPEN→OPEN) |
|
|
16
17
|
|
|
17
18
|
## Dependencies
|
|
18
19
|
|
|
19
|
-
- `jq`: JSON processing for
|
|
20
|
+
- `jq`: JSON processing for status files
|
|
20
21
|
- `gtimeout`/`timeout`: Per-iteration timeout protection
|
|
21
22
|
- `git`: Progress detection via commit diff
|
|
22
23
|
|
|
@@ -24,10 +25,19 @@ Vendored autonomous execution loop. Spawns fresh Claude Code instances per itera
|
|
|
24
25
|
|
|
25
26
|
- All scripts use `set -e` and are POSIX-compatible bash
|
|
26
27
|
- Driver pattern: `drivers/{name}.sh` implements the driver interface
|
|
27
|
-
-
|
|
28
|
-
-
|
|
28
|
+
- Primary task source: `_bmad-output/implementation-artifacts/sprint-status.yaml`
|
|
29
|
+
- State files: `status.json` (loop state), `.story_retries` (per-story retry counts), `.flagged_stories` (exceeded retry limit)
|
|
30
|
+
- Logs written to `logs/ralph.log` and `logs/claude_output_*.log`
|
|
29
31
|
- Scripts guard main execution with `[[ "${BASH_SOURCE[0]}" == "${0}" ]]`
|
|
30
32
|
|
|
33
|
+
## Post-Iteration Output
|
|
34
|
+
|
|
35
|
+
After each iteration, Ralph prints:
|
|
36
|
+
- Completed stories with titles and proof file paths
|
|
37
|
+
- Progress summary with next story in queue
|
|
38
|
+
- Session issues (from `.session-issues.md` written by subagents)
|
|
39
|
+
- Session retro highlights (action items from `session-retro-{date}.md`)
|
|
40
|
+
|
|
31
41
|
## Testing
|
|
32
42
|
|
|
33
43
|
```bash
|
package/ralph/harness_status.sh
CHANGED
|
@@ -130,15 +130,6 @@ if [[ -f "$PROGRESS_FILE" ]]; then
|
|
|
130
130
|
|
|
131
131
|
echo ""
|
|
132
132
|
|
|
133
|
-
# Verification summary
|
|
134
|
-
VLOG="$PROJECT_DIR/ralph/verification-log.json"
|
|
135
|
-
if [[ -f "$VLOG" ]]; then
|
|
136
|
-
v_total=$(jq '.events | length' "$VLOG" 2>/dev/null || echo "0")
|
|
137
|
-
v_pass=$(jq '[.events[] | select(.result == "pass")] | length' "$VLOG" 2>/dev/null || echo "0")
|
|
138
|
-
echo " Verification: $v_pass passed / $v_total checks"
|
|
139
|
-
echo ""
|
|
140
|
-
fi
|
|
141
|
-
|
|
142
133
|
# Next action
|
|
143
134
|
current=$(jq -r '.tasks[] | select(.status == "pending" or .status == "in_progress") | .id' "$PROGRESS_FILE" 2>/dev/null | head -1)
|
|
144
135
|
if [[ -n "$current" && "$current" != "null" ]]; then
|
package/ralph/ralph.sh
CHANGED
|
@@ -38,7 +38,7 @@ LOG_DIR=""
|
|
|
38
38
|
MAX_ITERATIONS=${MAX_ITERATIONS:-50}
|
|
39
39
|
MAX_STORY_RETRIES=${MAX_STORY_RETRIES:-3}
|
|
40
40
|
LOOP_TIMEOUT_SECONDS=${LOOP_TIMEOUT_SECONDS:-14400} # 4 hours default
|
|
41
|
-
ITERATION_TIMEOUT_MINUTES=${ITERATION_TIMEOUT_MINUTES:-
|
|
41
|
+
ITERATION_TIMEOUT_MINUTES=${ITERATION_TIMEOUT_MINUTES:-30}
|
|
42
42
|
|
|
43
43
|
# Rate limiting
|
|
44
44
|
MAX_CALLS_PER_HOUR=${MAX_CALLS_PER_HOUR:-100}
|
|
@@ -447,6 +447,7 @@ print_progress_summary() {
|
|
|
447
447
|
counts=$(get_task_counts)
|
|
448
448
|
local total=${counts%% *}
|
|
449
449
|
local completed=${counts##* }
|
|
450
|
+
local remaining=$((total - completed))
|
|
450
451
|
local elapsed=$(( $(date +%s) - loop_start_time ))
|
|
451
452
|
local elapsed_fmt
|
|
452
453
|
|
|
@@ -458,7 +459,62 @@ print_progress_summary() {
|
|
|
458
459
|
elapsed_fmt="${elapsed}s"
|
|
459
460
|
fi
|
|
460
461
|
|
|
461
|
-
log_status "INFO" "Progress: ${completed}/${total}
|
|
462
|
+
log_status "INFO" "Progress: ${completed}/${total} done, ${remaining} remaining (iterations: ${loop_count}, elapsed: ${elapsed_fmt})"
|
|
463
|
+
|
|
464
|
+
# Show the next story in line (first non-done, non-flagged)
|
|
465
|
+
if [[ -f "$SPRINT_STATUS_FILE" ]]; then
|
|
466
|
+
local next_story=""
|
|
467
|
+
while IFS=: read -r key value; do
|
|
468
|
+
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
469
|
+
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
470
|
+
[[ -z "$key" || "$key" == \#* ]] && continue
|
|
471
|
+
if [[ "$key" =~ ^[0-9]+-[0-9]+- && "$value" != "done" ]]; then
|
|
472
|
+
if ! is_story_flagged "$key"; then
|
|
473
|
+
next_story="$key ($value)"
|
|
474
|
+
break
|
|
475
|
+
fi
|
|
476
|
+
fi
|
|
477
|
+
done < "$SPRINT_STATUS_FILE"
|
|
478
|
+
if [[ -n "$next_story" ]]; then
|
|
479
|
+
log_status "INFO" "Next up: ${next_story}"
|
|
480
|
+
fi
|
|
481
|
+
fi
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
# ─── Iteration Insights ──────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
print_iteration_insights() {
|
|
487
|
+
local project_root
|
|
488
|
+
project_root="$(pwd)"
|
|
489
|
+
local issues_file="$project_root/_bmad-output/implementation-artifacts/.session-issues.md"
|
|
490
|
+
local today
|
|
491
|
+
today=$(date +%Y-%m-%d)
|
|
492
|
+
local retro_file="$project_root/_bmad-output/implementation-artifacts/session-retro-${today}.md"
|
|
493
|
+
|
|
494
|
+
# Show session issues (last 20 lines — most recent subagent)
|
|
495
|
+
if [[ -f "$issues_file" ]]; then
|
|
496
|
+
local issue_count
|
|
497
|
+
issue_count=$(grep -c '^### ' "$issues_file" 2>/dev/null || echo "0")
|
|
498
|
+
if [[ $issue_count -gt 0 ]]; then
|
|
499
|
+
echo ""
|
|
500
|
+
log_status "INFO" "━━━ Session Issues ($issue_count entries) ━━━"
|
|
501
|
+
# Print the last subagent's issues block
|
|
502
|
+
awk '/^### /{block=""} {block=block $0 "\n"} END{printf "%s", block}' "$issues_file" | head -15
|
|
503
|
+
echo ""
|
|
504
|
+
fi
|
|
505
|
+
fi
|
|
506
|
+
|
|
507
|
+
# Show retro summary if generated
|
|
508
|
+
if [[ -f "$retro_file" ]]; then
|
|
509
|
+
log_status "INFO" "━━━ Session Retro ━━━"
|
|
510
|
+
# Print action items section if present, otherwise first 10 lines
|
|
511
|
+
if grep -q '## Action items\|## Action Items' "$retro_file" 2>/dev/null; then
|
|
512
|
+
sed -n '/^## Action [Ii]tems/,/^## /p' "$retro_file" | head -20
|
|
513
|
+
else
|
|
514
|
+
head -10 "$retro_file"
|
|
515
|
+
fi
|
|
516
|
+
echo ""
|
|
517
|
+
fi
|
|
462
518
|
}
|
|
463
519
|
|
|
464
520
|
# ─── Driver Management ──────────────────────────────────────────────────────
|
|
@@ -474,6 +530,13 @@ load_platform_driver() {
|
|
|
474
530
|
source "$driver_file"
|
|
475
531
|
|
|
476
532
|
driver_valid_tools
|
|
533
|
+
|
|
534
|
+
# Auto-populate CLAUDE_ALLOWED_TOOLS from driver's valid tool patterns
|
|
535
|
+
# so Ralph runs autonomously without permission prompts
|
|
536
|
+
if [[ -z "$CLAUDE_ALLOWED_TOOLS" && ${#VALID_TOOL_PATTERNS[@]} -gt 0 ]]; then
|
|
537
|
+
CLAUDE_ALLOWED_TOOLS=$(IFS=','; echo "${VALID_TOOL_PATTERNS[*]}")
|
|
538
|
+
fi
|
|
539
|
+
|
|
477
540
|
log_status "INFO" "Platform driver: $(driver_display_name) ($(driver_cli_binary))"
|
|
478
541
|
}
|
|
479
542
|
|
|
@@ -496,8 +559,10 @@ execute_iteration() {
|
|
|
496
559
|
log_status "LOOP" "Iteration $iteration — Task: ${task_id:-'(reading from prompt)'}"
|
|
497
560
|
local timeout_seconds=$((ITERATION_TIMEOUT_MINUTES * 60))
|
|
498
561
|
|
|
499
|
-
# Build loop context
|
|
500
|
-
local
|
|
562
|
+
# Build loop context — pass time budget so the session can prioritize retro
|
|
563
|
+
local start_time
|
|
564
|
+
start_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
565
|
+
local loop_context="Loop #${iteration}. Time budget: ${ITERATION_TIMEOUT_MINUTES} minutes (started: ${start_time}). Reserve the last 5 minutes for Step 8 (session retrospective) — do not start new story work if less than 10 minutes remain."
|
|
501
566
|
if [[ -n "$task_id" ]]; then
|
|
502
567
|
loop_context+=" Current task: $task_id."
|
|
503
568
|
fi
|
|
@@ -509,6 +574,10 @@ execute_iteration() {
|
|
|
509
574
|
return 1
|
|
510
575
|
fi
|
|
511
576
|
|
|
577
|
+
# Write deadline file for time-warning hook
|
|
578
|
+
local deadline=$(( $(date +%s) + timeout_seconds ))
|
|
579
|
+
echo "$deadline" > "ralph/.iteration_deadline"
|
|
580
|
+
|
|
512
581
|
log_status "INFO" "Starting $(driver_display_name) (timeout: ${ITERATION_TIMEOUT_MINUTES}m)..."
|
|
513
582
|
|
|
514
583
|
# Execute with timeout
|
|
@@ -648,7 +717,7 @@ Options:
|
|
|
648
717
|
--max-iterations NUM Maximum loop iterations (default: 50)
|
|
649
718
|
--max-story-retries NUM Max retries per story before flagging (default: 3)
|
|
650
719
|
--timeout SECONDS Total loop timeout in seconds (default: 14400 = 4h)
|
|
651
|
-
--iteration-timeout MIN Per-iteration timeout in minutes (default:
|
|
720
|
+
--iteration-timeout MIN Per-iteration timeout in minutes (default: 30)
|
|
652
721
|
--calls NUM Max API calls per hour (default: 100)
|
|
653
722
|
--prompt FILE Prompt file for each iteration
|
|
654
723
|
--progress FILE Progress file (tasks JSON)
|
|
@@ -834,11 +903,12 @@ main() {
|
|
|
834
903
|
after_snapshot=$(snapshot_story_statuses)
|
|
835
904
|
detect_story_changes "$before_snapshot" "$after_snapshot"
|
|
836
905
|
|
|
837
|
-
#
|
|
906
|
+
# Only increment retry for the FIRST non-done, non-flagged story
|
|
907
|
+
# (the one harness-run would have picked up). Other stories were
|
|
908
|
+
# never attempted — don't penalise them for not progressing.
|
|
838
909
|
if [[ -n "$UNCHANGED_STORIES" ]]; then
|
|
839
910
|
while IFS= read -r skey; do
|
|
840
911
|
[[ -z "$skey" ]] && continue
|
|
841
|
-
# Skip already-flagged stories
|
|
842
912
|
if is_story_flagged "$skey"; then
|
|
843
913
|
continue
|
|
844
914
|
fi
|
|
@@ -850,13 +920,29 @@ main() {
|
|
|
850
920
|
else
|
|
851
921
|
log_status "WARN" "Story ${skey} — retry ${retry_count}/${MAX_STORY_RETRIES}"
|
|
852
922
|
fi
|
|
923
|
+
break # only retry the first actionable story
|
|
853
924
|
done <<< "$UNCHANGED_STORIES"
|
|
854
925
|
fi
|
|
855
926
|
|
|
856
927
|
if [[ -n "$CHANGED_STORIES" ]]; then
|
|
857
928
|
while IFS= read -r skey; do
|
|
858
929
|
[[ -z "$skey" ]] && continue
|
|
859
|
-
|
|
930
|
+
# Extract story title from story file if available
|
|
931
|
+
local story_file="$project_root/_bmad-output/implementation-artifacts/${skey}.md"
|
|
932
|
+
local story_title=""
|
|
933
|
+
if [[ -f "$story_file" ]]; then
|
|
934
|
+
story_title=$(grep -m1 '^# \|^## Story' "$story_file" 2>/dev/null | sed 's/^#* *//' | head -c 60)
|
|
935
|
+
fi
|
|
936
|
+
local proof_file="$project_root/verification/${skey}-proof.md"
|
|
937
|
+
local proof_info=""
|
|
938
|
+
if [[ -f "$proof_file" ]]; then
|
|
939
|
+
proof_info=" [proof: verification/${skey}-proof.md]"
|
|
940
|
+
fi
|
|
941
|
+
if [[ -n "$story_title" ]]; then
|
|
942
|
+
log_status "SUCCESS" "Story ${skey}: DONE — ${story_title}${proof_info}"
|
|
943
|
+
else
|
|
944
|
+
log_status "SUCCESS" "Story ${skey}: DONE${proof_info}"
|
|
945
|
+
fi
|
|
860
946
|
done <<< "$CHANGED_STORIES"
|
|
861
947
|
fi
|
|
862
948
|
|
|
@@ -892,6 +978,9 @@ main() {
|
|
|
892
978
|
# Print progress summary after every iteration
|
|
893
979
|
print_progress_summary
|
|
894
980
|
|
|
981
|
+
# ── Show session issues and retro highlights ──
|
|
982
|
+
print_iteration_insights
|
|
983
|
+
|
|
895
984
|
log_status "LOOP" "=== End Iteration #$loop_count ==="
|
|
896
985
|
done
|
|
897
986
|
|
|
@@ -919,14 +1008,6 @@ main() {
|
|
|
919
1008
|
"$(if [[ $completed -eq $total && $total -gt 0 ]]; then echo "completed"; else echo "stopped"; fi)" \
|
|
920
1009
|
"completed:$completed/$total"
|
|
921
1010
|
|
|
922
|
-
# Mandatory retrospective — cannot be skipped
|
|
923
|
-
log_status "INFO" "Triggering mandatory sprint retrospective..."
|
|
924
|
-
if [[ -f "$SCRIPT_DIR/retro.sh" ]]; then
|
|
925
|
-
local project_root
|
|
926
|
-
project_root="$(pwd)"
|
|
927
|
-
"$SCRIPT_DIR/retro.sh" --project-dir "$project_root" 2>&1 || \
|
|
928
|
-
log_status "WARN" "Retro report generation failed"
|
|
929
|
-
fi
|
|
930
1011
|
}
|
|
931
1012
|
|
|
932
1013
|
# ─── CLI Parsing ─────────────────────────────────────────────────────────────
|
package/ralph/verify_gates.sh
CHANGED
|
@@ -162,39 +162,10 @@ increment_iteration() {
|
|
|
162
162
|
fi
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
# ─── Verification log ─────────────────────────────────────────────────────
|
|
166
|
-
|
|
167
|
-
append_verification_log() {
|
|
168
|
-
local result="$1"
|
|
169
|
-
local log_file="$PROJECT_DIR/ralph/verification-log.json"
|
|
170
|
-
local timestamp
|
|
171
|
-
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
172
|
-
|
|
173
|
-
local event
|
|
174
|
-
event=$(jq -n \
|
|
175
|
-
--arg story_id "$STORY_ID" \
|
|
176
|
-
--arg result "$result" \
|
|
177
|
-
--argjson gates_passed "$GATES_PASSED" \
|
|
178
|
-
--argjson gates_total "$GATES_TOTAL" \
|
|
179
|
-
--arg timestamp "$timestamp" \
|
|
180
|
-
'{story_id: $story_id, result: $result, gates_passed: $gates_passed, gates_total: $gates_total, timestamp: $timestamp}')
|
|
181
|
-
|
|
182
|
-
if [[ -f "$log_file" ]]; then
|
|
183
|
-
log_data=$(jq --argjson event "$event" '.events += [$event]' "$log_file" 2>/dev/null)
|
|
184
|
-
if [[ -n "$log_data" ]]; then
|
|
185
|
-
echo "$log_data" > "$log_file"
|
|
186
|
-
fi
|
|
187
|
-
else
|
|
188
|
-
jq -n --argjson event "$event" '{events: [$event]}' > "$log_file"
|
|
189
|
-
fi
|
|
190
|
-
}
|
|
191
|
-
|
|
192
165
|
# ─── Decision ─────────────────────────────────────────────────────────────
|
|
193
166
|
|
|
194
167
|
if [[ $GATES_PASSED -eq $GATES_TOTAL ]]; then
|
|
195
168
|
# Log pass event
|
|
196
|
-
append_verification_log "pass"
|
|
197
|
-
|
|
198
169
|
# All gates pass — mark story complete
|
|
199
170
|
local_updated=$(jq --arg id "$STORY_ID" \
|
|
200
171
|
'(.tasks[] | select(.id == $id)).status = "complete"' \
|
|
@@ -224,8 +195,6 @@ if [[ $GATES_PASSED -eq $GATES_TOTAL ]]; then
|
|
|
224
195
|
exit 0
|
|
225
196
|
else
|
|
226
197
|
# Log fail event
|
|
227
|
-
append_verification_log "fail"
|
|
228
|
-
|
|
229
198
|
# Gates failed — increment iteration, report failures
|
|
230
199
|
increment_iteration
|
|
231
200
|
|
package/ralph/doc_gardener.sh
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# doc_gardener.sh — Documentation freshness scanner
|
|
3
|
-
# Finds: missing AGENTS.md, stale AGENTS.md, stale exec-plans.
|
|
4
|
-
# Used by the doc-gardener subagent and during retrospectives.
|
|
5
|
-
#
|
|
6
|
-
# Usage: ralph/doc_gardener.sh --project-dir DIR [--json]
|
|
7
|
-
|
|
8
|
-
set -e
|
|
9
|
-
|
|
10
|
-
PROJECT_DIR=""
|
|
11
|
-
JSON_OUTPUT=false
|
|
12
|
-
GENERATE_REPORT=false
|
|
13
|
-
COMPLEXITY_THRESHOLD=3 # Minimum source files to require AGENTS.md
|
|
14
|
-
|
|
15
|
-
show_help() {
|
|
16
|
-
cat << 'HELPEOF'
|
|
17
|
-
Doc-Gardener Scanner — find stale and missing documentation
|
|
18
|
-
|
|
19
|
-
Usage:
|
|
20
|
-
ralph/doc_gardener.sh --project-dir DIR [--json]
|
|
21
|
-
|
|
22
|
-
Checks:
|
|
23
|
-
1. Modules with 3+ source files but no AGENTS.md
|
|
24
|
-
2. AGENTS.md files older than corresponding source code changes
|
|
25
|
-
3. Exec-plans in active/ for already-completed stories
|
|
26
|
-
|
|
27
|
-
Options:
|
|
28
|
-
--project-dir DIR Project root directory
|
|
29
|
-
--json Output findings as JSON (default: human-readable)
|
|
30
|
-
--report Generate quality-score.md and tech-debt-tracker.md
|
|
31
|
-
--threshold N Min source files to require AGENTS.md (default: 3)
|
|
32
|
-
-h, --help Show this help message
|
|
33
|
-
HELPEOF
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
while [[ $# -gt 0 ]]; do
|
|
37
|
-
case $1 in
|
|
38
|
-
-h|--help)
|
|
39
|
-
show_help
|
|
40
|
-
exit 0
|
|
41
|
-
;;
|
|
42
|
-
--project-dir)
|
|
43
|
-
PROJECT_DIR="$2"
|
|
44
|
-
shift 2
|
|
45
|
-
;;
|
|
46
|
-
--json)
|
|
47
|
-
JSON_OUTPUT=true
|
|
48
|
-
shift
|
|
49
|
-
;;
|
|
50
|
-
--report)
|
|
51
|
-
GENERATE_REPORT=true
|
|
52
|
-
shift
|
|
53
|
-
;;
|
|
54
|
-
--threshold)
|
|
55
|
-
COMPLEXITY_THRESHOLD="$2"
|
|
56
|
-
shift 2
|
|
57
|
-
;;
|
|
58
|
-
*)
|
|
59
|
-
echo "Unknown option: $1" >&2
|
|
60
|
-
exit 1
|
|
61
|
-
;;
|
|
62
|
-
esac
|
|
63
|
-
done
|
|
64
|
-
|
|
65
|
-
if [[ -z "$PROJECT_DIR" ]]; then
|
|
66
|
-
echo "Error: --project-dir is required" >&2
|
|
67
|
-
exit 1
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
if [[ ! -d "$PROJECT_DIR" ]]; then
|
|
71
|
-
echo "Error: project directory not found: $PROJECT_DIR" >&2
|
|
72
|
-
exit 1
|
|
73
|
-
fi
|
|
74
|
-
|
|
75
|
-
# ─── Findings collection ─────────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
FINDINGS_JSON="[]"
|
|
78
|
-
|
|
79
|
-
add_finding() {
|
|
80
|
-
local type="$1"
|
|
81
|
-
local path="$2"
|
|
82
|
-
local message="$3"
|
|
83
|
-
|
|
84
|
-
FINDINGS_JSON=$(echo "$FINDINGS_JSON" | jq \
|
|
85
|
-
--arg type "$type" \
|
|
86
|
-
--arg path "$path" \
|
|
87
|
-
--arg message "$message" \
|
|
88
|
-
'. += [{"type": $type, "path": $path, "message": $message}]')
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# ─── Check 1: Missing AGENTS.md for modules above complexity threshold ────
|
|
92
|
-
|
|
93
|
-
check_missing_agents_md() {
|
|
94
|
-
# Find directories with source files but no AGENTS.md
|
|
95
|
-
# Exclude hidden dirs, node_modules, _bmad, .ralph, docs, tests
|
|
96
|
-
while IFS= read -r dir; do
|
|
97
|
-
[[ -z "$dir" ]] && continue
|
|
98
|
-
|
|
99
|
-
# Count source files (common extensions)
|
|
100
|
-
local src_count
|
|
101
|
-
src_count=$(find "$dir" -maxdepth 1 \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.sh" -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.rb" \) 2>/dev/null | wc -l | tr -d ' ')
|
|
102
|
-
|
|
103
|
-
if [[ $src_count -ge $COMPLEXITY_THRESHOLD ]]; then
|
|
104
|
-
if [[ ! -f "$dir/AGENTS.md" ]]; then
|
|
105
|
-
local rel_path="${dir#$PROJECT_DIR/}"
|
|
106
|
-
add_finding "missing_agents_md" "$rel_path" "Module $rel_path has $src_count source files but no AGENTS.md"
|
|
107
|
-
fi
|
|
108
|
-
fi
|
|
109
|
-
done < <(find "$PROJECT_DIR" -type d \
|
|
110
|
-
-not -path "$PROJECT_DIR/.*" \
|
|
111
|
-
-not -path "*/node_modules/*" \
|
|
112
|
-
-not -path "*/_bmad/*" \
|
|
113
|
-
-not -path "*/.ralph/*" \
|
|
114
|
-
-not -path "*/docs/*" \
|
|
115
|
-
-not -path "*/tests/*" \
|
|
116
|
-
-not -path "$PROJECT_DIR" \
|
|
117
|
-
2>/dev/null)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
# ─── Check 2: Stale AGENTS.md (code changed after docs) ──────────────────
|
|
121
|
-
|
|
122
|
-
check_stale_agents_md() {
|
|
123
|
-
while IFS= read -r agents_file; do
|
|
124
|
-
[[ -z "$agents_file" ]] && continue
|
|
125
|
-
|
|
126
|
-
local dir
|
|
127
|
-
dir=$(dirname "$agents_file")
|
|
128
|
-
local rel_dir="${dir#$PROJECT_DIR/}"
|
|
129
|
-
|
|
130
|
-
# Get AGENTS.md last commit time
|
|
131
|
-
local agents_commit_time
|
|
132
|
-
agents_commit_time=$(git -C "$PROJECT_DIR" log -1 --format="%ct" -- "$agents_file" 2>/dev/null || echo "0")
|
|
133
|
-
|
|
134
|
-
# Get latest source file commit time in the same directory
|
|
135
|
-
local latest_src_time="0"
|
|
136
|
-
while IFS= read -r src_file; do
|
|
137
|
-
[[ -z "$src_file" ]] && continue
|
|
138
|
-
local src_time
|
|
139
|
-
src_time=$(git -C "$PROJECT_DIR" log -1 --format="%ct" -- "$src_file" 2>/dev/null || echo "0")
|
|
140
|
-
if [[ $src_time -gt $latest_src_time ]]; then
|
|
141
|
-
latest_src_time=$src_time
|
|
142
|
-
fi
|
|
143
|
-
done < <(find "$dir" -maxdepth 1 \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.sh" -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.rb" \) -not -name "AGENTS.md" 2>/dev/null)
|
|
144
|
-
|
|
145
|
-
if [[ $latest_src_time -gt $agents_commit_time && $agents_commit_time -gt 0 ]]; then
|
|
146
|
-
add_finding "stale_agents_md" "$rel_dir" "AGENTS.md in $rel_dir is stale — source code changed after docs"
|
|
147
|
-
fi
|
|
148
|
-
done < <(find "$PROJECT_DIR" -name "AGENTS.md" \
|
|
149
|
-
-not -path "*/node_modules/*" \
|
|
150
|
-
-not -path "*/_bmad/*" \
|
|
151
|
-
-not -path "*/.ralph/*" \
|
|
152
|
-
2>/dev/null)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
# ─── Check 3: Stale exec-plans (completed stories still in active/) ──────
|
|
156
|
-
|
|
157
|
-
check_stale_exec_plans() {
|
|
158
|
-
local progress_file="$PROJECT_DIR/ralph/progress.json"
|
|
159
|
-
local active_dir="$PROJECT_DIR/docs/exec-plans/active"
|
|
160
|
-
|
|
161
|
-
if [[ ! -f "$progress_file" || ! -d "$active_dir" ]]; then
|
|
162
|
-
return 0
|
|
163
|
-
fi
|
|
164
|
-
|
|
165
|
-
# Find active exec-plans for completed stories
|
|
166
|
-
for plan_file in "$active_dir"/*.md; do
|
|
167
|
-
[[ -f "$plan_file" ]] || continue
|
|
168
|
-
|
|
169
|
-
local story_id
|
|
170
|
-
story_id=$(basename "$plan_file" .md)
|
|
171
|
-
|
|
172
|
-
local story_status
|
|
173
|
-
story_status=$(jq -r --arg id "$story_id" '.tasks[] | select(.id == $id) | .status // ""' "$progress_file" 2>/dev/null)
|
|
174
|
-
|
|
175
|
-
if [[ "$story_status" == "complete" ]]; then
|
|
176
|
-
add_finding "stale_exec_plan" "docs/exec-plans/active/$story_id.md" \
|
|
177
|
-
"Exec-plan for story $story_id is still in active/ but story is complete — should be in completed/"
|
|
178
|
-
fi
|
|
179
|
-
done
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
# ─── Quality scoring ──────────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
# Collect module info for quality grading
|
|
185
|
-
declare -A MODULE_GRADES
|
|
186
|
-
|
|
187
|
-
grade_modules() {
|
|
188
|
-
while IFS= read -r dir; do
|
|
189
|
-
[[ -z "$dir" ]] && continue
|
|
190
|
-
|
|
191
|
-
local src_count
|
|
192
|
-
src_count=$(find "$dir" -maxdepth 1 \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.sh" -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.rb" \) 2>/dev/null | wc -l | tr -d ' ')
|
|
193
|
-
|
|
194
|
-
# Skip directories with no source files
|
|
195
|
-
[[ $src_count -eq 0 ]] && continue
|
|
196
|
-
|
|
197
|
-
local rel_path="${dir#$PROJECT_DIR/}"
|
|
198
|
-
local has_agents="false"
|
|
199
|
-
local is_stale="false"
|
|
200
|
-
|
|
201
|
-
if [[ -f "$dir/AGENTS.md" ]]; then
|
|
202
|
-
has_agents="true"
|
|
203
|
-
|
|
204
|
-
# Check staleness
|
|
205
|
-
local agents_time
|
|
206
|
-
agents_time=$(git -C "$PROJECT_DIR" log -1 --format="%ct" -- "$dir/AGENTS.md" 2>/dev/null || echo "0")
|
|
207
|
-
local latest_src="0"
|
|
208
|
-
while IFS= read -r sf; do
|
|
209
|
-
[[ -z "$sf" ]] && continue
|
|
210
|
-
local st
|
|
211
|
-
st=$(git -C "$PROJECT_DIR" log -1 --format="%ct" -- "$sf" 2>/dev/null || echo "0")
|
|
212
|
-
[[ $st -gt $latest_src ]] && latest_src=$st
|
|
213
|
-
done < <(find "$dir" -maxdepth 1 \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.sh" \) -not -name "AGENTS.md" 2>/dev/null)
|
|
214
|
-
|
|
215
|
-
if [[ $latest_src -gt $agents_time && $agents_time -gt 0 ]]; then
|
|
216
|
-
is_stale="true"
|
|
217
|
-
fi
|
|
218
|
-
fi
|
|
219
|
-
|
|
220
|
-
# Grade: A = has fresh AGENTS.md, B = has stale AGENTS.md, F = missing
|
|
221
|
-
local grade="F"
|
|
222
|
-
if [[ "$has_agents" == "true" && "$is_stale" == "false" ]]; then
|
|
223
|
-
grade="A"
|
|
224
|
-
elif [[ "$has_agents" == "true" && "$is_stale" == "true" ]]; then
|
|
225
|
-
grade="B"
|
|
226
|
-
fi
|
|
227
|
-
|
|
228
|
-
MODULE_GRADES["$rel_path"]="$grade"
|
|
229
|
-
done < <(find "$PROJECT_DIR" -type d \
|
|
230
|
-
-not -path "$PROJECT_DIR/.*" \
|
|
231
|
-
-not -path "*/node_modules/*" \
|
|
232
|
-
-not -path "*/_bmad/*" \
|
|
233
|
-
-not -path "*/.ralph/*" \
|
|
234
|
-
-not -path "*/docs/*" \
|
|
235
|
-
-not -path "*/tests/*" \
|
|
236
|
-
-not -path "$PROJECT_DIR" \
|
|
237
|
-
2>/dev/null)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
generate_quality_report() {
|
|
241
|
-
local output_file="$PROJECT_DIR/docs/quality/quality-score.md"
|
|
242
|
-
mkdir -p "$(dirname "$output_file")"
|
|
243
|
-
|
|
244
|
-
local timestamp
|
|
245
|
-
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
246
|
-
|
|
247
|
-
{
|
|
248
|
-
echo "<!-- DO NOT EDIT MANUALLY — generated by doc-gardener -->"
|
|
249
|
-
echo ""
|
|
250
|
-
echo "# Documentation Quality Score"
|
|
251
|
-
echo ""
|
|
252
|
-
echo "**Generated:** $timestamp"
|
|
253
|
-
echo ""
|
|
254
|
-
echo "## Module Grades"
|
|
255
|
-
echo ""
|
|
256
|
-
echo "| Module | Grade | Status |"
|
|
257
|
-
echo "|--------|-------|--------|"
|
|
258
|
-
|
|
259
|
-
# Sort and output grades
|
|
260
|
-
for module in $(echo "${!MODULE_GRADES[@]}" | tr ' ' '\n' | sort); do
|
|
261
|
-
local grade="${MODULE_GRADES[$module]}"
|
|
262
|
-
local status_text
|
|
263
|
-
case "$grade" in
|
|
264
|
-
A) status_text="AGENTS.md present and current" ;;
|
|
265
|
-
B) status_text="AGENTS.md present but stale" ;;
|
|
266
|
-
F) status_text="AGENTS.md missing" ;;
|
|
267
|
-
esac
|
|
268
|
-
echo "| $module | $grade | $status_text |"
|
|
269
|
-
done
|
|
270
|
-
|
|
271
|
-
echo ""
|
|
272
|
-
echo "## Grade Legend"
|
|
273
|
-
echo ""
|
|
274
|
-
echo "- **A**: Module has current AGENTS.md"
|
|
275
|
-
echo "- **B**: Module has AGENTS.md but code changed since last update"
|
|
276
|
-
echo "- **F**: Module has no AGENTS.md (3+ source files)"
|
|
277
|
-
} > "$output_file"
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
generate_tech_debt_report() {
|
|
281
|
-
local output_file="$PROJECT_DIR/docs/exec-plans/tech-debt-tracker.md"
|
|
282
|
-
mkdir -p "$(dirname "$output_file")"
|
|
283
|
-
|
|
284
|
-
local timestamp
|
|
285
|
-
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
286
|
-
|
|
287
|
-
{
|
|
288
|
-
echo "<!-- DO NOT EDIT MANUALLY — generated by doc-gardener -->"
|
|
289
|
-
echo ""
|
|
290
|
-
echo "# Documentation Tech Debt"
|
|
291
|
-
echo ""
|
|
292
|
-
echo "**Generated:** $timestamp"
|
|
293
|
-
echo ""
|
|
294
|
-
|
|
295
|
-
local debt_count
|
|
296
|
-
debt_count=$(echo "$FINDINGS_JSON" | jq '. | length')
|
|
297
|
-
|
|
298
|
-
if [[ $debt_count -eq 0 ]]; then
|
|
299
|
-
echo "No documentation debt items."
|
|
300
|
-
else
|
|
301
|
-
echo "| # | Type | Path | Issue |"
|
|
302
|
-
echo "|---|------|------|-------|"
|
|
303
|
-
|
|
304
|
-
local i=1
|
|
305
|
-
echo "$FINDINGS_JSON" | jq -r '.[] | "\(.type)\t\(.path)\t\(.message)"' | while IFS=$'\t' read -r type path message; do
|
|
306
|
-
echo "| $i | $type | $path | $message |"
|
|
307
|
-
i=$((i + 1))
|
|
308
|
-
done
|
|
309
|
-
fi
|
|
310
|
-
} > "$output_file"
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
# ─── Run all checks ──────────────────────────────────────────────────────
|
|
314
|
-
|
|
315
|
-
check_missing_agents_md
|
|
316
|
-
check_stale_agents_md
|
|
317
|
-
check_stale_exec_plans
|
|
318
|
-
|
|
319
|
-
# Generate reports if requested
|
|
320
|
-
if [[ "$GENERATE_REPORT" == "true" ]]; then
|
|
321
|
-
grade_modules
|
|
322
|
-
generate_quality_report
|
|
323
|
-
generate_tech_debt_report
|
|
324
|
-
fi
|
|
325
|
-
|
|
326
|
-
# ─── Output ───────────────────────────────────────────────────────────────
|
|
327
|
-
|
|
328
|
-
finding_count=$(echo "$FINDINGS_JSON" | jq '. | length')
|
|
329
|
-
|
|
330
|
-
if [[ "$JSON_OUTPUT" == "true" ]]; then
|
|
331
|
-
jq -n \
|
|
332
|
-
--argjson findings "$FINDINGS_JSON" \
|
|
333
|
-
--argjson count "$finding_count" \
|
|
334
|
-
--arg scanned_at "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
|
335
|
-
'{
|
|
336
|
-
scanned_at: $scanned_at,
|
|
337
|
-
finding_count: $count,
|
|
338
|
-
findings: $findings
|
|
339
|
-
}'
|
|
340
|
-
else
|
|
341
|
-
echo "Doc-Gardener Scan"
|
|
342
|
-
echo ""
|
|
343
|
-
|
|
344
|
-
if [[ $finding_count -eq 0 ]]; then
|
|
345
|
-
echo "[OK] No documentation issues found"
|
|
346
|
-
else
|
|
347
|
-
echo "$FINDINGS_JSON" | jq -r '.[] | " [WARN] \(.type): \(.message)"'
|
|
348
|
-
fi
|
|
349
|
-
|
|
350
|
-
echo ""
|
|
351
|
-
echo "$finding_count finding(s) total"
|
|
352
|
-
fi
|
package/ralph/retro.sh
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# retro.sh — Sprint Retrospective Report Generator
|
|
3
|
-
# Produces docs/quality/retro-report.md with sprint summary, verification
|
|
4
|
-
# effectiveness, and documentation health analysis.
|
|
5
|
-
# Must complete within 30 seconds (NFR20).
|
|
6
|
-
#
|
|
7
|
-
# Usage: ralph/retro.sh --project-dir DIR
|
|
8
|
-
|
|
9
|
-
set -e
|
|
10
|
-
|
|
11
|
-
PROJECT_DIR=""
|
|
12
|
-
GENERATE_COVERAGE=false
|
|
13
|
-
GENERATE_FOLLOWUP=false
|
|
14
|
-
|
|
15
|
-
show_help() {
|
|
16
|
-
cat << 'HELPEOF'
|
|
17
|
-
Sprint Retrospective — generate structured retro report
|
|
18
|
-
|
|
19
|
-
Usage:
|
|
20
|
-
ralph/retro.sh --project-dir DIR
|
|
21
|
-
|
|
22
|
-
Generates docs/quality/retro-report.md with:
|
|
23
|
-
1. Sprint summary (stories, iterations, duration)
|
|
24
|
-
2. Verification effectiveness (pass rates, iteration counts)
|
|
25
|
-
3. Documentation health (doc-gardener findings)
|
|
26
|
-
|
|
27
|
-
Options:
|
|
28
|
-
--project-dir DIR Project root directory
|
|
29
|
-
--coverage Also generate docs/quality/test-coverage.md
|
|
30
|
-
-h, --help Show this help message
|
|
31
|
-
HELPEOF
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
while [[ $# -gt 0 ]]; do
|
|
35
|
-
case $1 in
|
|
36
|
-
-h|--help)
|
|
37
|
-
show_help
|
|
38
|
-
exit 0
|
|
39
|
-
;;
|
|
40
|
-
--project-dir)
|
|
41
|
-
PROJECT_DIR="$2"
|
|
42
|
-
shift 2
|
|
43
|
-
;;
|
|
44
|
-
--coverage)
|
|
45
|
-
GENERATE_COVERAGE=true
|
|
46
|
-
shift
|
|
47
|
-
;;
|
|
48
|
-
--followup)
|
|
49
|
-
GENERATE_FOLLOWUP=true
|
|
50
|
-
shift
|
|
51
|
-
;;
|
|
52
|
-
*)
|
|
53
|
-
echo "Unknown option: $1" >&2
|
|
54
|
-
exit 1
|
|
55
|
-
;;
|
|
56
|
-
esac
|
|
57
|
-
done
|
|
58
|
-
|
|
59
|
-
if [[ -z "$PROJECT_DIR" ]]; then
|
|
60
|
-
echo "Error: --project-dir is required" >&2
|
|
61
|
-
exit 1
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
PROGRESS_FILE="$PROJECT_DIR/ralph/progress.json"
|
|
65
|
-
VLOG_FILE="$PROJECT_DIR/ralph/verification-log.json"
|
|
66
|
-
OUTPUT_FILE="$PROJECT_DIR/docs/quality/retro-report.md"
|
|
67
|
-
|
|
68
|
-
mkdir -p "$(dirname "$OUTPUT_FILE")"
|
|
69
|
-
|
|
70
|
-
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
71
|
-
|
|
72
|
-
# ─── Gather data ──────────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
# Sprint progress
|
|
75
|
-
total_stories=0
|
|
76
|
-
completed_stories=0
|
|
77
|
-
total_iterations=0
|
|
78
|
-
|
|
79
|
-
if [[ -f "$PROGRESS_FILE" ]]; then
|
|
80
|
-
total_stories=$(jq '.tasks | length' "$PROGRESS_FILE" 2>/dev/null || echo "0")
|
|
81
|
-
completed_stories=$(jq '[.tasks[] | select(.status == "complete")] | length' "$PROGRESS_FILE" 2>/dev/null || echo "0")
|
|
82
|
-
total_iterations=$(jq '[.tasks[].iterations // 0] | add // 0' "$PROGRESS_FILE" 2>/dev/null || echo "0")
|
|
83
|
-
fi
|
|
84
|
-
|
|
85
|
-
avg_iterations="N/A"
|
|
86
|
-
if [[ $completed_stories -gt 0 ]]; then
|
|
87
|
-
avg_iterations=$(echo "scale=1; $total_iterations / $completed_stories" | bc 2>/dev/null || echo "$((total_iterations / completed_stories))")
|
|
88
|
-
fi
|
|
89
|
-
|
|
90
|
-
# Verification log
|
|
91
|
-
v_total=0
|
|
92
|
-
v_pass=0
|
|
93
|
-
v_fail=0
|
|
94
|
-
pass_rate="N/A"
|
|
95
|
-
|
|
96
|
-
if [[ -f "$VLOG_FILE" ]]; then
|
|
97
|
-
v_total=$(jq '.events | length' "$VLOG_FILE" 2>/dev/null || echo "0")
|
|
98
|
-
v_pass=$(jq '[.events[] | select(.result == "pass")] | length' "$VLOG_FILE" 2>/dev/null || echo "0")
|
|
99
|
-
v_fail=$(jq '[.events[] | select(.result == "fail")] | length' "$VLOG_FILE" 2>/dev/null || echo "0")
|
|
100
|
-
if [[ $v_total -gt 0 ]]; then
|
|
101
|
-
pass_rate="$((v_pass * 100 / v_total))%"
|
|
102
|
-
fi
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
# ─── Generate report ──────────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
{
|
|
108
|
-
echo "<!-- DO NOT EDIT MANUALLY — generated by retro.sh -->"
|
|
109
|
-
echo ""
|
|
110
|
-
echo "# Sprint Retrospective Report"
|
|
111
|
-
echo ""
|
|
112
|
-
echo "**Generated:** $timestamp"
|
|
113
|
-
echo ""
|
|
114
|
-
|
|
115
|
-
# ── Sprint Summary ──
|
|
116
|
-
echo "## Sprint Summary"
|
|
117
|
-
echo ""
|
|
118
|
-
echo "| Metric | Value |"
|
|
119
|
-
echo "|--------|-------|"
|
|
120
|
-
echo "| Stories completed | $completed_stories / $total_stories |"
|
|
121
|
-
echo "| Total verification iterations | $total_iterations |"
|
|
122
|
-
echo "| Average iterations per story | $avg_iterations |"
|
|
123
|
-
echo "| Verification checks | $v_total ($v_pass pass, $v_fail fail) |"
|
|
124
|
-
echo "| Pass rate | $pass_rate |"
|
|
125
|
-
echo ""
|
|
126
|
-
|
|
127
|
-
# ── Verification Effectiveness ──
|
|
128
|
-
echo "## Verification Effectiveness"
|
|
129
|
-
echo ""
|
|
130
|
-
|
|
131
|
-
if [[ -f "$PROGRESS_FILE" ]]; then
|
|
132
|
-
echo "### Per-Story Iteration Counts"
|
|
133
|
-
echo ""
|
|
134
|
-
echo "| Story | Title | Iterations | Status |"
|
|
135
|
-
echo "|-------|-------|------------|--------|"
|
|
136
|
-
jq -r '.tasks[] | "\(.id)\t\(.title)\t\(.iterations // 0)\t\(.status)"' "$PROGRESS_FILE" 2>/dev/null | \
|
|
137
|
-
while IFS=$'\t' read -r id title iters status; do
|
|
138
|
-
echo "| $id | ${title:0:40} | $iters | $status |"
|
|
139
|
-
done
|
|
140
|
-
echo ""
|
|
141
|
-
fi
|
|
142
|
-
|
|
143
|
-
if [[ -f "$VLOG_FILE" && $v_fail -gt 0 ]]; then
|
|
144
|
-
echo "### Common Failure Patterns"
|
|
145
|
-
echo ""
|
|
146
|
-
# Count failures per gate count
|
|
147
|
-
jq -r '.events[] | select(.result == "fail") | "gates_passed=\(.gates_passed)/\(.gates_total)"' "$VLOG_FILE" 2>/dev/null | \
|
|
148
|
-
sort | uniq -c | sort -rn | head -5 | while read -r count pattern; do
|
|
149
|
-
echo "- $count occurrences: $pattern"
|
|
150
|
-
done
|
|
151
|
-
echo ""
|
|
152
|
-
fi
|
|
153
|
-
|
|
154
|
-
# ── Documentation Health ──
|
|
155
|
-
echo "## Documentation Health"
|
|
156
|
-
echo ""
|
|
157
|
-
|
|
158
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
159
|
-
if [[ -f "$SCRIPT_DIR/doc_gardener.sh" ]]; then
|
|
160
|
-
doc_findings=$("$SCRIPT_DIR/doc_gardener.sh" --project-dir "$PROJECT_DIR" --json 2>/dev/null || echo '{"finding_count":0,"findings":[]}')
|
|
161
|
-
doc_count=$(echo "$doc_findings" | jq '.finding_count // 0' 2>/dev/null || echo "0")
|
|
162
|
-
echo "Doc-gardener findings: **$doc_count**"
|
|
163
|
-
echo ""
|
|
164
|
-
|
|
165
|
-
if [[ $doc_count -gt 0 ]]; then
|
|
166
|
-
echo "$doc_findings" | jq -r '.findings[] | "- [\(.type)] \(.message)"' 2>/dev/null
|
|
167
|
-
echo ""
|
|
168
|
-
fi
|
|
169
|
-
else
|
|
170
|
-
echo "Doc-gardener not available."
|
|
171
|
-
echo ""
|
|
172
|
-
fi
|
|
173
|
-
|
|
174
|
-
# ── Recommendations ──
|
|
175
|
-
echo "## Recommendations"
|
|
176
|
-
echo ""
|
|
177
|
-
|
|
178
|
-
if [[ $v_fail -gt 0 ]]; then
|
|
179
|
-
echo "- Review verification failures — $v_fail checks failed"
|
|
180
|
-
fi
|
|
181
|
-
|
|
182
|
-
if [[ $total_iterations -gt $((completed_stories * 2)) && $completed_stories -gt 0 ]]; then
|
|
183
|
-
echo "- High iteration count ($avg_iterations avg) — stories may need better AC definition"
|
|
184
|
-
fi
|
|
185
|
-
|
|
186
|
-
if [[ $completed_stories -lt $total_stories ]]; then
|
|
187
|
-
remaining=$((total_stories - completed_stories))
|
|
188
|
-
echo "- $remaining stories incomplete — carry forward to next sprint"
|
|
189
|
-
fi
|
|
190
|
-
|
|
191
|
-
if [[ $completed_stories -eq $total_stories && $total_stories -gt 0 ]]; then
|
|
192
|
-
echo "- All stories completed — sprint success"
|
|
193
|
-
fi
|
|
194
|
-
|
|
195
|
-
} > "$OUTPUT_FILE"
|
|
196
|
-
|
|
197
|
-
# ─── Coverage report ──────────────────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
if [[ "$GENERATE_COVERAGE" == "true" ]]; then
|
|
200
|
-
COV_FILE="$PROJECT_DIR/docs/quality/test-coverage.md"
|
|
201
|
-
STATE_FILE="$PROJECT_DIR/.claude/codeharness.local.md"
|
|
202
|
-
|
|
203
|
-
# Read baseline and current from state file
|
|
204
|
-
baseline_cov="N/A"
|
|
205
|
-
current_cov="N/A"
|
|
206
|
-
if [[ -f "$STATE_FILE" ]]; then
|
|
207
|
-
baseline_cov=$(grep -o 'baseline: *[0-9]*' "$STATE_FILE" 2>/dev/null | head -1 | awk '{print $2}')
|
|
208
|
-
current_cov=$(grep -o 'current: *[0-9]*' "$STATE_FILE" 2>/dev/null | head -1 | awk '{print $2}')
|
|
209
|
-
fi
|
|
210
|
-
baseline_cov="${baseline_cov:-N/A}"
|
|
211
|
-
current_cov="${current_cov:-N/A}"
|
|
212
|
-
|
|
213
|
-
{
|
|
214
|
-
echo "<!-- DO NOT EDIT MANUALLY — generated by retro.sh -->"
|
|
215
|
-
echo ""
|
|
216
|
-
echo "# Test Coverage Report"
|
|
217
|
-
echo ""
|
|
218
|
-
echo "**Generated:** $timestamp"
|
|
219
|
-
echo ""
|
|
220
|
-
echo "## Coverage Summary"
|
|
221
|
-
echo ""
|
|
222
|
-
echo "| Metric | Value |"
|
|
223
|
-
echo "|--------|-------|"
|
|
224
|
-
echo "| Baseline (sprint start) | ${baseline_cov}% |"
|
|
225
|
-
echo "| Final (sprint end) | ${current_cov}% |"
|
|
226
|
-
|
|
227
|
-
if [[ "$baseline_cov" != "N/A" && "$current_cov" != "N/A" ]]; then
|
|
228
|
-
delta=$((current_cov - baseline_cov))
|
|
229
|
-
echo "| Delta | +${delta}% |"
|
|
230
|
-
fi
|
|
231
|
-
|
|
232
|
-
echo ""
|
|
233
|
-
echo "## Per-Story Coverage Deltas"
|
|
234
|
-
echo ""
|
|
235
|
-
echo "| Story | Title | Before | After | Delta |"
|
|
236
|
-
echo "|-------|-------|--------|-------|-------|"
|
|
237
|
-
|
|
238
|
-
if [[ -f "$PROGRESS_FILE" ]]; then
|
|
239
|
-
jq -r '.tasks[] | "\(.id)\t\(.title)\t\(.coverage_delta.before // "N/A")\t\(.coverage_delta.after // "N/A")"' "$PROGRESS_FILE" 2>/dev/null | \
|
|
240
|
-
while IFS=$'\t' read -r id title before after; do
|
|
241
|
-
d=""
|
|
242
|
-
if [[ "$before" != "N/A" && "$after" != "N/A" && "$before" != "null" && "$after" != "null" ]]; then
|
|
243
|
-
d="+$((after - before))%"
|
|
244
|
-
fi
|
|
245
|
-
echo "| $id | ${title:0:30} | ${before}% | ${after}% | $d |"
|
|
246
|
-
done
|
|
247
|
-
fi
|
|
248
|
-
} > "$COV_FILE"
|
|
249
|
-
|
|
250
|
-
echo "[OK] Coverage report generated → $COV_FILE"
|
|
251
|
-
fi
|
|
252
|
-
|
|
253
|
-
# ─── Follow-up story generation ───────────────────────────────────────────
|
|
254
|
-
|
|
255
|
-
if [[ "$GENERATE_FOLLOWUP" == "true" ]]; then
|
|
256
|
-
FOLLOWUP_FILE="$PROJECT_DIR/docs/quality/retro-followup.md"
|
|
257
|
-
followup_num=0
|
|
258
|
-
|
|
259
|
-
{
|
|
260
|
-
echo "# Sprint Retrospective Follow-up Items"
|
|
261
|
-
echo ""
|
|
262
|
-
echo "**Generated:** $timestamp"
|
|
263
|
-
echo ""
|
|
264
|
-
echo "Review and approve items below before adding to next sprint."
|
|
265
|
-
echo ""
|
|
266
|
-
echo "| # | Type | Item | Source |"
|
|
267
|
-
echo "|---|------|------|--------|"
|
|
268
|
-
|
|
269
|
-
# Carry-forward: incomplete stories
|
|
270
|
-
if [[ -f "$PROGRESS_FILE" ]]; then
|
|
271
|
-
jq -r '.tasks[] | select(.status != "complete") | "\(.id)\t\(.title)"' "$PROGRESS_FILE" 2>/dev/null | \
|
|
272
|
-
while IFS=$'\t' read -r id title; do
|
|
273
|
-
followup_num=$((followup_num + 1))
|
|
274
|
-
echo "| $followup_num | carry-forward | Story $id: $title (pending/incomplete) | Sprint backlog |"
|
|
275
|
-
done
|
|
276
|
-
fi
|
|
277
|
-
|
|
278
|
-
# High-iteration stories: need better AC
|
|
279
|
-
if [[ -f "$PROGRESS_FILE" ]]; then
|
|
280
|
-
jq -r '.tasks[] | select(.iterations > 3) | "\(.id)\t\(.title)\t\(.iterations)"' "$PROGRESS_FILE" 2>/dev/null | \
|
|
281
|
-
while IFS=$'\t' read -r id title iters; do
|
|
282
|
-
followup_num=$((followup_num + 1))
|
|
283
|
-
echo "| $followup_num | story | Refine AC for $id ($title) — took $iters iterations | Retro analysis |"
|
|
284
|
-
done
|
|
285
|
-
fi
|
|
286
|
-
|
|
287
|
-
# Verification failures: enforcement gaps
|
|
288
|
-
if [[ -f "$VLOG_FILE" && $v_fail -gt 0 ]]; then
|
|
289
|
-
followup_num=$((followup_num + 1))
|
|
290
|
-
echo "| $followup_num | enforcement | Review $v_fail verification failures — check gate coverage | Verification log |"
|
|
291
|
-
fi
|
|
292
|
-
|
|
293
|
-
} > "$FOLLOWUP_FILE"
|
|
294
|
-
|
|
295
|
-
echo "[OK] Follow-up items generated → $FOLLOWUP_FILE"
|
|
296
|
-
fi
|
|
297
|
-
|
|
298
|
-
echo "[OK] Retro report generated → $OUTPUT_FILE"
|