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.
- package/dist/commands/doctor-checks.js +28 -0
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/run.js +6 -2
- package/dist/run/ralph-process.js +12 -0
- package/dist/run/run-dashboard.js +2 -1
- package/dist/transition/context-output.js +3 -5
- package/dist/transition/context.js +54 -55
- package/dist/transition/fix-plan-sync.js +3 -2
- package/dist/transition/fix-plan.js +20 -0
- package/dist/transition/section-patterns.js +1 -1
- package/dist/transition/specs-index.js +2 -2
- package/dist/utils/format-status.js +13 -0
- package/dist/watch/renderer.js +2 -1
- package/dist/watch/state-reader.js +2 -0
- package/package.json +1 -1
- package/ralph/lib/response_analyzer.sh +27 -15
- package/ralph/ralph_loop.sh +257 -21
- package/ralph/templates/PROMPT.md +15 -8
|
@@ -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
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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 },
|
package/dist/commands/run.js
CHANGED
|
@@ -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([
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
212
|
+
5. Toggle the completed story checkbox
|
|
230
213
|
6. Commit with descriptive conventional commit message
|
|
231
214
|
|
|
232
|
-
## Specs
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
##
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
##
|
|
263
|
-
-
|
|
264
|
-
-
|
|
265
|
-
-
|
|
266
|
-
-
|
|
267
|
-
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
151
|
-
{ key: "high", heading: "High Priority (
|
|
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
|
*
|
package/dist/watch/renderer.js
CHANGED
|
@@ -296,7 +296,8 @@ export function renderAnalysisPanel(analysis, cols) {
|
|
|
296
296
|
const lines = [
|
|
297
297
|
[
|
|
298
298
|
`Files: ${String(analysis.filesModified)}`,
|
|
299
|
-
`
|
|
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
|
@@ -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
|
-
#
|
|
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=$
|
|
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
|
-
(
|
|
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:
|
|
1313
|
-
#
|
|
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}
|
|
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"
|
package/ralph/ralph_loop.sh
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
2349
|
-
|
|
2350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2357
|
-
trap
|
|
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
|
|
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
|
-
|
|
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.
|
|
8
|
-
2.
|
|
9
|
-
3. Implement the
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
310
|
-
|
|
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.
|