codeharness 0.9.0 → 0.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/dist/index.js +9 -5
- 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 +119 -17
- 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.11.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";
|
|
@@ -2405,10 +2405,13 @@ function buildSpawnArgs(opts) {
|
|
|
2405
2405
|
if (opts.live) {
|
|
2406
2406
|
args.push("--live");
|
|
2407
2407
|
}
|
|
2408
|
+
if (opts.reset) {
|
|
2409
|
+
args.push("--reset");
|
|
2410
|
+
}
|
|
2408
2411
|
return args;
|
|
2409
2412
|
}
|
|
2410
2413
|
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", "
|
|
2414
|
+
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").option("--reset", "Clear retry counters, flagged stories, and circuit breaker before starting", false).action(async (options, cmd) => {
|
|
2412
2415
|
const globalOpts = cmd.optsWithGlobals();
|
|
2413
2416
|
const isJson = !!globalOpts.json;
|
|
2414
2417
|
const outputOpts = { json: isJson };
|
|
@@ -2477,7 +2480,8 @@ function registerRunCommand(program) {
|
|
|
2477
2480
|
iterationTimeout,
|
|
2478
2481
|
calls,
|
|
2479
2482
|
live: options.live,
|
|
2480
|
-
maxStoryRetries
|
|
2483
|
+
maxStoryRetries,
|
|
2484
|
+
reset: options.reset
|
|
2481
2485
|
});
|
|
2482
2486
|
const env = { ...process.env };
|
|
2483
2487
|
if (isJson) {
|
|
@@ -3294,7 +3298,7 @@ function validateProofQuality(proofPath) {
|
|
|
3294
3298
|
return { verified: 0, pending: 0, escalated: 0, total: 0, passed: false };
|
|
3295
3299
|
}
|
|
3296
3300
|
const content = readFileSync10(proofPath, "utf-8");
|
|
3297
|
-
const acHeaderPattern = /^## AC \d
|
|
3301
|
+
const acHeaderPattern = /^## AC ?(\d+):/gm;
|
|
3298
3302
|
const matches = [...content.matchAll(acHeaderPattern)];
|
|
3299
3303
|
if (matches.length === 0) {
|
|
3300
3304
|
return { verified: 0, pending: 0, escalated: 0, total: 0, passed: false };
|
|
@@ -6783,7 +6787,7 @@ function registerGithubImportCommand(program) {
|
|
|
6783
6787
|
}
|
|
6784
6788
|
|
|
6785
6789
|
// src/index.ts
|
|
6786
|
-
var VERSION = true ? "0.
|
|
6790
|
+
var VERSION = true ? "0.11.0" : "0.0.0-dev";
|
|
6787
6791
|
function createProgram() {
|
|
6788
6792
|
const program = new Command();
|
|
6789
6793
|
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}
|
|
@@ -50,6 +50,9 @@ CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-json}"
|
|
|
50
50
|
CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-}"
|
|
51
51
|
CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-false}" # Fresh context per iteration by default
|
|
52
52
|
|
|
53
|
+
# Reset retry state on start
|
|
54
|
+
RESET_RETRIES=false
|
|
55
|
+
|
|
53
56
|
# Live output
|
|
54
57
|
LIVE_OUTPUT=false
|
|
55
58
|
|
|
@@ -447,6 +450,7 @@ print_progress_summary() {
|
|
|
447
450
|
counts=$(get_task_counts)
|
|
448
451
|
local total=${counts%% *}
|
|
449
452
|
local completed=${counts##* }
|
|
453
|
+
local remaining=$((total - completed))
|
|
450
454
|
local elapsed=$(( $(date +%s) - loop_start_time ))
|
|
451
455
|
local elapsed_fmt
|
|
452
456
|
|
|
@@ -458,7 +462,62 @@ print_progress_summary() {
|
|
|
458
462
|
elapsed_fmt="${elapsed}s"
|
|
459
463
|
fi
|
|
460
464
|
|
|
461
|
-
log_status "INFO" "Progress: ${completed}/${total}
|
|
465
|
+
log_status "INFO" "Progress: ${completed}/${total} done, ${remaining} remaining (iterations: ${loop_count}, elapsed: ${elapsed_fmt})"
|
|
466
|
+
|
|
467
|
+
# Show the next story in line (first non-done, non-flagged)
|
|
468
|
+
if [[ -f "$SPRINT_STATUS_FILE" ]]; then
|
|
469
|
+
local next_story=""
|
|
470
|
+
while IFS=: read -r key value; do
|
|
471
|
+
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
472
|
+
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
473
|
+
[[ -z "$key" || "$key" == \#* ]] && continue
|
|
474
|
+
if [[ "$key" =~ ^[0-9]+-[0-9]+- && "$value" != "done" ]]; then
|
|
475
|
+
if ! is_story_flagged "$key"; then
|
|
476
|
+
next_story="$key ($value)"
|
|
477
|
+
break
|
|
478
|
+
fi
|
|
479
|
+
fi
|
|
480
|
+
done < "$SPRINT_STATUS_FILE"
|
|
481
|
+
if [[ -n "$next_story" ]]; then
|
|
482
|
+
log_status "INFO" "Next up: ${next_story}"
|
|
483
|
+
fi
|
|
484
|
+
fi
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
# ─── Iteration Insights ──────────────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
print_iteration_insights() {
|
|
490
|
+
local project_root
|
|
491
|
+
project_root="$(pwd)"
|
|
492
|
+
local issues_file="$project_root/_bmad-output/implementation-artifacts/.session-issues.md"
|
|
493
|
+
local today
|
|
494
|
+
today=$(date +%Y-%m-%d)
|
|
495
|
+
local retro_file="$project_root/_bmad-output/implementation-artifacts/session-retro-${today}.md"
|
|
496
|
+
|
|
497
|
+
# Show session issues (last 20 lines — most recent subagent)
|
|
498
|
+
if [[ -f "$issues_file" ]]; then
|
|
499
|
+
local issue_count
|
|
500
|
+
issue_count=$(grep -c '^### ' "$issues_file" 2>/dev/null || echo "0")
|
|
501
|
+
if [[ $issue_count -gt 0 ]]; then
|
|
502
|
+
echo ""
|
|
503
|
+
log_status "INFO" "━━━ Session Issues ($issue_count entries) ━━━"
|
|
504
|
+
# Print the last subagent's issues block
|
|
505
|
+
awk '/^### /{block=""} {block=block $0 "\n"} END{printf "%s", block}' "$issues_file" | head -15
|
|
506
|
+
echo ""
|
|
507
|
+
fi
|
|
508
|
+
fi
|
|
509
|
+
|
|
510
|
+
# Show retro summary if generated
|
|
511
|
+
if [[ -f "$retro_file" ]]; then
|
|
512
|
+
log_status "INFO" "━━━ Session Retro ━━━"
|
|
513
|
+
# Print action items section if present, otherwise first 10 lines
|
|
514
|
+
if grep -q '## Action items\|## Action Items' "$retro_file" 2>/dev/null; then
|
|
515
|
+
sed -n '/^## Action [Ii]tems/,/^## /p' "$retro_file" | head -20
|
|
516
|
+
else
|
|
517
|
+
head -10 "$retro_file"
|
|
518
|
+
fi
|
|
519
|
+
echo ""
|
|
520
|
+
fi
|
|
462
521
|
}
|
|
463
522
|
|
|
464
523
|
# ─── Driver Management ──────────────────────────────────────────────────────
|
|
@@ -474,6 +533,13 @@ load_platform_driver() {
|
|
|
474
533
|
source "$driver_file"
|
|
475
534
|
|
|
476
535
|
driver_valid_tools
|
|
536
|
+
|
|
537
|
+
# Auto-populate CLAUDE_ALLOWED_TOOLS from driver's valid tool patterns
|
|
538
|
+
# so Ralph runs autonomously without permission prompts
|
|
539
|
+
if [[ -z "$CLAUDE_ALLOWED_TOOLS" && ${#VALID_TOOL_PATTERNS[@]} -gt 0 ]]; then
|
|
540
|
+
CLAUDE_ALLOWED_TOOLS=$(IFS=','; echo "${VALID_TOOL_PATTERNS[*]}")
|
|
541
|
+
fi
|
|
542
|
+
|
|
477
543
|
log_status "INFO" "Platform driver: $(driver_display_name) ($(driver_cli_binary))"
|
|
478
544
|
}
|
|
479
545
|
|
|
@@ -496,8 +562,10 @@ execute_iteration() {
|
|
|
496
562
|
log_status "LOOP" "Iteration $iteration — Task: ${task_id:-'(reading from prompt)'}"
|
|
497
563
|
local timeout_seconds=$((ITERATION_TIMEOUT_MINUTES * 60))
|
|
498
564
|
|
|
499
|
-
# Build loop context
|
|
500
|
-
local
|
|
565
|
+
# Build loop context — pass time budget so the session can prioritize retro
|
|
566
|
+
local start_time
|
|
567
|
+
start_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
568
|
+
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
569
|
if [[ -n "$task_id" ]]; then
|
|
502
570
|
loop_context+=" Current task: $task_id."
|
|
503
571
|
fi
|
|
@@ -509,6 +577,10 @@ execute_iteration() {
|
|
|
509
577
|
return 1
|
|
510
578
|
fi
|
|
511
579
|
|
|
580
|
+
# Write deadline file for time-warning hook
|
|
581
|
+
local deadline=$(( $(date +%s) + timeout_seconds ))
|
|
582
|
+
echo "$deadline" > "ralph/.iteration_deadline"
|
|
583
|
+
|
|
512
584
|
log_status "INFO" "Starting $(driver_display_name) (timeout: ${ITERATION_TIMEOUT_MINUTES}m)..."
|
|
513
585
|
|
|
514
586
|
# Execute with timeout
|
|
@@ -648,11 +720,12 @@ Options:
|
|
|
648
720
|
--max-iterations NUM Maximum loop iterations (default: 50)
|
|
649
721
|
--max-story-retries NUM Max retries per story before flagging (default: 3)
|
|
650
722
|
--timeout SECONDS Total loop timeout in seconds (default: 14400 = 4h)
|
|
651
|
-
--iteration-timeout MIN Per-iteration timeout in minutes (default:
|
|
723
|
+
--iteration-timeout MIN Per-iteration timeout in minutes (default: 30)
|
|
652
724
|
--calls NUM Max API calls per hour (default: 100)
|
|
653
725
|
--prompt FILE Prompt file for each iteration
|
|
654
726
|
--progress FILE Progress file (tasks JSON)
|
|
655
727
|
--live Show live output streaming
|
|
728
|
+
--reset Clear retry counters, flagged stories, and circuit breaker before starting
|
|
656
729
|
--reset-circuit Reset circuit breaker and exit
|
|
657
730
|
--status Show current status and exit
|
|
658
731
|
|
|
@@ -736,7 +809,20 @@ main() {
|
|
|
736
809
|
fi
|
|
737
810
|
fi
|
|
738
811
|
|
|
739
|
-
#
|
|
812
|
+
# Reset retry state if --reset flag was passed
|
|
813
|
+
if [[ "$RESET_RETRIES" == "true" ]]; then
|
|
814
|
+
if [[ -f "$STORY_RETRY_FILE" ]]; then
|
|
815
|
+
rm -f "$STORY_RETRY_FILE"
|
|
816
|
+
log_status "INFO" "Cleared story retry counters"
|
|
817
|
+
fi
|
|
818
|
+
if [[ -f "$FLAGGED_STORIES_FILE" ]]; then
|
|
819
|
+
rm -f "$FLAGGED_STORIES_FILE"
|
|
820
|
+
log_status "INFO" "Cleared flagged stories"
|
|
821
|
+
fi
|
|
822
|
+
reset_circuit_breaker "Reset via --reset flag"
|
|
823
|
+
log_status "INFO" "Circuit breaker reset to CLOSED"
|
|
824
|
+
fi
|
|
825
|
+
|
|
740
826
|
# .story_retries and .flagged_stories are file-based — they persist automatically
|
|
741
827
|
|
|
742
828
|
log_status "SUCCESS" "Ralph loop starting"
|
|
@@ -834,11 +920,12 @@ main() {
|
|
|
834
920
|
after_snapshot=$(snapshot_story_statuses)
|
|
835
921
|
detect_story_changes "$before_snapshot" "$after_snapshot"
|
|
836
922
|
|
|
837
|
-
#
|
|
923
|
+
# Only increment retry for the FIRST non-done, non-flagged story
|
|
924
|
+
# (the one harness-run would have picked up). Other stories were
|
|
925
|
+
# never attempted — don't penalise them for not progressing.
|
|
838
926
|
if [[ -n "$UNCHANGED_STORIES" ]]; then
|
|
839
927
|
while IFS= read -r skey; do
|
|
840
928
|
[[ -z "$skey" ]] && continue
|
|
841
|
-
# Skip already-flagged stories
|
|
842
929
|
if is_story_flagged "$skey"; then
|
|
843
930
|
continue
|
|
844
931
|
fi
|
|
@@ -850,13 +937,29 @@ main() {
|
|
|
850
937
|
else
|
|
851
938
|
log_status "WARN" "Story ${skey} — retry ${retry_count}/${MAX_STORY_RETRIES}"
|
|
852
939
|
fi
|
|
940
|
+
break # only retry the first actionable story
|
|
853
941
|
done <<< "$UNCHANGED_STORIES"
|
|
854
942
|
fi
|
|
855
943
|
|
|
856
944
|
if [[ -n "$CHANGED_STORIES" ]]; then
|
|
857
945
|
while IFS= read -r skey; do
|
|
858
946
|
[[ -z "$skey" ]] && continue
|
|
859
|
-
|
|
947
|
+
# Extract story title from story file if available
|
|
948
|
+
local story_file="$project_root/_bmad-output/implementation-artifacts/${skey}.md"
|
|
949
|
+
local story_title=""
|
|
950
|
+
if [[ -f "$story_file" ]]; then
|
|
951
|
+
story_title=$(grep -m1 '^# \|^## Story' "$story_file" 2>/dev/null | sed 's/^#* *//' | head -c 60)
|
|
952
|
+
fi
|
|
953
|
+
local proof_file="$project_root/verification/${skey}-proof.md"
|
|
954
|
+
local proof_info=""
|
|
955
|
+
if [[ -f "$proof_file" ]]; then
|
|
956
|
+
proof_info=" [proof: verification/${skey}-proof.md]"
|
|
957
|
+
fi
|
|
958
|
+
if [[ -n "$story_title" ]]; then
|
|
959
|
+
log_status "SUCCESS" "Story ${skey}: DONE — ${story_title}${proof_info}"
|
|
960
|
+
else
|
|
961
|
+
log_status "SUCCESS" "Story ${skey}: DONE${proof_info}"
|
|
962
|
+
fi
|
|
860
963
|
done <<< "$CHANGED_STORIES"
|
|
861
964
|
fi
|
|
862
965
|
|
|
@@ -892,6 +995,9 @@ main() {
|
|
|
892
995
|
# Print progress summary after every iteration
|
|
893
996
|
print_progress_summary
|
|
894
997
|
|
|
998
|
+
# ── Show session issues and retro highlights ──
|
|
999
|
+
print_iteration_insights
|
|
1000
|
+
|
|
895
1001
|
log_status "LOOP" "=== End Iteration #$loop_count ==="
|
|
896
1002
|
done
|
|
897
1003
|
|
|
@@ -919,14 +1025,6 @@ main() {
|
|
|
919
1025
|
"$(if [[ $completed -eq $total && $total -gt 0 ]]; then echo "completed"; else echo "stopped"; fi)" \
|
|
920
1026
|
"completed:$completed/$total"
|
|
921
1027
|
|
|
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
1028
|
}
|
|
931
1029
|
|
|
932
1030
|
# ─── CLI Parsing ─────────────────────────────────────────────────────────────
|
|
@@ -975,6 +1073,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
975
1073
|
LIVE_OUTPUT=true
|
|
976
1074
|
shift
|
|
977
1075
|
;;
|
|
1076
|
+
--reset)
|
|
1077
|
+
RESET_RETRIES=true
|
|
1078
|
+
shift
|
|
1079
|
+
;;
|
|
978
1080
|
--reset-circuit)
|
|
979
1081
|
# Derive state paths so circuit breaker uses the correct directory
|
|
980
1082
|
HARNESS_STATE_DIR="$(pwd)/.claude"
|
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"
|