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 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.9.0" : "0.0.0-dev";
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", "15").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) => {
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.9.0" : "0.0.0-dev";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharness",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {
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, termination, rate limiting |
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 and command building |
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 progress/status files
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
- - State files: `status.json` (loop state), `progress.json` (task tracking)
28
- - Logs written to `logs/ralph.log`
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
@@ -41,9 +41,12 @@ driver_valid_tools() {
41
41
  "Bash"
42
42
  "Bash(git *)"
43
43
  "Bash(npm *)"
44
+ "Bash(npx *)"
44
45
  "Bash(bats *)"
45
46
  "Bash(python *)"
46
47
  "Bash(node *)"
48
+ "Bash(showboat *)"
49
+ "Bash(codeharness *)"
47
50
  "NotebookEdit"
48
51
  )
49
52
  }
@@ -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:-15}
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} stories complete (iterations: ${loop_count}, elapsed: ${elapsed_fmt})"
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 loop_context="Loop #${iteration}."
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: 15)
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
- # For each non-done, non-flagged story, increment retry count
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
- log_status "SUCCESS" "Story ${skey}: DONE"
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 ─────────────────────────────────────────────────────────────
@@ -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
 
@@ -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"