bmalph 2.10.0 → 2.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.
@@ -68,6 +68,34 @@ export async function checkJq(projectDir) {
68
68
  : "Install jq: sudo apt-get install jq",
69
69
  };
70
70
  }
71
+ export async function checkGitRepo(projectDir) {
72
+ const label = "git repository with commits";
73
+ const gitDir = await runBashCommand("git rev-parse --git-dir", { cwd: projectDir });
74
+ if (gitDir.exitCode !== 0) {
75
+ return {
76
+ label,
77
+ passed: false,
78
+ detail: "not a git repository",
79
+ hint: "Run: git init && git add -A && git commit -m 'initial commit'",
80
+ };
81
+ }
82
+ const head = await runBashCommand("git rev-parse HEAD", { cwd: projectDir });
83
+ if (head.exitCode !== 0) {
84
+ return {
85
+ label,
86
+ passed: false,
87
+ detail: "no commits found",
88
+ hint: "Run: git add -A && git commit -m 'initial commit'",
89
+ };
90
+ }
91
+ const branch = await runBashCommand("git branch --show-current", { cwd: projectDir });
92
+ const branchName = branch.stdout.trim();
93
+ return {
94
+ label,
95
+ passed: true,
96
+ detail: branchName ? `branch: ${branchName}` : undefined,
97
+ };
98
+ }
71
99
  export async function checkBmadDir(projectDir) {
72
100
  return checkDir(join(projectDir, "_bmad"), "_bmad/ directory present", "Run: bmalph init");
73
101
  }
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { withErrorHandling } from "../utils/errors.js";
3
3
  import { resolveProjectPlatform } from "../platform/resolve.js";
4
- import { checkNodeVersion, checkBash, checkJq, checkConfig, checkBmadDir, checkRalphLoop, checkRalphLib, } from "./doctor-checks.js";
4
+ import { checkNodeVersion, checkBash, checkGitRepo, checkJq, checkConfig, checkBmadDir, checkRalphLoop, checkRalphLib, } from "./doctor-checks.js";
5
5
  import { checkGitignore, checkVersionMarker, checkUpstreamVersions, } from "./doctor-health-checks.js";
6
6
  import { checkCircuitBreaker, checkRalphSession, checkApiCalls, checkUpstreamGitHubStatus, } from "./doctor-runtime-checks.js";
7
7
  export async function doctorCommand(options) {
@@ -62,6 +62,7 @@ export async function runDoctor(options, checksOverride) {
62
62
  const CORE_CHECKS = [
63
63
  { id: "node-version", run: checkNodeVersion },
64
64
  { id: "bash-available", run: checkBash },
65
+ { id: "git-repo", run: checkGitRepo },
65
66
  { id: "jq-available", run: checkJq },
66
67
  { id: "config-valid", run: checkConfig },
67
68
  { id: "bmad-dir", run: checkBmadDir },
@@ -3,7 +3,7 @@ import { readConfig } from "../utils/config.js";
3
3
  import { withErrorHandling } from "../utils/errors.js";
4
4
  import { isPlatformId, getPlatform, getFullTierPlatformNames } from "../platform/registry.js";
5
5
  import { validateCursorRuntime } from "../platform/cursor-runtime-checks.js";
6
- import { validateBashAvailable, validateRalphLoop, spawnRalphLoop } from "../run/ralph-process.js";
6
+ import { validateBashAvailable, validateRalphLoop, validateGitRepo, spawnRalphLoop, } from "../run/ralph-process.js";
7
7
  import { startRunDashboard } from "../run/run-dashboard.js";
8
8
  import { parseInterval } from "../utils/validate.js";
9
9
  import { getDashboardTerminalSupport } from "../watch/frame-writer.js";
@@ -40,7 +40,11 @@ async function executeRun(options) {
40
40
  useDashboard = false;
41
41
  }
42
42
  }
43
- await Promise.all([validateBashAvailable(), validateRalphLoop(projectDir)]);
43
+ await Promise.all([
44
+ validateBashAvailable(),
45
+ validateRalphLoop(projectDir),
46
+ validateGitRepo(projectDir),
47
+ ]);
44
48
  if (platform.id === "cursor") {
45
49
  await validateCursorRuntime(projectDir);
46
50
  }
@@ -123,6 +123,18 @@ export async function validateRalphLoop(projectDir) {
123
123
  throw new Error(`${RALPH_LOOP_PATH} not found. Run: bmalph init`);
124
124
  }
125
125
  }
126
+ export async function validateGitRepo(projectDir) {
127
+ const gitDir = await runBashCommand("git rev-parse --git-dir", { cwd: projectDir });
128
+ if (gitDir.exitCode !== 0) {
129
+ throw new Error("No git repository found. Ralph requires git for progress detection.\n" +
130
+ "Run: git init && git add -A && git commit -m 'initial commit'");
131
+ }
132
+ const head = await runBashCommand("git rev-parse HEAD", { cwd: projectDir });
133
+ if (head.exitCode !== 0) {
134
+ throw new Error("Git repository has no commits. Ralph requires at least one commit for progress detection.\n" +
135
+ "Run: git add -A && git commit -m 'initial commit'");
136
+ }
137
+ }
126
138
  export function spawnRalphLoop(projectDir, platformId, options) {
127
139
  const env = { ...process.env, PLATFORM_DRIVER: platformId };
128
140
  if (options.reviewMode) {
@@ -1,3 +1,4 @@
1
+ import { formatExitReason } from "../utils/format-status.js";
1
2
  import { createRefreshCallback } from "../watch/dashboard.js";
2
3
  import { createTerminalFrameWriter } from "../watch/frame-writer.js";
3
4
  import { FileWatcher } from "../watch/file-watcher.js";
@@ -9,7 +10,7 @@ export function renderStatusBar(ralph, reviewMode) {
9
10
  case "running":
10
11
  return `Ralph: running (PID ${pid})${badge} | q: stop/detach`;
11
12
  case "stopped":
12
- return `Ralph: stopped (exit ${ralph.exitCode ?? "?"}) | q: quit`;
13
+ return `Ralph: stopped ${formatExitReason(ralph.exitCode)} | q: quit`;
13
14
  case "detached":
14
15
  return `Ralph: detached (PID ${pid})`;
15
16
  }
@@ -22,13 +22,11 @@ export async function generateContextOutputs(projectDir, inputs) {
22
22
  info("Generating PROJECT_CONTEXT.md...");
23
23
  const projectContextPath = join(projectDir, ".ralph/PROJECT_CONTEXT.md");
24
24
  const projectContextExisted = await exists(projectContextPath);
25
- let projectContext = null;
26
25
  let truncationWarnings = [];
27
26
  if (inputs.artifactContents.size > 0) {
28
27
  const { context, truncated } = extractProjectContext(inputs.artifactContents);
29
- projectContext = context;
30
28
  truncationWarnings = detectTruncation(truncated);
31
- const contextMd = generateProjectContextMd(projectContext, projectName);
29
+ const contextMd = generateProjectContextMd(context, projectName);
32
30
  await atomicWriteFile(projectContextPath, contextMd);
33
31
  generatedFiles.push({
34
32
  path: ".ralph/PROJECT_CONTEXT.md",
@@ -46,7 +44,7 @@ export async function generateContextOutputs(projectDir, inputs) {
46
44
  prompt = existingPrompt.replace(/\[YOUR PROJECT NAME\]/g, projectName);
47
45
  }
48
46
  else {
49
- prompt = generatePrompt(projectName, projectContext ?? undefined);
47
+ prompt = generatePrompt(projectName);
50
48
  }
51
49
  }
52
50
  catch (err) {
@@ -56,7 +54,7 @@ export async function generateContextOutputs(projectDir, inputs) {
56
54
  else {
57
55
  warn(`Could not read existing PROMPT.md: ${formatError(err)}`);
58
56
  }
59
- prompt = generatePrompt(projectName, projectContext ?? undefined);
57
+ prompt = generatePrompt(projectName);
60
58
  }
61
59
  await atomicWriteFile(join(projectDir, ".ralph/PROMPT.md"), prompt);
62
60
  generatedFiles.push({ path: ".ralph/PROMPT.md", action: promptExisted ? "updated" : "created" });
@@ -186,39 +186,22 @@ export function generateProjectContextMd(context, projectName) {
186
186
  }
187
187
  return lines.join("\n");
188
188
  }
189
- export function generatePrompt(projectName, context) {
190
- // Build context sections if provided
191
- const contextSections = context
192
- ? [
193
- context.projectGoals && `### Project Goals\n${context.projectGoals}`,
194
- context.successMetrics && `### Success Metrics\n${context.successMetrics}`,
195
- context.architectureConstraints &&
196
- `### Architecture Constraints\n${context.architectureConstraints}`,
197
- context.scopeBoundaries && `### Scope\n${context.scopeBoundaries}`,
198
- context.technicalRisks && `### Technical Risks\n${context.technicalRisks}`,
199
- context.targetUsers && `### Target Users\n${context.targetUsers}`,
200
- context.nonFunctionalRequirements &&
201
- `### Non-Functional Requirements\n${context.nonFunctionalRequirements}`,
202
- context.designGuidelines && `### Design Guidelines\n${context.designGuidelines}`,
203
- context.researchInsights && `### Research Insights\n${context.researchInsights}`,
204
- ]
205
- .filter(Boolean)
206
- .join("\n\n")
207
- : "";
208
- const projectContextBlock = contextSections
209
- ? `
210
-
211
- ## Project Specifications (CRITICAL - READ THIS)
212
-
213
- ${contextSections}
214
- `
215
- : "";
189
+ export function generatePrompt(projectName) {
216
190
  return `# Ralph Development Instructions
217
191
 
218
192
  ## Context
219
193
  You are an autonomous AI development agent working on the ${projectName} project.
220
194
  You follow BMAD-METHOD's developer (Amelia) persona and TDD methodology.
221
- ${projectContextBlock}
195
+
196
+ ## Current Objectives
197
+ 1. Read .ralph/@fix_plan.md and identify the next incomplete story
198
+ 2. Check existing codebase for related code — especially which existing files need changes to integrate your work
199
+ 3. Implement the story using TDD (red-green-refactor)
200
+ 4. Run tests after implementation
201
+ 5. Toggle the completed story checkbox in @fix_plan.md from \`- [ ]\` to \`- [x]\`
202
+ 6. Commit with descriptive conventional commit message
203
+ 7. Read specs ONLY if the story's inline acceptance criteria are insufficient
204
+
222
205
  ## Development Methodology (BMAD Dev Agent)
223
206
 
224
207
  For each story in @fix_plan.md:
@@ -226,30 +209,27 @@ For each story in @fix_plan.md:
226
209
  2. Write failing tests first (RED)
227
210
  3. Implement minimum code to pass tests (GREEN)
228
211
  4. Refactor while keeping tests green (REFACTOR)
229
- 5. Toggle the completed story checkbox in @fix_plan.md from \`- [ ]\` to \`- [x]\`
212
+ 5. Toggle the completed story checkbox
230
213
  6. Commit with descriptive conventional commit message
231
214
 
232
- ## Specs Reading Strategy
233
- 1. Read .ralph/SPECS_INDEX.md first for a prioritized overview of all spec files
234
- 2. Follow the reading order in SPECS_INDEX.md:
235
- - **Critical**: Always read fully (PRD, architecture, stories)
236
- - **High**: Read for implementation details (test design, readiness)
237
- - **Medium**: Reference as needed (UX specs, sprint plans)
238
- - **Low**: Optional background (brainstorming sessions)
239
- 3. For files marked [LARGE], scan headers first and read relevant sections
215
+ ## Specs Reference (Read On Demand)
216
+ - .ralph/SPECS_INDEX.md lists all spec files with paths and priorities
217
+ - .ralph/PROJECT_CONTEXT.md summarizes project goals, constraints, and scope
218
+ - Read specific specs only when the current story requires clarification
219
+ - For files marked [LARGE] in SPECS_INDEX.md, scan headers first
240
220
 
241
- ## Current Objectives
242
- 1. Read .ralph/PROJECT_CONTEXT.md for project goals, constraints, and scope
243
- 2. Read .ralph/SPECS_INDEX.md for prioritized spec file overview
244
- 3. Study .ralph/specs/ following the reading order in SPECS_INDEX.md
245
- 4. Use the exact spec paths listed in SPECS_INDEX.md instead of assuming a fixed subdirectory layout
246
- 5. Prioritize planning specs first (PRD, architecture, epics/stories, test design, UX)
247
- 6. Review implementation artifacts next (sprint plans, detailed stories) when they exist
248
- 7. Check docs/ for project knowledge and research documents (if present)
249
- 8. Review .ralph/@fix_plan.md for current priorities
250
- 9. Implement the highest priority story using TDD
251
- 10. Run tests after each implementation
252
- 11. Update the completed story checkbox in @fix_plan.md before committing
221
+ ## Key Principles
222
+ - Write code within the first few minutes of each loop
223
+ - ONE story per loop - focus completely on it
224
+ - TDD: tests first, always
225
+ - Search the codebase before assuming something isn't implemented
226
+ - Creating new files is often only half the task — wire them into the existing application
227
+ - Commit working changes with descriptive messages
228
+
229
+ ## Session Continuity
230
+ - If you have context from a previous loop, do NOT re-read spec files
231
+ - Resume implementation where you left off
232
+ - Only consult specs when you encounter ambiguity in the current story
253
233
 
254
234
  ## Progress Tracking (CRITICAL)
255
235
  - Ralph tracks progress by counting story checkboxes in @fix_plan.md
@@ -259,12 +239,12 @@ For each story in @fix_plan.md:
259
239
  - Set \`TASKS_COMPLETED_THIS_LOOP\` to the exact number of story checkboxes toggled this loop
260
240
  - Only valid values: 0 or 1
261
241
 
262
- ## Key Principles
263
- - ONE story per loop - focus completely on it
264
- - TDD: tests first, always
265
- - Search the codebase before assuming something isn't implemented
266
- - Write comprehensive tests with clear documentation
267
- - Commit working changes with descriptive messages
242
+ ## Execution Guidelines
243
+ - Before making changes: search codebase using subagents
244
+ - After implementation: run essential tests for the modified code
245
+ - If tests fail: fix them as part of your current work
246
+ - Keep .ralph/@AGENT.md updated with build/run instructions
247
+ - No placeholder implementations - build it properly
268
248
 
269
249
  ## Testing Guidelines
270
250
  - Write tests BEFORE implementation (TDD)
@@ -272,6 +252,25 @@ For each story in @fix_plan.md:
272
252
  - Run the full test suite after implementation
273
253
  - Fix any regressions immediately
274
254
 
255
+ ## Autonomous Mode (CRITICAL)
256
+ - do not ask the user questions during loop execution
257
+ - do not use AskUserQuestion, EnterPlanMode, or ExitPlanMode during loop execution
258
+ - make the safest reasonable assumption and continue
259
+ - prefer small, reversible changes when requirements are ambiguous
260
+ - surface blockers in the Ralph status block instead of starting a conversation
261
+
262
+ ## Self-Review Checklist (Before Reporting Status)
263
+
264
+ Before writing your RALPH_STATUS block, review your own work:
265
+
266
+ 1. Re-read the diff of files you modified this loop — check for obvious bugs, typos, missing error handling
267
+ 2. Verify you did not introduce regressions in existing functionality
268
+ 3. Confirm your changes match the spec in .ralph/specs/ for the story you worked on
269
+ 4. Check that new functions have proper error handling and edge case coverage
270
+ 5. Ensure you did not leave TODO/FIXME/HACK comments without justification
271
+
272
+ If you find issues, fix them before reporting status.
273
+
275
274
  ## Status Reporting (CRITICAL)
276
275
 
277
276
  At the end of your response, ALWAYS include this status block:
@@ -3,7 +3,7 @@ import { join, relative } from "node:path";
3
3
  import { debug, info, warn } from "../utils/logger.js";
4
4
  import { isEnoent, formatError } from "../utils/errors.js";
5
5
  import { atomicWriteFile, exists } from "../utils/file-system.js";
6
- import { generateFixPlan, parseFixPlan, mergeFixPlanProgress, detectOrphanedCompletedStories, detectRenumberedStories, buildCompletedTitleMap, normalizeTitle, } from "./fix-plan.js";
6
+ import { generateFixPlan, parseFixPlan, mergeFixPlanProgress, collapseCompletedStories, detectOrphanedCompletedStories, detectRenumberedStories, buildCompletedTitleMap, normalizeTitle, } from "./fix-plan.js";
7
7
  import { parseSprintStatus } from "./sprint-status.js";
8
8
  export async function syncFixPlan(projectDir, inputs) {
9
9
  let completedIds = new Set();
@@ -73,7 +73,8 @@ export async function syncFixPlan(projectDir, inputs) {
73
73
  info(`Generating fix plan for ${inputs.stories.length} stories...`);
74
74
  const newFixPlan = generateFixPlan(inputs.stories, undefined, inputs.planningSpecsSubpath);
75
75
  const mergedFixPlan = mergeFixPlanProgress(newFixPlan, completedIds, useTitleBasedMerge ? newTitleMap : undefined, useTitleBasedMerge ? completedTitles : undefined);
76
- await atomicWriteFile(fixPlanPath, mergedFixPlan);
76
+ const compactedFixPlan = collapseCompletedStories(mergedFixPlan);
77
+ await atomicWriteFile(fixPlanPath, compactedFixPlan);
77
78
  return {
78
79
  warnings: [...completionWarnings, ...orphanWarnings, ...renumberWarnings],
79
80
  fixPlanPreserved,
@@ -108,6 +108,26 @@ export function buildCompletedTitleMap(items) {
108
108
  }
109
109
  return map;
110
110
  }
111
+ const COMPLETED_STORY_LINE = /^\s*-\s*\[[xX]\]\s*Story\s+[\d.]+:/;
112
+ const INDENTED_DETAIL_LINE = /^\s+>/;
113
+ export function collapseCompletedStories(fixPlan) {
114
+ const lines = fixPlan.split("\n");
115
+ const result = [];
116
+ let skippingDetails = false;
117
+ for (const line of lines) {
118
+ if (COMPLETED_STORY_LINE.test(line)) {
119
+ skippingDetails = true;
120
+ result.push(line);
121
+ continue;
122
+ }
123
+ if (skippingDetails && INDENTED_DETAIL_LINE.test(line)) {
124
+ continue;
125
+ }
126
+ skippingDetails = false;
127
+ result.push(line);
128
+ }
129
+ return result.join("\n");
130
+ }
111
131
  export function mergeFixPlanProgress(newFixPlan, completedIds, titleMap, completedTitles) {
112
132
  // Replace [ ] with [x] for completed story IDs or title matches
113
133
  return newFixPlan.replace(createOpenFixPlanStoryLinePattern(), (match, prefix, suffix, id) => {
@@ -1,5 +1,5 @@
1
1
  function headingPattern(expression) {
2
- return new RegExp(`^##\\s+${expression}(?:\\s*:)?\\s*$`, "im");
2
+ return new RegExp(`^##\\s+(?:\\d+(?:\\.\\d+)*\\.?\\s+)?${expression}(?:\\s*:)?\\s*$`, "im");
3
3
  }
4
4
  export const PROJECT_GOALS_SECTION_PATTERNS = [
5
5
  headingPattern("Executive Summary"),
@@ -147,8 +147,8 @@ export function formatSpecsIndexMd(index) {
147
147
  "",
148
148
  ];
149
149
  const priorityConfig = [
150
- { key: "critical", heading: "Critical (Always Read First)" },
151
- { key: "high", heading: "High Priority (Read for Implementation)" },
150
+ { key: "critical", heading: "Critical (Read When Needed for Current Story)" },
151
+ { key: "high", heading: "High Priority (Reference as Needed)" },
152
152
  { key: "medium", heading: "Medium Priority (Reference as Needed)" },
153
153
  { key: "low", heading: "Low Priority (Optional)" },
154
154
  ];
@@ -1,4 +1,17 @@
1
1
  import chalk from "chalk";
2
+ const EXIT_CODE_LABELS = new Map([
3
+ [0, "completed"],
4
+ [1, "error"],
5
+ [124, "timed out"],
6
+ [130, "interrupted (SIGINT)"],
7
+ [137, "killed (OOM or SIGKILL)"],
8
+ [143, "terminated (SIGTERM)"],
9
+ ]);
10
+ export function formatExitReason(code) {
11
+ if (code === null)
12
+ return "unknown";
13
+ return EXIT_CODE_LABELS.get(code) ?? `error (exit ${code})`;
14
+ }
2
15
  /**
3
16
  * Shared status formatting with chalk colors.
4
17
  *
@@ -296,7 +296,8 @@ export function renderAnalysisPanel(analysis, cols) {
296
296
  const lines = [
297
297
  [
298
298
  `Files: ${String(analysis.filesModified)}`,
299
- `Confidence: ${String(analysis.confidenceScore)}%`,
299
+ `Parse: ${String(analysis.formatConfidence)}%`,
300
+ `Completion: ${String(analysis.confidenceScore)}%`,
300
301
  ].join(" "),
301
302
  [`Test-only: ${yesNo(analysis.isTestOnly)}`, `Stuck: ${yesNo(analysis.isStuck)}`].join(" "),
302
303
  [
@@ -95,6 +95,7 @@ export async function readAnalysisInfo(projectDir) {
95
95
  return null;
96
96
  const a = analysis;
97
97
  const filesModified = typeof a.files_modified === "number" ? a.files_modified : 0;
98
+ const formatConfidence = typeof a.format_confidence === "number" ? a.format_confidence : 0;
98
99
  const confidenceScore = typeof a.confidence_score === "number" ? a.confidence_score : 0;
99
100
  const isTestOnly = typeof a.is_test_only === "boolean" ? a.is_test_only : false;
100
101
  const isStuck = typeof a.is_stuck === "boolean" ? a.is_stuck : false;
@@ -108,6 +109,7 @@ export async function readAnalysisInfo(projectDir) {
108
109
  const permissionDenialCount = typeof a.permission_denial_count === "number" ? a.permission_denial_count : 0;
109
110
  return {
110
111
  filesModified,
112
+ formatConfidence,
111
113
  confidenceScore,
112
114
  isTestOnly,
113
115
  isStuck,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmalph",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Unified AI Development Framework - BMAD phases with Ralph execution loop",
5
5
  "type": "module",
6
6
  "bin": {
@@ -817,14 +817,6 @@ parse_json_response() {
817
817
  has_completion_signal="true"
818
818
  fi
819
819
 
820
- # Boost confidence based on structured data availability
821
- if [[ "$has_result_field" == "true" ]]; then
822
- confidence=$((confidence + 20)) # Structured response boost
823
- fi
824
- if [[ $progress_count -gt 0 ]]; then
825
- confidence=$((confidence + progress_count * 5)) # Progress indicators boost
826
- fi
827
-
828
820
  # Write normalized result using jq for safe JSON construction
829
821
  # String fields use --arg (auto-escapes), numeric/boolean use --argjson
830
822
  jq -n \
@@ -844,12 +836,14 @@ parse_json_response() {
844
836
  --argjson permission_denial_count "$permission_denial_count" \
845
837
  --argjson denied_commands "$denied_commands_json" \
846
838
  --arg tests_status "$tests_status" \
839
+ --argjson has_result_field "$has_result_field" \
847
840
  '{
848
841
  status: $status,
849
842
  exit_signal: $exit_signal,
850
843
  is_test_only: $is_test_only,
851
844
  is_stuck: $is_stuck,
852
845
  has_completion_signal: $has_completion_signal,
846
+ has_result_field: $has_result_field,
853
847
  files_modified: $files_modified,
854
848
  error_count: $error_count,
855
849
  summary: $summary,
@@ -888,6 +882,7 @@ analyze_response() {
888
882
  local has_progress=false
889
883
  local confidence_score=0
890
884
  local exit_signal=false
885
+ local format_confidence=0
891
886
  local work_summary=""
892
887
  local files_modified=0
893
888
  local tasks_completed_this_loop=0
@@ -920,6 +915,7 @@ analyze_response() {
920
915
  tasks_completed_this_loop=$(jq -r -j '.tasks_completed_this_loop // 0' "$json_parse_result_file" 2>/dev/null || echo "0")
921
916
  tests_status=$(jq -r -j '.tests_status // "UNKNOWN"' "$json_parse_result_file" 2>/dev/null || echo "UNKNOWN")
922
917
  local json_confidence=$(jq -r -j '.confidence' "$json_parse_result_file" 2>/dev/null || echo "0")
918
+ local json_has_result_field=$(jq -r -j '.has_result_field' "$json_parse_result_file" 2>/dev/null || echo "false")
923
919
  local session_id=$(jq -r -j '.session_id' "$json_parse_result_file" 2>/dev/null || echo "")
924
920
 
925
921
  # Extract permission denial fields (Issue #101)
@@ -933,11 +929,16 @@ analyze_response() {
933
929
  [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Persisted session ID: $session_id" >&2
934
930
  fi
935
931
 
936
- # JSON parsing provides high confidence
932
+ # Separate format confidence from completion confidence (Issue #124)
933
+ if [[ "$json_has_result_field" == "true" ]]; then
934
+ format_confidence=100
935
+ else
936
+ format_confidence=80
937
+ fi
937
938
  if [[ "$exit_signal" == "true" ]]; then
938
939
  confidence_score=100
939
940
  else
940
- confidence_score=$((json_confidence + 50))
941
+ confidence_score=$json_confidence
941
942
  fi
942
943
 
943
944
  if [[ ! "$tasks_completed_this_loop" =~ ^-?[0-9]+$ ]]; then
@@ -993,6 +994,7 @@ analyze_response() {
993
994
  --argjson is_stuck "$is_stuck" \
994
995
  --argjson has_progress "$has_progress" \
995
996
  --argjson files_modified "$files_modified" \
997
+ --argjson format_confidence "$format_confidence" \
996
998
  --argjson confidence_score "$confidence_score" \
997
999
  --argjson exit_signal "$exit_signal" \
998
1000
  --argjson tasks_completed_this_loop "$tasks_completed_this_loop" \
@@ -1013,6 +1015,7 @@ analyze_response() {
1013
1015
  is_stuck: $is_stuck,
1014
1016
  has_progress: $has_progress,
1015
1017
  files_modified: $files_modified,
1018
+ format_confidence: $format_confidence,
1016
1019
  confidence_score: $confidence_score,
1017
1020
  exit_signal: $exit_signal,
1018
1021
  tasks_completed_this_loop: $tasks_completed_this_loop,
@@ -1043,6 +1046,7 @@ analyze_response() {
1043
1046
  local ralph_status_json=""
1044
1047
  if ralph_status_json=$(extract_ralph_status_block_json "$output_content" 2>/dev/null); then
1045
1048
  ralph_status_block_found=true
1049
+ format_confidence=70
1046
1050
 
1047
1051
  local status
1048
1052
  status=$(printf '%s' "$ralph_status_json" | jq -r -j '.status' 2>/dev/null)
@@ -1084,6 +1088,7 @@ analyze_response() {
1084
1088
 
1085
1089
  if [[ "$ralph_status_block_found" != "true" ]]; then
1086
1090
  # No status block found — fall back to heuristic analysis
1091
+ format_confidence=30
1087
1092
 
1088
1093
  # 2. Detect completion keywords in natural language output
1089
1094
  for keyword in "${COMPLETION_KEYWORDS[@]}"; do
@@ -1197,7 +1202,11 @@ analyze_response() {
1197
1202
 
1198
1203
  if [[ $files_modified -gt 0 ]]; then
1199
1204
  has_progress=true
1200
- ((confidence_score+=20))
1205
+ # Only boost completion confidence in heuristic path (Issue #124)
1206
+ # RALPH_STATUS block is authoritative — git changes shouldn't inflate it
1207
+ if [[ "$ralph_status_block_found" != "true" ]]; then
1208
+ ((confidence_score+=20))
1209
+ fi
1201
1210
  fi
1202
1211
  fi
1203
1212
 
@@ -1232,6 +1241,7 @@ analyze_response() {
1232
1241
  --argjson is_stuck "$is_stuck" \
1233
1242
  --argjson has_progress "$has_progress" \
1234
1243
  --argjson files_modified "$files_modified" \
1244
+ --argjson format_confidence "$format_confidence" \
1235
1245
  --argjson confidence_score "$confidence_score" \
1236
1246
  --argjson exit_signal "$exit_signal" \
1237
1247
  --argjson tasks_completed_this_loop "$tasks_completed_this_loop" \
@@ -1252,6 +1262,7 @@ analyze_response() {
1252
1262
  is_stuck: $is_stuck,
1253
1263
  has_progress: $has_progress,
1254
1264
  files_modified: $files_modified,
1265
+ format_confidence: $format_confidence,
1255
1266
  confidence_score: $confidence_score,
1256
1267
  exit_signal: $exit_signal,
1257
1268
  tasks_completed_this_loop: $tasks_completed_this_loop,
@@ -1309,9 +1320,8 @@ update_exit_signals() {
1309
1320
  fi
1310
1321
 
1311
1322
  # Update completion_indicators array (only when Claude explicitly signals exit)
1312
- # Note: Previously used confidence >= 60, but JSON mode always has confidence >= 70
1313
- # due to deterministic scoring (+50 for JSON format, +20 for result field).
1314
- # This caused premature exits after 5 loops. Now we respect Claude's explicit intent.
1323
+ # Note: Format confidence (parse quality) is separated from completion confidence
1324
+ # since Issue #124. Only exit_signal drives completion indicators, not confidence score.
1315
1325
  local exit_signal=$(jq -r -j '.analysis.exit_signal // false' "$analysis_file")
1316
1326
  if [[ "$has_permission_denials" != "true" && "$has_progress_tracking_mismatch" != "true" && "$exit_signal" == "true" ]]; then
1317
1327
  signals=$(echo "$signals" | jq ".completion_indicators += [$loop_number]")
@@ -1338,6 +1348,7 @@ log_analysis_summary() {
1338
1348
 
1339
1349
  local loop=$(jq -r -j '.loop_number' "$analysis_file")
1340
1350
  local exit_sig=$(jq -r -j '.analysis.exit_signal' "$analysis_file")
1351
+ local format_conf=$(jq -r -j '.analysis.format_confidence // 0' "$analysis_file")
1341
1352
  local confidence=$(jq -r -j '.analysis.confidence_score' "$analysis_file")
1342
1353
  local test_only=$(jq -r -j '.analysis.is_test_only' "$analysis_file")
1343
1354
  local files_changed=$(jq -r -j '.analysis.files_modified' "$analysis_file")
@@ -1347,7 +1358,8 @@ log_analysis_summary() {
1347
1358
  echo -e "${BLUE}║ Response Analysis - Loop #$loop ║${NC}"
1348
1359
  echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
1349
1360
  echo -e "${YELLOW}Exit Signal:${NC} $exit_sig"
1350
- echo -e "${YELLOW}Confidence:${NC} $confidence%"
1361
+ echo -e "${YELLOW}Parse quality:${NC} $format_conf%"
1362
+ echo -e "${YELLOW}Completion:${NC} $confidence%"
1351
1363
  echo -e "${YELLOW}Test Only:${NC} $test_only"
1352
1364
  echo -e "${YELLOW}Files Changed:${NC} $files_changed"
1353
1365
  echo -e "${YELLOW}Summary:${NC} $summary"
@@ -531,6 +531,20 @@ log_status() {
531
531
  echo "[$timestamp] [$level] $message" >> "$LOG_DIR/ralph.log"
532
532
  }
533
533
 
534
+ # Human-readable label for a process exit code
535
+ describe_exit_code() {
536
+ local code=$1
537
+ case "$code" in
538
+ 0) echo "completed" ;;
539
+ 1) echo "error" ;;
540
+ 124) echo "timed out" ;;
541
+ 130) echo "interrupted (SIGINT)" ;;
542
+ 137) echo "killed (OOM or SIGKILL)" ;;
543
+ 143) echo "terminated (SIGTERM)" ;;
544
+ *) echo "error (exit $code)" ;;
545
+ esac
546
+ }
547
+
534
548
  # Update status JSON for external monitoring
535
549
  update_status() {
536
550
  local loop_count=$1
@@ -538,6 +552,7 @@ update_status() {
538
552
  local last_action=$3
539
553
  local status=$4
540
554
  local exit_reason=${5:-""}
555
+ local driver_exit_code=${6:-""}
541
556
 
542
557
  jq -n \
543
558
  --arg timestamp "$(get_iso_timestamp)" \
@@ -548,6 +563,7 @@ update_status() {
548
563
  --arg status "$status" \
549
564
  --arg exit_reason "$exit_reason" \
550
565
  --arg next_reset "$(get_next_hour_time)" \
566
+ --arg driver_exit_code "$driver_exit_code" \
551
567
  '{
552
568
  timestamp: $timestamp,
553
569
  loop_count: $loop_count,
@@ -556,7 +572,8 @@ update_status() {
556
572
  last_action: $last_action,
557
573
  status: $status,
558
574
  exit_reason: $exit_reason,
559
- next_reset: $next_reset
575
+ next_reset: $next_reset,
576
+ driver_exit_code: (if $driver_exit_code != "" then ($driver_exit_code | tonumber) else null end)
560
577
  }' > "$STATUS_FILE"
561
578
 
562
579
  # Merge quality gate status if results exist
@@ -632,6 +649,44 @@ validate_claude_permission_mode() {
632
649
  esac
633
650
  }
634
651
 
652
+ validate_git_repo() {
653
+ if ! command -v git &>/dev/null; then
654
+ log_status "ERROR" "git is not installed or not on PATH."
655
+ echo ""
656
+ echo "Ralph requires git for progress detection."
657
+ echo ""
658
+ echo "Install git:"
659
+ echo " macOS: brew install git (or: xcode-select --install)"
660
+ echo " Ubuntu: sudo apt-get install git"
661
+ echo " Windows: https://git-scm.com/downloads"
662
+ echo ""
663
+ echo "After installing, run this command again."
664
+ return 1
665
+ fi
666
+
667
+ if ! git rev-parse --git-dir &>/dev/null 2>&1; then
668
+ log_status "ERROR" "No git repository found in $(pwd)."
669
+ echo ""
670
+ echo "Ralph requires a git repository for progress detection."
671
+ echo ""
672
+ echo "To fix this, run:"
673
+ echo " git init && git add -A && git commit -m 'initial commit'"
674
+ return 1
675
+ fi
676
+
677
+ if ! git rev-parse HEAD &>/dev/null 2>&1; then
678
+ log_status "ERROR" "Git repository has no commits."
679
+ echo ""
680
+ echo "Ralph requires at least one commit for progress detection."
681
+ echo ""
682
+ echo "To fix this, run:"
683
+ echo " git add -A && git commit -m 'initial commit'"
684
+ return 1
685
+ fi
686
+
687
+ return 0
688
+ }
689
+
635
690
  warn_if_allowed_tools_ignored() {
636
691
  if driver_supports_tool_allowlist; then
637
692
  return 0
@@ -826,6 +881,51 @@ count_fix_plan_checkboxes() {
826
881
  printf '%s %s %s\n' "$completed_items" "$uncompleted_items" "$total_items"
827
882
  }
828
883
 
884
+ # Extract the first unchecked task line from @fix_plan.md.
885
+ # Returns the raw checkbox line trimmed of leading whitespace, capped at 100 chars.
886
+ # Outputs empty string if no unchecked tasks exist or file is missing.
887
+ # Args: $1 = path to @fix_plan.md (optional, defaults to $RALPH_DIR/@fix_plan.md)
888
+ extract_next_fix_plan_task() {
889
+ local fix_plan_file="${1:-$RALPH_DIR/@fix_plan.md}"
890
+ [[ -f "$fix_plan_file" ]] || return 0
891
+ local line
892
+ line=$(grep -m 1 -E "^[[:space:]]*- \[ \]" "$fix_plan_file" 2>/dev/null || true)
893
+ # Trim leading whitespace
894
+ line="${line#"${line%%[![:space:]]*}"}"
895
+ # Trim trailing whitespace
896
+ line="${line%"${line##*[![:space:]]}"}"
897
+ printf '%s' "${line:0:100}"
898
+ }
899
+
900
+ # Collapse completed story detail lines in @fix_plan.md.
901
+ # For each [x]/[X] story line, strips subsequent indented blockquote lines ( > ...).
902
+ # Incomplete stories keep their detail lines intact.
903
+ # Args: $1 = path to @fix_plan.md (modifies in place via atomic write)
904
+ collapse_completed_stories() {
905
+ local fix_plan_file="${1:-$RALPH_DIR/@fix_plan.md}"
906
+ [[ -f "$fix_plan_file" ]] || return 0
907
+
908
+ local tmp_file="${fix_plan_file}.collapse_tmp"
909
+ local skipping=false
910
+
911
+ while IFS= read -r line || [[ -n "$line" ]]; do
912
+ if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*\[[xX]\][[:space:]]*Story[[:space:]]+[0-9] ]]; then
913
+ skipping=true
914
+ printf '%s\n' "$line"
915
+ continue
916
+ fi
917
+
918
+ if $skipping && [[ "$line" =~ ^[[:space:]]+\> ]]; then
919
+ continue
920
+ fi
921
+
922
+ skipping=false
923
+ printf '%s\n' "$line"
924
+ done < "$fix_plan_file" > "$tmp_file"
925
+
926
+ mv "$tmp_file" "$fix_plan_file"
927
+ }
928
+
829
929
  enforce_fix_plan_progress_tracking() {
830
930
  local analysis_file=$1
831
931
  local completed_before=$2
@@ -1241,11 +1341,17 @@ validate_allowed_tools() {
1241
1341
  # Provides loop-specific context via --append-system-prompt
1242
1342
  build_loop_context() {
1243
1343
  local loop_count=$1
1344
+ local session_id="${2:-}"
1244
1345
  local context=""
1245
1346
 
1246
1347
  # Add loop number
1247
1348
  context="Loop #${loop_count}. "
1248
1349
 
1350
+ # Signal session continuity when resuming a valid session
1351
+ if [[ -n "$session_id" ]]; then
1352
+ context+="Session continued — do NOT re-read spec files. Resume implementation. "
1353
+ fi
1354
+
1249
1355
  # Extract incomplete tasks from @fix_plan.md
1250
1356
  # Bug #3 Fix: Support indented markdown checkboxes with [[:space:]]* pattern
1251
1357
  if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then
@@ -1254,6 +1360,13 @@ build_loop_context() {
1254
1360
  local total_tasks=0
1255
1361
  read -r completed_tasks incomplete_tasks total_tasks < <(count_fix_plan_checkboxes "$RALPH_DIR/@fix_plan.md")
1256
1362
  context+="Remaining tasks: ${incomplete_tasks}. "
1363
+
1364
+ # Inject the next unchecked task to give the AI a clear directive
1365
+ local next_task
1366
+ next_task=$(extract_next_fix_plan_task "$RALPH_DIR/@fix_plan.md")
1367
+ if [[ -n "$next_task" ]]; then
1368
+ context+="Next: ${next_task}. "
1369
+ fi
1257
1370
  fi
1258
1371
 
1259
1372
  # Add circuit breaker state
@@ -1293,10 +1406,81 @@ build_loop_context() {
1293
1406
  fi
1294
1407
  fi
1295
1408
 
1409
+ # Add git diff summary from previous loop (last segment — truncated first if over budget)
1410
+ if [[ -f "$RALPH_DIR/.loop_diff_summary" ]]; then
1411
+ local diff_summary
1412
+ diff_summary=$(head -c 150 "$RALPH_DIR/.loop_diff_summary" 2>/dev/null)
1413
+ if [[ -n "$diff_summary" ]]; then
1414
+ context+="${diff_summary}. "
1415
+ fi
1416
+ fi
1417
+
1296
1418
  # Limit total length to ~500 chars
1297
1419
  echo "${context:0:500}"
1298
1420
  }
1299
1421
 
1422
+ # Capture a compact git diff summary after each loop iteration.
1423
+ # Writes to $RALPH_DIR/.loop_diff_summary for the next loop's build_loop_context().
1424
+ # Args: $1 = loop_start_sha (git HEAD at loop start)
1425
+ capture_loop_diff_summary() {
1426
+ local loop_start_sha="${1:-}"
1427
+ local summary_file="$RALPH_DIR/.loop_diff_summary"
1428
+
1429
+ # Clear previous summary
1430
+ rm -f "$summary_file"
1431
+
1432
+ # Require git and a valid repo
1433
+ if ! command -v git &>/dev/null || ! git rev-parse --git-dir &>/dev/null 2>&1; then
1434
+ return 0
1435
+ fi
1436
+
1437
+ local current_sha
1438
+ current_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
1439
+ local numstat_output=""
1440
+
1441
+ if [[ -n "$loop_start_sha" && -n "$current_sha" && "$loop_start_sha" != "$current_sha" ]]; then
1442
+ # Commits exist: union of committed + working tree changes, deduplicated by filename
1443
+ numstat_output=$(
1444
+ {
1445
+ git diff --numstat "$loop_start_sha" HEAD 2>/dev/null
1446
+ git diff --numstat HEAD 2>/dev/null
1447
+ git diff --numstat --cached 2>/dev/null
1448
+ } | awk -F'\t' '!seen[$3]++'
1449
+ )
1450
+ else
1451
+ # No commits: staged + unstaged only
1452
+ numstat_output=$(
1453
+ {
1454
+ git diff --numstat 2>/dev/null
1455
+ git diff --numstat --cached 2>/dev/null
1456
+ } | awk -F'\t' '!seen[$3]++'
1457
+ )
1458
+ fi
1459
+
1460
+ [[ -z "$numstat_output" ]] && return 0
1461
+
1462
+ # Format: Changed: file (+add/-del), file2 (+add/-del)
1463
+ # Skip binary files (numstat shows - - for binary)
1464
+ # Use tab separator — numstat output is tab-delimited (handles filenames with spaces)
1465
+ local formatted
1466
+ formatted=$(echo "$numstat_output" | awk -F'\t' '
1467
+ $1 != "-" {
1468
+ if (n++) printf ", "
1469
+ printf "%s (+%s/-%s)", $3, $1, $2
1470
+ }
1471
+ ')
1472
+
1473
+ [[ -z "$formatted" ]] && return 0
1474
+
1475
+ local result="Changed: ${formatted}"
1476
+ # Self-truncate to ~150 chars (144 content + "...")
1477
+ if [[ ${#result} -gt 147 ]]; then
1478
+ result="${result:0:144}..."
1479
+ fi
1480
+
1481
+ echo "$result" > "$summary_file"
1482
+ }
1483
+
1300
1484
  # Check if a code review should run this iteration
1301
1485
  # Returns 0 (true) when review is due, 1 (false) otherwise
1302
1486
  # Args: $1 = loop_count, $2 = fix_plan_completed_delta (optional, for ultimate mode)
@@ -1957,12 +2141,16 @@ get_live_stream_filter() {
1957
2141
  execute_claude_code() {
1958
2142
  local timestamp=$(date '+%Y-%m-%d_%H-%M-%S')
1959
2143
  local output_file="$LOG_DIR/claude_output_${timestamp}.log"
2144
+ local stderr_file="$LOG_DIR/claude_stderr_${timestamp}.log"
1960
2145
  local loop_count=$1
1961
2146
  local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
1962
2147
  calls_made=$((calls_made + 1))
1963
2148
  local fix_plan_completed_before=0
1964
2149
  read -r fix_plan_completed_before _ _ < <(count_fix_plan_checkboxes "$RALPH_DIR/@fix_plan.md")
1965
2150
 
2151
+ # Clear previous diff summary to prevent stale context on early exit (#117)
2152
+ rm -f "$RALPH_DIR/.loop_diff_summary"
2153
+
1966
2154
  # Fix #141: Capture git HEAD SHA at loop start to detect commits as progress
1967
2155
  # Store in file for access by progress detection after Claude execution
1968
2156
  local loop_start_sha=""
@@ -1975,21 +2163,22 @@ execute_claude_code() {
1975
2163
  local timeout_seconds=$((CLAUDE_TIMEOUT_MINUTES * 60))
1976
2164
  log_status "INFO" "⏳ Starting $DRIVER_DISPLAY_NAME execution... (timeout: ${CLAUDE_TIMEOUT_MINUTES}m)"
1977
2165
 
2166
+ # Initialize or resume session (must happen before build_loop_context
2167
+ # so the session_id can gate the "session continued" signal)
2168
+ local session_id=""
2169
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" ]] && supports_driver_sessions; then
2170
+ session_id=$(init_claude_session)
2171
+ fi
2172
+
1978
2173
  # Build loop context for session continuity
1979
2174
  local loop_context=""
1980
2175
  if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
1981
- loop_context=$(build_loop_context "$loop_count")
2176
+ loop_context=$(build_loop_context "$loop_count" "$session_id")
1982
2177
  if [[ -n "$loop_context" && "$VERBOSE_PROGRESS" == "true" ]]; then
1983
2178
  log_status "INFO" "Loop context: $loop_context"
1984
2179
  fi
1985
2180
  fi
1986
2181
 
1987
- # Initialize or resume session
1988
- local session_id=""
1989
- if [[ "$CLAUDE_USE_CONTINUE" == "true" ]] && supports_driver_sessions; then
1990
- session_id=$(init_claude_session)
1991
- fi
1992
-
1993
2182
  # Live mode requires JSON output (stream-json) — override text format
1994
2183
  if [[ "$LIVE_OUTPUT" == "true" && "$CLAUDE_OUTPUT_FORMAT" == "text" ]]; then
1995
2184
  log_status "WARN" "Live mode requires JSON output format. Overriding text → json for this session."
@@ -2078,7 +2267,7 @@ execute_claude_code() {
2078
2267
  # read from stdin even in -p (print) mode, causing the process to hang
2079
2268
  set -o pipefail
2080
2269
  portable_timeout ${timeout_seconds}s stdbuf -oL "${LIVE_CMD_ARGS[@]}" \
2081
- < /dev/null 2>&1 | stdbuf -oL tee "$output_file" | stdbuf -oL jq --unbuffered -j "$jq_filter" 2>/dev/null | tee "$LIVE_LOG_FILE"
2270
+ < /dev/null 2>"$stderr_file" | stdbuf -oL tee "$output_file" | stdbuf -oL jq --unbuffered -j "$jq_filter" 2>/dev/null | tee "$LIVE_LOG_FILE"
2082
2271
 
2083
2272
  # Capture exit codes from pipeline
2084
2273
  local -a pipe_status=("${PIPESTATUS[@]}")
@@ -2130,7 +2319,7 @@ execute_claude_code() {
2130
2319
  # stdin must be redirected from /dev/null because newer Claude CLI versions
2131
2320
  # read from stdin even in -p (print) mode, causing SIGTTIN suspension
2132
2321
  # when the process is backgrounded
2133
- if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" < /dev/null > "$output_file" 2>&1 &
2322
+ if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" < /dev/null > "$output_file" 2>"$stderr_file" &
2134
2323
  then
2135
2324
  : # Continue to wait loop
2136
2325
  else
@@ -2145,7 +2334,7 @@ execute_claude_code() {
2145
2334
  # Note: Legacy mode doesn't use --allowedTools, so tool permissions
2146
2335
  # will be handled by Claude Code's default permission system
2147
2336
  if [[ "$use_modern_cli" == "false" ]]; then
2148
- if portable_timeout ${timeout_seconds}s $CLAUDE_CODE_CMD < "$PROMPT_FILE" > "$output_file" 2>&1 &
2337
+ if portable_timeout ${timeout_seconds}s $CLAUDE_CODE_CMD < "$PROMPT_FILE" > "$output_file" 2>"$stderr_file" &
2149
2338
  then
2150
2339
  : # Continue to wait loop
2151
2340
  else
@@ -2204,6 +2393,9 @@ EOF
2204
2393
  exit_code=$?
2205
2394
  fi
2206
2395
 
2396
+ # Expose the raw driver exit code to the main loop for status reporting
2397
+ LAST_DRIVER_EXIT_CODE=$exit_code
2398
+
2207
2399
  if [ $exit_code -eq 0 ]; then
2208
2400
  # Only increment counter on successful execution
2209
2401
  echo "$calls_made" > "$CALL_COUNT_FILE"
@@ -2227,6 +2419,11 @@ EOF
2227
2419
  read -r fix_plan_completed_after _ _ < <(count_fix_plan_checkboxes "$RALPH_DIR/@fix_plan.md")
2228
2420
  enforce_fix_plan_progress_tracking "$RESPONSE_ANALYSIS_FILE" "$fix_plan_completed_before" "$fix_plan_completed_after"
2229
2421
 
2422
+ # Collapse completed story details so the agent doesn't re-read them
2423
+ if [[ $fix_plan_completed_after -gt $fix_plan_completed_before ]]; then
2424
+ collapse_completed_stories "$RALPH_DIR/@fix_plan.md"
2425
+ fi
2426
+
2230
2427
  # Run quality gates
2231
2428
  local exit_signal_for_gates
2232
2429
  exit_signal_for_gates=$(jq -r '.analysis.exit_signal // false' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "false")
@@ -2289,6 +2486,9 @@ EOF
2289
2486
  fi
2290
2487
  fi
2291
2488
 
2489
+ # Capture diff summary for next loop's context (#117)
2490
+ capture_loop_diff_summary "$loop_start_sha"
2491
+
2292
2492
  local has_errors="false"
2293
2493
 
2294
2494
  # Two-stage error detection to avoid JSON field false positives
@@ -2339,24 +2539,50 @@ EOF
2339
2539
  log_status "ERROR" "🚫 Claude API 5-hour usage limit reached"
2340
2540
  return 2 # Special return code for API limit
2341
2541
  else
2342
- log_status "ERROR" "❌ $DRIVER_DISPLAY_NAME execution failed, check: $output_file"
2542
+ local exit_desc
2543
+ exit_desc=$(describe_exit_code "$exit_code")
2544
+ log_status "ERROR" "❌ $DRIVER_DISPLAY_NAME exited: $exit_desc (code $exit_code)"
2545
+ if [[ -f "$stderr_file" && -s "$stderr_file" ]]; then
2546
+ log_status "ERROR" " stderr (last 3 lines): $(tail -3 "$stderr_file")"
2547
+ log_status "ERROR" " full stderr log: $stderr_file"
2548
+ fi
2343
2549
  return 1
2344
2550
  fi
2345
2551
  fi
2346
2552
  }
2347
2553
 
2348
- # Cleanup function
2349
- cleanup() {
2350
- log_status "INFO" "Ralph loop interrupted. Cleaning up..."
2554
+ # Guard against double cleanup (EXIT fires after signal handler exits)
2555
+ _CLEANUP_DONE=false
2556
+
2557
+ # EXIT trap — catches set -e failures and other unexpected exits
2558
+ _on_exit() {
2559
+ local code=$?
2560
+ [[ "$_CLEANUP_DONE" == "true" ]] && return
2561
+ _CLEANUP_DONE=true
2562
+ if [[ "$code" -ne 0 ]]; then
2563
+ local desc
2564
+ desc=$(describe_exit_code "$code")
2565
+ log_status "ERROR" "Ralph loop exiting unexpectedly: $desc (code $code)"
2566
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "unexpected_exit" "stopped" "$desc" "$code"
2567
+ fi
2568
+ }
2569
+
2570
+ # Signal handler — preserves signal identity in exit code
2571
+ _on_signal() {
2572
+ local sig=$1
2573
+ log_status "INFO" "Ralph loop interrupted by $sig. Cleaning up..."
2351
2574
  reset_session "manual_interrupt"
2352
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
2353
- exit 0
2575
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped" "$sig"
2576
+ _CLEANUP_DONE=true
2577
+ [[ "$sig" == "SIGINT" ]] && exit 130
2578
+ exit 143
2354
2579
  }
2355
2580
 
2356
- # Set up signal handlers
2357
- trap cleanup SIGINT SIGTERM
2581
+ trap _on_exit EXIT
2582
+ trap '_on_signal SIGINT' SIGINT
2583
+ trap '_on_signal SIGTERM' SIGTERM
2358
2584
 
2359
- # Global variable for loop count (needed by cleanup function)
2585
+ # Global variable for loop count (needed by trap handlers)
2360
2586
  loop_count=0
2361
2587
 
2362
2588
  # Main loop
@@ -2458,6 +2684,11 @@ main() {
2458
2684
  exit 1
2459
2685
  fi
2460
2686
 
2687
+ # Check for git repository (required for progress detection)
2688
+ if ! validate_git_repo; then
2689
+ exit 1
2690
+ fi
2691
+
2461
2692
  # Initialize session tracking before entering the loop
2462
2693
  init_session_tracking
2463
2694
 
@@ -2602,7 +2833,12 @@ main() {
2602
2833
  printf "\n"
2603
2834
  fi
2604
2835
  else
2605
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "failed" "error"
2836
+ # Infrastructure failures (timeout, crash, OOM) intentionally bypass
2837
+ # record_loop_result to avoid counting as agent stagnation. The circuit
2838
+ # breaker only tracks progress during successful executions. (Issue #145)
2839
+ local exit_desc
2840
+ exit_desc=$(describe_exit_code "${LAST_DRIVER_EXIT_CODE:-1}")
2841
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "failed" "error" "$exit_desc" "${LAST_DRIVER_EXIT_CODE:-}"
2606
2842
  log_status "WARN" "Execution failed, waiting 30 seconds before retry..."
2607
2843
  sleep 30
2608
2844
  fi
@@ -4,21 +4,28 @@
4
4
  You are Ralph, an autonomous AI development agent working on a [YOUR PROJECT NAME] project.
5
5
 
6
6
  ## Current Objectives
7
- 1. Study .ralph/specs/* to learn about the project specifications
8
- 2. Review .ralph/@fix_plan.md for current priorities
9
- 3. Implement the highest priority item using best practices
7
+ 1. Review .ralph/@fix_plan.md for current priorities
8
+ 2. Search the codebase for related code — especially which existing files need changes to integrate your work
9
+ 3. Implement the task from the loop context (or the first unchecked item in @fix_plan.md on the first loop)
10
10
  4. Use parallel subagents for complex tasks (max 100 concurrent)
11
11
  5. Run tests after each implementation
12
- 6. Update documentation and the completed story checkbox in @fix_plan.md
12
+ 6. Update the completed story checkbox in @fix_plan.md and commit
13
+ 7. Read .ralph/specs/* ONLY if the task requires specific context you don't already have
13
14
 
14
15
  ## Key Principles
15
- - ONE task per loop - focus on the most important thing
16
+ - Write code within the first few minutes of each loop
17
+ - ONE task per loop — implement the task specified in the loop context
16
18
  - Search the codebase before assuming something isn't implemented
19
+ - Creating new files is often only half the task — wire them into the existing application
17
20
  - Use subagents for expensive operations (file searching, analysis)
18
- - Write comprehensive tests with clear documentation
19
21
  - Toggle completed story checkboxes in .ralph/@fix_plan.md without rewriting story lines
20
22
  - Commit working changes with descriptive messages
21
23
 
24
+ ## Session Continuity
25
+ - If you have context from a previous loop, do NOT re-read spec files
26
+ - Resume implementation where you left off
27
+ - Only consult specs when you encounter ambiguity in the current task
28
+
22
29
  ## Progress Tracking (CRITICAL)
23
30
  - Ralph tracks progress by counting story checkboxes in .ralph/@fix_plan.md
24
31
  - When you complete a story, change `- [ ]` to `- [x]` on that exact story line
@@ -306,7 +313,7 @@ RECOMMENDATION: Blocked on [specific dependency] - need [what's needed]
306
313
  - examples/: Example usage and test cases
307
314
 
308
315
  ## Current Task
309
- Follow .ralph/@fix_plan.md and choose the most important item to implement next.
310
- Use your judgment to prioritize what will have the biggest impact on project progress.
316
+ Implement the task specified in the loop context.
317
+ If no task is specified (first loop), pick the first unchecked item from .ralph/@fix_plan.md.
311
318
 
312
319
  Remember: Quality over speed. Build it right the first time. Know when you're done.