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 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.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", "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) => {
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+:/gm;
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.9.0" : "0.0.0-dev";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharness",
3
- "version": "0.9.0",
3
+ "version": "0.11.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}
@@ -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} stories complete (iterations: ${loop_count}, elapsed: ${elapsed_fmt})"
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 loop_context="Loop #${iteration}."
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: 15)
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
- # Preserve retry state across restarts (Task 5.3)
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
- # For each non-done, non-flagged story, increment retry count
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
- log_status "SUCCESS" "Story ${skey}: DONE"
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"
@@ -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"