codeharness 0.8.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
@@ -919,6 +919,14 @@ function storyVerificationPatch() {
919
919
  - [ ] All acceptance criteria verified with real-world evidence
920
920
  - [ ] Test coverage meets target (100%)
921
921
 
922
+ ### Verification Tags
923
+
924
+ For each AC, append a verification tag to indicate how it can be verified:
925
+ - \`<!-- verification: cli-verifiable -->\` \u2014 AC can be verified by running CLI commands in a subprocess
926
+ - \`<!-- verification: integration-required -->\` \u2014 AC requires integration testing, multi-system interaction, or manual verification
927
+
928
+ ACs referencing workflows, sprint planning, user sessions, or external system interactions should be tagged as \`integration-required\`. If no tag is present, a heuristic classifier will attempt to determine verifiability at runtime.
929
+
922
930
  ## Documentation Requirements
923
931
 
924
932
  - [ ] Relevant AGENTS.md files updated (list modules touched)
@@ -1340,7 +1348,7 @@ function importStoriesToBeads(stories, opts, beadsFns) {
1340
1348
  }
1341
1349
 
1342
1350
  // src/commands/init.ts
1343
- var HARNESS_VERSION = true ? "0.8.0" : "0.0.0-dev";
1351
+ var HARNESS_VERSION = true ? "0.10.0" : "0.0.0-dev";
1344
1352
  function getStackLabel(stack) {
1345
1353
  if (stack === "nodejs") return "Node.js (package.json)";
1346
1354
  if (stack === "python") return "Python";
@@ -2400,7 +2408,7 @@ function buildSpawnArgs(opts) {
2400
2408
  return args;
2401
2409
  }
2402
2410
  function registerRunCommand(program) {
2403
- 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) => {
2404
2412
  const globalOpts = cmd.optsWithGlobals();
2405
2413
  const isJson = !!globalOpts.json;
2406
2414
  const outputOpts = { json: isJson };
@@ -2573,6 +2581,29 @@ var DB_KEYWORDS = [
2573
2581
  "sql",
2574
2582
  "table"
2575
2583
  ];
2584
+ var INTEGRATION_KEYWORDS = [
2585
+ "sprint planning",
2586
+ "workflow",
2587
+ "run /command",
2588
+ "user session",
2589
+ "multi-step",
2590
+ "external system",
2591
+ "real infrastructure",
2592
+ "integration test",
2593
+ "manual verification"
2594
+ ];
2595
+ function classifyVerifiability(description) {
2596
+ const lower = description.toLowerCase();
2597
+ for (const kw of INTEGRATION_KEYWORDS) {
2598
+ if (lower.includes(kw)) return "integration-required";
2599
+ }
2600
+ return "cli-verifiable";
2601
+ }
2602
+ var VERIFICATION_TAG_PATTERN = /<!--\s*verification:\s*(cli-verifiable|integration-required)\s*-->/;
2603
+ function parseVerificationTag(text) {
2604
+ const match = VERIFICATION_TAG_PATTERN.exec(text);
2605
+ return match ? match[1] : null;
2606
+ }
2576
2607
  function classifyAC(description) {
2577
2608
  const lower = description.toLowerCase();
2578
2609
  for (const kw of UI_KEYWORDS) {
@@ -2622,10 +2653,13 @@ function parseStoryACs(storyFilePath) {
2622
2653
  if (currentId !== null && currentDesc.length > 0) {
2623
2654
  const description = currentDesc.join(" ").trim();
2624
2655
  if (description) {
2656
+ const tag = parseVerificationTag(description);
2657
+ const verifiability = tag ?? classifyVerifiability(description);
2625
2658
  acs.push({
2626
2659
  id: currentId,
2627
2660
  description,
2628
- type: classifyAC(description)
2661
+ type: classifyAC(description),
2662
+ verifiability
2629
2663
  });
2630
2664
  } else {
2631
2665
  warn(`Skipping malformed AC #${currentId}: empty description`);
@@ -2764,6 +2798,62 @@ function getNewestSourceMtime(dir) {
2764
2798
  walk(dir);
2765
2799
  return newest;
2766
2800
  }
2801
+ function getSourceFilesInModule(modulePath) {
2802
+ const files = [];
2803
+ function walk(current) {
2804
+ let entries;
2805
+ try {
2806
+ entries = readdirSync2(current);
2807
+ } catch {
2808
+ return;
2809
+ }
2810
+ const dirName = current.split("/").pop() ?? "";
2811
+ if (dirName === "node_modules" || dirName === ".git" || dirName === "__tests__" || dirName === "dist" || dirName === "coverage" || dirName.startsWith(".") && current !== modulePath) return;
2812
+ for (const entry of entries) {
2813
+ const fullPath = join9(current, entry);
2814
+ let stat;
2815
+ try {
2816
+ stat = statSync(fullPath);
2817
+ } catch {
2818
+ continue;
2819
+ }
2820
+ if (stat.isDirectory()) {
2821
+ walk(fullPath);
2822
+ } else if (stat.isFile()) {
2823
+ const ext = getExtension(entry);
2824
+ if (SOURCE_EXTENSIONS.has(ext) && !isTestFile(entry)) {
2825
+ files.push(entry);
2826
+ }
2827
+ }
2828
+ }
2829
+ }
2830
+ walk(modulePath);
2831
+ return files;
2832
+ }
2833
+ function getMentionedFilesInAgentsMd(agentsPath) {
2834
+ if (!existsSync11(agentsPath)) return [];
2835
+ const content = readFileSync9(agentsPath, "utf-8");
2836
+ const mentioned = /* @__PURE__ */ new Set();
2837
+ const filenamePattern = /[\w./-]*[\w-]+\.(?:ts|js|py)\b/g;
2838
+ let match;
2839
+ while ((match = filenamePattern.exec(content)) !== null) {
2840
+ const fullMatch = match[0];
2841
+ const basename3 = fullMatch.split("/").pop();
2842
+ if (!isTestFile(basename3)) {
2843
+ mentioned.add(basename3);
2844
+ }
2845
+ }
2846
+ return Array.from(mentioned);
2847
+ }
2848
+ function checkAgentsMdCompleteness(agentsPath, modulePath) {
2849
+ const sourceFiles = getSourceFilesInModule(modulePath);
2850
+ const mentionedFiles = new Set(getMentionedFilesInAgentsMd(agentsPath));
2851
+ const missing = sourceFiles.filter((f) => !mentionedFiles.has(f));
2852
+ return {
2853
+ complete: missing.length === 0,
2854
+ missing
2855
+ };
2856
+ }
2767
2857
  function checkAgentsMdForModule(modulePath, dir) {
2768
2858
  const root = dir ?? process.cwd();
2769
2859
  const fullModulePath = join9(root, modulePath);
@@ -2782,13 +2872,15 @@ function checkAgentsMdForModule(modulePath, dir) {
2782
2872
  }
2783
2873
  const docMtime = statSync(agentsPath).mtime;
2784
2874
  const codeMtime = getNewestSourceMtime(fullModulePath);
2785
- if (codeMtime !== null && codeMtime.getTime() > docMtime.getTime()) {
2875
+ const { complete, missing } = checkAgentsMdCompleteness(agentsPath, fullModulePath);
2876
+ if (!complete) {
2877
+ const missingList = missing.join(", ");
2786
2878
  return {
2787
2879
  path: relative(root, agentsPath),
2788
2880
  grade: "stale",
2789
2881
  lastModified: docMtime,
2790
2882
  codeLastModified: codeMtime,
2791
- reason: `AGENTS.md stale for module: ${modulePath}`
2883
+ reason: `AGENTS.md stale for module: ${modulePath} \u2014 missing: ${missingList}`
2792
2884
  };
2793
2885
  }
2794
2886
  return {
@@ -2818,22 +2910,30 @@ function scanDocHealth(dir) {
2818
2910
  if (existsSync11(rootAgentsPath)) {
2819
2911
  if (modules.length > 0) {
2820
2912
  const docMtime = statSync(rootAgentsPath).mtime;
2821
- let newestCode = null;
2913
+ let allMissing = [];
2822
2914
  let staleModule = "";
2915
+ let newestCode = null;
2823
2916
  for (const mod of modules) {
2824
- const modMtime = getNewestSourceMtime(join9(root, mod));
2917
+ const fullModPath = join9(root, mod);
2918
+ const modAgentsPath = join9(fullModPath, "AGENTS.md");
2919
+ if (existsSync11(modAgentsPath)) continue;
2920
+ const { missing } = checkAgentsMdCompleteness(rootAgentsPath, fullModPath);
2921
+ if (missing.length > 0 && staleModule === "") {
2922
+ staleModule = mod;
2923
+ allMissing = missing;
2924
+ }
2925
+ const modMtime = getNewestSourceMtime(fullModPath);
2825
2926
  if (modMtime !== null && (newestCode === null || modMtime.getTime() > newestCode.getTime())) {
2826
2927
  newestCode = modMtime;
2827
- staleModule = mod;
2828
2928
  }
2829
2929
  }
2830
- if (newestCode !== null && newestCode.getTime() > docMtime.getTime()) {
2930
+ if (allMissing.length > 0) {
2831
2931
  documents.push({
2832
2932
  path: "AGENTS.md",
2833
2933
  grade: "stale",
2834
2934
  lastModified: docMtime,
2835
2935
  codeLastModified: newestCode,
2836
- reason: `AGENTS.md stale for module: ${staleModule}`
2936
+ reason: `AGENTS.md stale for module: ${staleModule} \u2014 missing: ${allMissing.join(", ")}`
2837
2937
  });
2838
2938
  } else {
2839
2939
  documents.push({
@@ -3189,10 +3289,44 @@ function runShowboatVerify(proofPath) {
3189
3289
  return { passed: false, output: stdout || stderr || message };
3190
3290
  }
3191
3291
  }
3192
- function proofHasContent(proofPath) {
3193
- if (!existsSync12(proofPath)) return false;
3292
+ function validateProofQuality(proofPath) {
3293
+ if (!existsSync12(proofPath)) {
3294
+ return { verified: 0, pending: 0, escalated: 0, total: 0, passed: false };
3295
+ }
3194
3296
  const content = readFileSync10(proofPath, "utf-8");
3195
- return content.includes("<!-- /showboat exec -->") || content.includes("<!-- showboat image:");
3297
+ const acHeaderPattern = /^## AC \d+:/gm;
3298
+ const matches = [...content.matchAll(acHeaderPattern)];
3299
+ if (matches.length === 0) {
3300
+ return { verified: 0, pending: 0, escalated: 0, total: 0, passed: false };
3301
+ }
3302
+ let verified = 0;
3303
+ let pending = 0;
3304
+ let escalated = 0;
3305
+ for (let i = 0; i < matches.length; i++) {
3306
+ const start = matches[i].index;
3307
+ const end = i + 1 < matches.length ? matches[i + 1].index : content.length;
3308
+ const section = content.slice(start, end);
3309
+ if (section.includes("[ESCALATE]")) {
3310
+ escalated++;
3311
+ continue;
3312
+ }
3313
+ const hasEvidence = section.includes("<!-- /showboat exec -->") || section.includes("<!-- showboat image:") || /```(?:bash|shell)\n[\s\S]*?```\n+```output\n/m.test(section);
3314
+ if (hasEvidence) {
3315
+ verified++;
3316
+ } else {
3317
+ pending++;
3318
+ }
3319
+ }
3320
+ const total = verified + pending + escalated;
3321
+ return {
3322
+ verified,
3323
+ pending,
3324
+ escalated,
3325
+ total,
3326
+ // Proof passes when no pending ACs remain and at least one is verified.
3327
+ // Escalated ACs are allowed — they are explicitly acknowledged as unverifiable.
3328
+ passed: pending === 0 && verified > 0
3329
+ };
3196
3330
  }
3197
3331
  function updateVerificationState(storyId, result, dir) {
3198
3332
  const { state, body } = readStateWithBody(dir);
@@ -3333,36 +3467,52 @@ function verifyStory(storyId, isJson, root) {
3333
3467
  return;
3334
3468
  }
3335
3469
  const storyTitle = extractStoryTitle(storyFilePath);
3336
- const proofPath = createProofDocument(storyId, storyTitle, acs, root);
3337
- let showboatStatus = "skipped";
3338
- if (proofHasContent(proofPath)) {
3339
- const showboatResult = runShowboatVerify(proofPath);
3340
- if (showboatResult.output === "showboat not available") {
3341
- showboatStatus = "skipped";
3342
- warn("Showboat not installed \u2014 skipping re-verification");
3470
+ const expectedProofPath = join11(root, "verification", `${storyId}-proof.md`);
3471
+ const proofPath = existsSync13(expectedProofPath) ? expectedProofPath : createProofDocument(storyId, storyTitle, acs, root);
3472
+ const proofQuality = validateProofQuality(proofPath);
3473
+ if (!proofQuality.passed) {
3474
+ if (isJson) {
3475
+ jsonOutput({
3476
+ status: "fail",
3477
+ message: `Proof quality check failed: ${proofQuality.verified}/${proofQuality.total} ACs verified`,
3478
+ proofQuality: { verified: proofQuality.verified, pending: proofQuality.pending, escalated: proofQuality.escalated, total: proofQuality.total }
3479
+ });
3343
3480
  } else {
3344
- showboatStatus = showboatResult.passed ? "pass" : "fail";
3345
- if (!showboatResult.passed) {
3346
- fail(`Showboat verify failed: ${showboatResult.output}`, { json: isJson });
3347
- process.exitCode = 1;
3348
- return;
3349
- }
3481
+ fail(`Proof quality check failed: ${proofQuality.verified}/${proofQuality.total} ACs verified`);
3482
+ }
3483
+ process.exitCode = 1;
3484
+ return;
3485
+ }
3486
+ if (proofQuality.escalated > 0) {
3487
+ warn(`Story ${storyId} has ${proofQuality.escalated} ACs requiring integration verification`);
3488
+ info("Run these ACs manually or in a dedicated verification session");
3489
+ }
3490
+ let showboatStatus = "skipped";
3491
+ const showboatResult = runShowboatVerify(proofPath);
3492
+ if (showboatResult.output === "showboat not available") {
3493
+ showboatStatus = "skipped";
3494
+ warn("Showboat not installed \u2014 skipping re-verification");
3495
+ } else {
3496
+ showboatStatus = showboatResult.passed ? "pass" : "fail";
3497
+ if (!showboatResult.passed) {
3498
+ fail(`Showboat verify failed: ${showboatResult.output}`, { json: isJson });
3499
+ process.exitCode = 1;
3500
+ return;
3350
3501
  }
3351
3502
  }
3352
- const acsVerified = showboatStatus === "pass";
3353
- const verifiedCount = acsVerified ? acs.length : 0;
3354
3503
  const result = {
3355
3504
  storyId,
3356
3505
  success: true,
3357
- totalACs: acs.length,
3358
- verifiedCount,
3359
- failedCount: acs.length - verifiedCount,
3506
+ totalACs: proofQuality.total,
3507
+ verifiedCount: proofQuality.verified,
3508
+ failedCount: proofQuality.pending,
3509
+ escalatedCount: proofQuality.escalated,
3360
3510
  proofPath: `verification/${storyId}-proof.md`,
3361
3511
  showboatVerifyStatus: showboatStatus,
3362
3512
  perAC: acs.map((ac) => ({
3363
3513
  id: ac.id,
3364
3514
  description: ac.description,
3365
- verified: acsVerified,
3515
+ verified: true,
3366
3516
  evidencePaths: []
3367
3517
  }))
3368
3518
  };
@@ -3394,7 +3544,10 @@ function verifyStory(storyId, isJson, root) {
3394
3544
  warn(`Failed to complete exec-plan: ${message}`);
3395
3545
  }
3396
3546
  if (isJson) {
3397
- jsonOutput(result);
3547
+ jsonOutput({
3548
+ ...result,
3549
+ proofQuality: { verified: proofQuality.verified, pending: proofQuality.pending, escalated: proofQuality.escalated, total: proofQuality.total }
3550
+ });
3398
3551
  } else {
3399
3552
  ok(`Story ${storyId}: verified \u2014 proof at verification/${storyId}-proof.md`);
3400
3553
  }
@@ -6630,7 +6783,7 @@ function registerGithubImportCommand(program) {
6630
6783
  }
6631
6784
 
6632
6785
  // src/index.ts
6633
- var VERSION = true ? "0.8.0" : "0.0.0-dev";
6786
+ var VERSION = true ? "0.10.0" : "0.0.0-dev";
6634
6787
  function createProgram() {
6635
6788
  const program = new Command();
6636
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.8.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"