bmalph 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -28
- package/dist/cli.js +4 -2
- package/dist/commands/doctor-checks.js +1 -1
- package/dist/commands/doctor-health-checks.js +2 -1
- package/dist/commands/init.js +3 -1
- package/dist/commands/reset.js +1 -1
- package/dist/commands/run.js +49 -1
- package/dist/commands/status.js +0 -9
- package/dist/commands/upgrade.js +1 -1
- package/dist/installer/metadata.js +1 -1
- package/dist/installer/project-files.js +2 -3
- package/dist/installer/ralph-assets.js +8 -0
- package/dist/installer/template-files.js +54 -0
- package/dist/reset.js +2 -3
- package/dist/run/ralph-process.js +13 -3
- package/dist/run/run-dashboard.js +6 -4
- package/dist/transition/artifact-scan.js +3 -6
- package/dist/transition/context.js +1 -1
- package/dist/utils/constants.js +22 -0
- package/dist/utils/github.js +4 -3
- package/dist/utils/ralph-runtime-state.js +3 -13
- package/dist/utils/validate.js +4 -10
- package/dist/watch/dashboard.js +1 -0
- package/dist/watch/renderer.js +23 -0
- package/dist/watch/state-reader.js +20 -1
- package/package.json +8 -2
- package/ralph/drivers/DRIVER_INTERFACE.md +422 -0
- package/ralph/drivers/codex.sh +2 -2
- package/ralph/lib/response_analyzer.sh +87 -87
- package/ralph/ralph_loop.sh +220 -1
- package/ralph/templates/PROMPT.md +12 -0
- package/ralph/templates/REVIEW_PROMPT.md +60 -0
- package/ralph/templates/ralphrc.template +18 -0
- package/dist/cli.js.map +0 -1
- package/dist/commands/check-updates.js.map +0 -1
- package/dist/commands/doctor-checks.js.map +0 -1
- package/dist/commands/doctor-health-checks.js.map +0 -1
- package/dist/commands/doctor-runtime-checks.js.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/implement.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/reset.js.map +0 -1
- package/dist/commands/run.js.map +0 -1
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/upgrade.js.map +0 -1
- package/dist/commands/watch.js.map +0 -1
- package/dist/installer/bmad-assets.js.map +0 -1
- package/dist/installer/commands.js.map +0 -1
- package/dist/installer/install.js.map +0 -1
- package/dist/installer/metadata.js.map +0 -1
- package/dist/installer/project-files.js.map +0 -1
- package/dist/installer/ralph-assets.js.map +0 -1
- package/dist/installer/template-files.js.map +0 -1
- package/dist/installer/types.js.map +0 -1
- package/dist/installer.js.map +0 -1
- package/dist/platform/aider.js.map +0 -1
- package/dist/platform/claude-code.js.map +0 -1
- package/dist/platform/codex.js.map +0 -1
- package/dist/platform/copilot.js.map +0 -1
- package/dist/platform/cursor-runtime-checks.js.map +0 -1
- package/dist/platform/cursor.js.map +0 -1
- package/dist/platform/detect.js.map +0 -1
- package/dist/platform/doctor-checks.js.map +0 -1
- package/dist/platform/guidance.js.map +0 -1
- package/dist/platform/instructions-snippet.js.map +0 -1
- package/dist/platform/opencode.js.map +0 -1
- package/dist/platform/registry.js.map +0 -1
- package/dist/platform/resolve.js.map +0 -1
- package/dist/platform/types.js.map +0 -1
- package/dist/platform/windsurf.js.map +0 -1
- package/dist/reset.js.map +0 -1
- package/dist/run/ralph-process.js.map +0 -1
- package/dist/run/run-dashboard.js.map +0 -1
- package/dist/run/types.js.map +0 -1
- package/dist/transition/artifact-collection.js.map +0 -1
- package/dist/transition/artifact-loading.js.map +0 -1
- package/dist/transition/artifact-scan.js.map +0 -1
- package/dist/transition/artifacts.js.map +0 -1
- package/dist/transition/context-output.js.map +0 -1
- package/dist/transition/context.js.map +0 -1
- package/dist/transition/fix-plan-sync.js.map +0 -1
- package/dist/transition/fix-plan.js.map +0 -1
- package/dist/transition/index.js.map +0 -1
- package/dist/transition/orchestration.js.map +0 -1
- package/dist/transition/preflight.js.map +0 -1
- package/dist/transition/section-patterns.js.map +0 -1
- package/dist/transition/specs-changelog.js.map +0 -1
- package/dist/transition/specs-index.js.map +0 -1
- package/dist/transition/specs-sync.js.map +0 -1
- package/dist/transition/sprint-status.js.map +0 -1
- package/dist/transition/story-id.js.map +0 -1
- package/dist/transition/story-parsing.js.map +0 -1
- package/dist/transition/tech-stack.js.map +0 -1
- package/dist/transition/types.js.map +0 -1
- package/dist/utils/artifact-definitions.js.map +0 -1
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/constants.js.map +0 -1
- package/dist/utils/dryrun.js.map +0 -1
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/file-system.js.map +0 -1
- package/dist/utils/format-status.js.map +0 -1
- package/dist/utils/github.js.map +0 -1
- package/dist/utils/json.js.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/ralph-runtime-state.js.map +0 -1
- package/dist/utils/state.js.map +0 -1
- package/dist/utils/validate.js.map +0 -1
- package/dist/watch/dashboard.js.map +0 -1
- package/dist/watch/file-watcher.js.map +0 -1
- package/dist/watch/frame-writer.js.map +0 -1
- package/dist/watch/renderer.js.map +0 -1
- package/dist/watch/state-reader.js.map +0 -1
- package/dist/watch/types.js.map +0 -1
package/dist/utils/constants.js
CHANGED
|
@@ -53,6 +53,28 @@ export const CONFIG_FILE = "bmalph/config.json";
|
|
|
53
53
|
/** Ralph status file path */
|
|
54
54
|
export const RALPH_STATUS_FILE = ".ralph/status.json";
|
|
55
55
|
// =============================================================================
|
|
56
|
+
// Ralph status mapping
|
|
57
|
+
// =============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Maps raw Ralph bash status strings to normalized status values.
|
|
60
|
+
* Single source of truth — used by both validate.ts and ralph-runtime-state.ts.
|
|
61
|
+
*/
|
|
62
|
+
export const RALPH_STATUS_MAP = {
|
|
63
|
+
running: "running",
|
|
64
|
+
halted: "blocked",
|
|
65
|
+
stopped: "blocked",
|
|
66
|
+
completed: "completed",
|
|
67
|
+
success: "completed",
|
|
68
|
+
graceful_exit: "completed",
|
|
69
|
+
paused: "blocked",
|
|
70
|
+
error: "blocked",
|
|
71
|
+
};
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// Gitignore entries managed by bmalph
|
|
74
|
+
// =============================================================================
|
|
75
|
+
/** Entries bmalph adds to .gitignore during init and checks during doctor */
|
|
76
|
+
export const GITIGNORE_ENTRIES = [".ralph/logs/", "_bmad-output/"];
|
|
77
|
+
// =============================================================================
|
|
56
78
|
// Dashboard constants
|
|
57
79
|
// =============================================================================
|
|
58
80
|
/** Default dashboard refresh interval in milliseconds */
|
package/dist/utils/github.js
CHANGED
|
@@ -132,9 +132,10 @@ export class GitHubClient {
|
|
|
132
132
|
typeof data.sha !== "string" ||
|
|
133
133
|
!("commit" in data) ||
|
|
134
134
|
!data.commit ||
|
|
135
|
-
typeof data.commit
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
typeof data.commit.message !==
|
|
136
|
+
"string" ||
|
|
137
|
+
!data.commit.author ||
|
|
138
|
+
typeof data.commit.author.date !== "string") {
|
|
138
139
|
return {
|
|
139
140
|
success: false,
|
|
140
141
|
error: {
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { RALPH_DIR, RALPH_STATUS_FILE } from "./constants.js";
|
|
3
|
+
import { RALPH_DIR, RALPH_STATUS_FILE, RALPH_STATUS_MAP } from "./constants.js";
|
|
4
4
|
import { isEnoent } from "./errors.js";
|
|
5
5
|
import { validateCircuitBreakerState, validateRalphLoopStatus, validateRalphSession, } from "./validate.js";
|
|
6
6
|
const RALPH_SESSION_FILE = ".ralph_session";
|
|
7
7
|
const CIRCUIT_BREAKER_STATE_FILE = ".circuit_breaker_state";
|
|
8
8
|
const CAMEL_CASE_CORE_KEYS = ["loopCount", "tasksCompleted", "tasksTotal"];
|
|
9
9
|
const SNAKE_CASE_CORE_KEYS = ["loop_count", "tasks_completed", "tasks_total"];
|
|
10
|
-
const RAW_RALPH_STATUS_MAP = {
|
|
11
|
-
running: "running",
|
|
12
|
-
halted: "blocked",
|
|
13
|
-
stopped: "blocked",
|
|
14
|
-
completed: "completed",
|
|
15
|
-
success: "completed",
|
|
16
|
-
graceful_exit: "completed",
|
|
17
|
-
paused: "blocked",
|
|
18
|
-
error: "blocked",
|
|
19
|
-
};
|
|
20
10
|
export async function readRalphRuntimeStatus(projectDir) {
|
|
21
11
|
const path = join(projectDir, RALPH_STATUS_FILE);
|
|
22
12
|
const raw = await readRuntimeJson(path);
|
|
@@ -149,9 +139,9 @@ function validateRalphSnakeCaseStatus(data) {
|
|
|
149
139
|
if (typeof data.status !== "string") {
|
|
150
140
|
throw new Error("ralphSnakeCaseStatus.status must be a string");
|
|
151
141
|
}
|
|
152
|
-
const mappedStatus =
|
|
142
|
+
const mappedStatus = RALPH_STATUS_MAP[data.status];
|
|
153
143
|
if (mappedStatus === undefined) {
|
|
154
|
-
throw new Error(`ralphSnakeCaseStatus.status must be one of: ${Object.keys(
|
|
144
|
+
throw new Error(`ralphSnakeCaseStatus.status must be one of: ${Object.keys(RALPH_STATUS_MAP).join(", ")}`);
|
|
155
145
|
}
|
|
156
146
|
if ("tasks_completed" in data && typeof data.tasks_completed !== "number") {
|
|
157
147
|
throw new Error("ralphSnakeCaseStatus.tasks_completed must be a number");
|
package/dist/utils/validate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PLATFORM_IDS } from "../platform/types.js";
|
|
2
|
-
import { DEFAULT_INTERVAL_MS, MAX_PROJECT_NAME_LENGTH, MIN_INTERVAL_MS } from "./constants.js";
|
|
2
|
+
import { DEFAULT_INTERVAL_MS, MAX_PROJECT_NAME_LENGTH, MIN_INTERVAL_MS, RALPH_STATUS_MAP, } from "./constants.js";
|
|
3
3
|
const VALID_STATUSES = ["planning", "implementing", "completed"];
|
|
4
4
|
// Invalid filesystem characters (Windows + POSIX)
|
|
5
5
|
const INVALID_FS_CHARS = /[<>:"/\\|?*]/;
|
|
@@ -178,21 +178,15 @@ export function validateRalphLoopStatus(data) {
|
|
|
178
178
|
tasksTotal: data.tasksTotal,
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
|
-
const BASH_STATUS_MAP = {
|
|
182
|
-
running: "running",
|
|
183
|
-
halted: "blocked",
|
|
184
|
-
stopped: "blocked",
|
|
185
|
-
completed: "completed",
|
|
186
|
-
success: "completed",
|
|
187
|
-
graceful_exit: "completed",
|
|
188
|
-
};
|
|
189
181
|
// Loose normalization helper for non-runtime consumers and legacy tests.
|
|
190
182
|
// Runtime-file parsing uses the stricter contract in ralph-runtime-state.ts.
|
|
191
183
|
export function normalizeRalphStatus(data) {
|
|
192
184
|
assertObject(data, "normalizeRalphStatus");
|
|
193
185
|
const loopCount = typeof data.loop_count === "number" ? data.loop_count : 0;
|
|
194
186
|
const rawStatus = typeof data.status === "string" ? data.status : undefined;
|
|
195
|
-
const status = (rawStatus !== undefined
|
|
187
|
+
const status = (rawStatus !== undefined
|
|
188
|
+
? RALPH_STATUS_MAP[rawStatus]
|
|
189
|
+
: undefined) ?? "unknown";
|
|
196
190
|
return {
|
|
197
191
|
loopCount,
|
|
198
192
|
status,
|
package/dist/watch/dashboard.js
CHANGED
|
@@ -72,6 +72,7 @@ export async function startDashboard(options) {
|
|
|
72
72
|
}
|
|
73
73
|
resolve();
|
|
74
74
|
};
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- setRawMode absent in pseudo-TTY
|
|
75
76
|
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
76
77
|
process.stdin.setRawMode(true);
|
|
77
78
|
process.stdin.resume();
|
package/dist/watch/renderer.js
CHANGED
|
@@ -311,6 +311,23 @@ export function renderAnalysisPanel(analysis, cols) {
|
|
|
311
311
|
];
|
|
312
312
|
return box("Last Analysis", lines, cols);
|
|
313
313
|
}
|
|
314
|
+
export function renderReviewPanel(review, cols) {
|
|
315
|
+
if (!review) {
|
|
316
|
+
return "";
|
|
317
|
+
}
|
|
318
|
+
const safeSeverity = sanitizeExternalText(review.severity);
|
|
319
|
+
const severityColor = safeSeverity === "CRITICAL" || safeSeverity === "HIGH"
|
|
320
|
+
? chalk.red
|
|
321
|
+
: safeSeverity === "MEDIUM"
|
|
322
|
+
? chalk.yellow
|
|
323
|
+
: chalk.green;
|
|
324
|
+
const safeSummary = sanitizeExternalText(review.summary);
|
|
325
|
+
const lines = [
|
|
326
|
+
`Severity: ${severityColor(safeSeverity)} Issues: ${String(review.issuesFound)}`,
|
|
327
|
+
safeSummary ? chalk.dim(safeSummary.slice(0, 70)) : "",
|
|
328
|
+
].filter(Boolean);
|
|
329
|
+
return box("Review Findings", lines, cols);
|
|
330
|
+
}
|
|
314
331
|
export function renderLogsPanel(logs, cols) {
|
|
315
332
|
if (logs.length === 0) {
|
|
316
333
|
return box("Recent Activity", [chalk.dim("No activity yet")], cols);
|
|
@@ -358,10 +375,12 @@ function hasAnyData(state) {
|
|
|
358
375
|
state.analysis !== null ||
|
|
359
376
|
state.execution !== null ||
|
|
360
377
|
state.session !== null ||
|
|
378
|
+
state.review !== null ||
|
|
361
379
|
state.recentLogs.length > 0 ||
|
|
362
380
|
state.liveLog.length > 0);
|
|
363
381
|
}
|
|
364
382
|
export function renderDashboard(state, cols, options = {}) {
|
|
383
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- columns is undefined in non-TTY
|
|
365
384
|
const width = cols ?? process.stdout.columns ?? 80;
|
|
366
385
|
const referenceTime = state.lastUpdated.getTime();
|
|
367
386
|
const footerRenderer = options.footerRenderer ?? renderFooter;
|
|
@@ -381,6 +400,10 @@ export function renderDashboard(state, cols, options = {}) {
|
|
|
381
400
|
const rightPanel = renderStoriesPanel(state.stories, width);
|
|
382
401
|
sections.push(renderSideBySide(leftPanel, rightPanel, width));
|
|
383
402
|
sections.push(renderAnalysisPanel(state.analysis, width));
|
|
403
|
+
const reviewPanel = renderReviewPanel(state.review, width);
|
|
404
|
+
if (reviewPanel) {
|
|
405
|
+
sections.push(reviewPanel);
|
|
406
|
+
}
|
|
384
407
|
if (state.execution !== null) {
|
|
385
408
|
sections.push(renderLiveLogPanel(state.liveLog, width));
|
|
386
409
|
}
|
|
@@ -10,13 +10,14 @@ const LOG_LINE_PATTERN = /^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(\w+)\] (
|
|
|
10
10
|
const DEFAULT_MAX_LOG_LINES = 8;
|
|
11
11
|
const TAIL_BYTES = 4096;
|
|
12
12
|
export async function readDashboardState(projectDir) {
|
|
13
|
-
const [loop, circuitBreaker, stories, analysis, execution, session, recentLogs, liveLog] = await Promise.all([
|
|
13
|
+
const [loop, circuitBreaker, stories, analysis, execution, session, review, recentLogs, liveLog] = await Promise.all([
|
|
14
14
|
readLoopInfo(projectDir),
|
|
15
15
|
readCircuitBreakerInfo(projectDir),
|
|
16
16
|
readStoryProgress(projectDir),
|
|
17
17
|
readAnalysisInfo(projectDir),
|
|
18
18
|
readExecutionProgress(projectDir),
|
|
19
19
|
readSessionInfo(projectDir),
|
|
20
|
+
readReviewInfo(projectDir),
|
|
20
21
|
readRecentLogs(projectDir),
|
|
21
22
|
readLiveLog(projectDir),
|
|
22
23
|
]);
|
|
@@ -28,6 +29,7 @@ export async function readDashboardState(projectDir) {
|
|
|
28
29
|
analysis,
|
|
29
30
|
execution,
|
|
30
31
|
session,
|
|
32
|
+
review,
|
|
31
33
|
recentLogs,
|
|
32
34
|
liveLog,
|
|
33
35
|
ralphCompleted,
|
|
@@ -157,6 +159,23 @@ export async function readSessionInfo(projectDir) {
|
|
|
157
159
|
lastUsed: result.value.last_used,
|
|
158
160
|
};
|
|
159
161
|
}
|
|
162
|
+
export async function readReviewInfo(projectDir) {
|
|
163
|
+
try {
|
|
164
|
+
const data = await readJsonFile(join(projectDir, RALPH_DIR, ".review_findings.json"));
|
|
165
|
+
if (data === null)
|
|
166
|
+
return null;
|
|
167
|
+
const issuesFound = typeof data.issues_found === "number" ? data.issues_found : 0;
|
|
168
|
+
if (issuesFound === 0)
|
|
169
|
+
return null;
|
|
170
|
+
const severity = typeof data.severity === "string" ? data.severity : "LOW";
|
|
171
|
+
const summary = typeof data.summary === "string" ? data.summary : "";
|
|
172
|
+
return { issuesFound, severity, summary };
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
debug(`Failed to read review info: ${formatError(err)}`);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
160
179
|
const LIVE_LOG_MAX_LINES = 5;
|
|
161
180
|
const LIVE_LOG_TAIL_BYTES = 2048;
|
|
162
181
|
export async function readLiveLog(projectDir) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bmalph",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Unified AI Development Framework - BMAD phases with Ralph execution loop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"fmt:fix": "prettier --write .",
|
|
23
23
|
"type-check": "tsc --noEmit",
|
|
24
24
|
"ci": "npm run type-check && npm run lint && npm run fmt:check && npm run build && npm run test:all",
|
|
25
|
+
"clean": "rm -rf dist",
|
|
26
|
+
"rebuild": "npm run clean && npm run build",
|
|
27
|
+
"prepack": "npm run build",
|
|
25
28
|
"prepublishOnly": "npm run lint && npm run build && npm run test:all",
|
|
26
29
|
"update-bundled": "bash scripts/update-bundled.sh"
|
|
27
30
|
},
|
|
@@ -55,13 +58,16 @@
|
|
|
55
58
|
"files": [
|
|
56
59
|
"bin/",
|
|
57
60
|
"dist/",
|
|
61
|
+
"!dist/**/*.map",
|
|
58
62
|
"bmad/",
|
|
59
63
|
"ralph/",
|
|
60
64
|
"slash-commands/",
|
|
61
65
|
"bundled-versions.json"
|
|
62
66
|
],
|
|
63
67
|
"dependencies": {
|
|
64
|
-
"@inquirer/
|
|
68
|
+
"@inquirer/confirm": "^5.1.9",
|
|
69
|
+
"@inquirer/input": "^4.1.9",
|
|
70
|
+
"@inquirer/select": "^4.2.0",
|
|
65
71
|
"chalk": "^5.6.2",
|
|
66
72
|
"commander": "^14.0.2"
|
|
67
73
|
},
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# Ralph Driver Interface Contract
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Ralph loop loads a platform driver by sourcing `ralph/drivers/${PLATFORM_DRIVER}.sh`
|
|
6
|
+
inside `load_platform_driver()` (`ralph_loop.sh` line 296). The `PLATFORM_DRIVER` variable
|
|
7
|
+
defaults to `"claude-code"` and can be overridden via `.ralphrc`.
|
|
8
|
+
|
|
9
|
+
After sourcing, `ralph_loop.sh` immediately calls three functions to populate core globals:
|
|
10
|
+
|
|
11
|
+
1. `driver_valid_tools` -- populates `VALID_TOOL_PATTERNS`
|
|
12
|
+
2. `driver_cli_binary` -- stored in `CLAUDE_CODE_CMD`
|
|
13
|
+
3. `driver_display_name` -- stored in `DRIVER_DISPLAY_NAME`
|
|
14
|
+
|
|
15
|
+
**File naming convention:** `${PLATFORM_DRIVER}.sh` (e.g., `claude-code.sh`, `codex.sh`).
|
|
16
|
+
|
|
17
|
+
**Scope:** This documents the sourceable driver contract used by `ralph_loop.sh`. Helper
|
|
18
|
+
scripts like `cursor-agent-wrapper.sh` are out of scope.
|
|
19
|
+
|
|
20
|
+
**Calling conventions:**
|
|
21
|
+
|
|
22
|
+
- Data is returned via stdout (`echo`).
|
|
23
|
+
- Booleans are returned via exit status (`0` = true, `1` = false).
|
|
24
|
+
- Some functions mutate global arrays as side effects.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Required Hooks
|
|
29
|
+
|
|
30
|
+
Called unconditionally by `ralph_loop.sh` with no `declare -F` guard or default stub.
|
|
31
|
+
Omitting any of these will break the loop at runtime.
|
|
32
|
+
|
|
33
|
+
### `driver_name()`
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
driver_name()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
No arguments. Echo a short lowercase identifier (e.g., `"claude-code"`, `"codex"`).
|
|
40
|
+
Used at line 2382 to gate platform-specific logic.
|
|
41
|
+
|
|
42
|
+
### `driver_display_name()`
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
driver_display_name()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
No arguments. Echo a human-readable name (e.g., `"Claude Code"`, `"OpenAI Codex"`).
|
|
49
|
+
Stored in `DRIVER_DISPLAY_NAME`, used in log messages and tmux pane titles.
|
|
50
|
+
|
|
51
|
+
### `driver_cli_binary()`
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
driver_cli_binary()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
No arguments. Echo the CLI executable name or resolved path (e.g., `"claude"`, `"codex"`).
|
|
58
|
+
Stored in `CLAUDE_CODE_CMD`. Most drivers return a static string; cursor resolves
|
|
59
|
+
dynamically.
|
|
60
|
+
|
|
61
|
+
### `driver_valid_tools()`
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
driver_valid_tools()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
No arguments. Must populate the global `VALID_TOOL_PATTERNS` array with the platform's
|
|
68
|
+
recognized tool name patterns. Used by `validate_allowed_tools()`.
|
|
69
|
+
|
|
70
|
+
### `driver_build_command(prompt_file, loop_context, session_id)`
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
driver_build_command "$prompt_file" "$loop_context" "$session_id"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Three string arguments:
|
|
77
|
+
|
|
78
|
+
| Argument | Description |
|
|
79
|
+
|----------|-------------|
|
|
80
|
+
| `$1` prompt_file | Path to the prompt file (e.g., `.ralph/PROMPT.md`) |
|
|
81
|
+
| `$2` loop_context | Context string for session continuity (may be empty) |
|
|
82
|
+
| `$3` session_id | Session ID for resume (empty string = new session) |
|
|
83
|
+
|
|
84
|
+
Must populate the global `CLAUDE_CMD_ARGS` array with the complete CLI command and
|
|
85
|
+
arguments. Return `0` on success, `1` on failure (e.g., prompt file not found).
|
|
86
|
+
|
|
87
|
+
**Reads globals:** `CLAUDE_OUTPUT_FORMAT`, `CLAUDE_PERMISSION_MODE` (claude-code only),
|
|
88
|
+
`CLAUDE_ALLOWED_TOOLS` (claude-code only), `CLAUDE_USE_CONTINUE`.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Optional Overrides with Loop Defaults
|
|
93
|
+
|
|
94
|
+
`ralph_loop.sh` defines default stubs at lines 284 and 288. All existing drivers override
|
|
95
|
+
them, but a minimal driver can rely on the defaults.
|
|
96
|
+
|
|
97
|
+
### `driver_supports_tool_allowlist()`
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
driver_supports_tool_allowlist()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
No arguments. Return `0` if the driver supports `--allowedTools` filtering, `1` otherwise.
|
|
104
|
+
|
|
105
|
+
**Default:** returns `1` (false). Currently only `claude-code` returns `0`.
|
|
106
|
+
|
|
107
|
+
### `driver_permission_denial_help()`
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
driver_permission_denial_help()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
No arguments. Print platform-specific troubleshooting guidance when the loop detects a
|
|
114
|
+
permission denial.
|
|
115
|
+
|
|
116
|
+
**Reads:** `RALPHRC_FILE`, `DRIVER_DISPLAY_NAME`.
|
|
117
|
+
|
|
118
|
+
**Default:** generic guidance text.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Optional Capability Hooks
|
|
123
|
+
|
|
124
|
+
Guarded by `declare -F` checks or wrapper functions in `ralph_loop.sh` (lines 1917-1954,
|
|
125
|
+
1576-1583). Safe to omit -- documented fallback behavior applies.
|
|
126
|
+
|
|
127
|
+
### `driver_supports_sessions()`
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
driver_supports_sessions()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
No arguments. Return `0` if the driver supports session resume, `1` otherwise.
|
|
134
|
+
|
|
135
|
+
**If not defined:** assumed true (`0`).
|
|
136
|
+
|
|
137
|
+
Implemented by all 5 drivers; `copilot` returns `1`.
|
|
138
|
+
|
|
139
|
+
### `driver_supports_live_output()`
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
driver_supports_live_output()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
No arguments. Return `0` if the driver supports structured streaming output (stream-json
|
|
146
|
+
or JSONL), `1` otherwise.
|
|
147
|
+
|
|
148
|
+
**If not defined:** assumed true (`0`).
|
|
149
|
+
|
|
150
|
+
`copilot` returns `1`; all others return `0`.
|
|
151
|
+
|
|
152
|
+
### `driver_prepare_live_command()`
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
driver_prepare_live_command()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
No arguments. Transform `CLAUDE_CMD_ARGS` into `LIVE_CMD_ARGS` for streaming mode.
|
|
159
|
+
|
|
160
|
+
**If not defined:** `LIVE_CMD_ARGS` is copied from `CLAUDE_CMD_ARGS` unchanged.
|
|
161
|
+
|
|
162
|
+
| Driver | Behavior |
|
|
163
|
+
|--------|----------|
|
|
164
|
+
| claude-code | Replaces `json` with `stream-json` and adds `--verbose --include-partial-messages` |
|
|
165
|
+
| codex | Copies as-is (output is already suitable) |
|
|
166
|
+
| opencode | Copies as-is (output is already suitable) |
|
|
167
|
+
| cursor | Replaces `json` with `stream-json` |
|
|
168
|
+
|
|
169
|
+
### `driver_stream_filter()`
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
driver_stream_filter()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
No arguments. Echo a `jq` filter expression that transforms raw streaming events into
|
|
176
|
+
displayable text.
|
|
177
|
+
|
|
178
|
+
**If not defined:** returns `"empty"` (no output).
|
|
179
|
+
|
|
180
|
+
Each driver has a platform-specific filter; `copilot` returns `'.'` (passthrough).
|
|
181
|
+
|
|
182
|
+
### `driver_extract_session_id_from_output(output_file)`
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
driver_extract_session_id_from_output "$output_file"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
One argument: path to the CLI output log file. Echo the extracted session ID.
|
|
189
|
+
|
|
190
|
+
Tried first in the session save chain before the generic `jq` extractor. Only `opencode`
|
|
191
|
+
implements this (uses `sed` to extract from a `"session"` JSON object).
|
|
192
|
+
|
|
193
|
+
### `driver_fallback_session_id(output_file)`
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
driver_fallback_session_id "$output_file"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
One argument: path to the output file (caller passes it at line 1583; the only
|
|
200
|
+
implementation in `opencode` ignores it).
|
|
201
|
+
|
|
202
|
+
Last-resort session ID recovery when both driver-specific and generic extractors fail.
|
|
203
|
+
Only `opencode` implements this (queries `opencode session list --format json`).
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Conventional Metadata Hooks
|
|
208
|
+
|
|
209
|
+
Present in every driver but NOT called by `ralph_loop.sh`. Consumed by bmalph's TypeScript
|
|
210
|
+
doctor/preflight checks in `src/platform/`. A new driver should implement these for
|
|
211
|
+
`bmalph doctor` compatibility.
|
|
212
|
+
|
|
213
|
+
### `driver_min_version()`
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
driver_min_version()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
No arguments. Echo the minimum required CLI version as a semver string.
|
|
220
|
+
|
|
221
|
+
### `driver_check_available()`
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
driver_check_available()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
No arguments. Return `0` if the CLI binary is installed and reachable, `1` otherwise.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Global Variables
|
|
232
|
+
|
|
233
|
+
### Written by drivers
|
|
234
|
+
|
|
235
|
+
| Variable | Written by | Type | Description |
|
|
236
|
+
|----------------------|----------------------------------|-------|------------------------------------------------------|
|
|
237
|
+
| `VALID_TOOL_PATTERNS`| `driver_valid_tools()` | array | Valid tool name patterns for allowlist validation |
|
|
238
|
+
| `CLAUDE_CMD_ARGS` | `driver_build_command()` | array | Complete CLI command with all arguments |
|
|
239
|
+
| `LIVE_CMD_ARGS` | `driver_prepare_live_command()` | array | Modified command for live streaming |
|
|
240
|
+
|
|
241
|
+
### Read by drivers (set by ralph_loop.sh or .ralphrc)
|
|
242
|
+
|
|
243
|
+
| Variable | Used in | Description |
|
|
244
|
+
|-------------------------|--------------------------------------------|------------------------------------------------|
|
|
245
|
+
| `CLAUDE_OUTPUT_FORMAT` | `driver_build_command()` | `"json"` or `"text"` |
|
|
246
|
+
| `CLAUDE_PERMISSION_MODE`| `driver_build_command()` (claude-code) | Permission mode flag, default `"bypassPermissions"` |
|
|
247
|
+
| `CLAUDE_ALLOWED_TOOLS` | `driver_build_command()` (claude-code) | Comma-separated tool allowlist |
|
|
248
|
+
| `CLAUDE_USE_CONTINUE` | `driver_build_command()` | `"true"` or `"false"`, gates session resume |
|
|
249
|
+
| `RALPHRC_FILE` | `driver_permission_denial_help()` | Path to `.ralphrc` config file |
|
|
250
|
+
| `DRIVER_DISPLAY_NAME` | `driver_permission_denial_help()` | Human-readable driver name |
|
|
251
|
+
|
|
252
|
+
### Environment globals (cursor-specific)
|
|
253
|
+
|
|
254
|
+
| Variable | Used in | Description |
|
|
255
|
+
|---------------|--------------------------------------|-------------------------------------|
|
|
256
|
+
| `OS`, `OSTYPE`| `driver_running_on_windows()` | OS detection |
|
|
257
|
+
| `LOCALAPPDATA`| `driver_localappdata_cli_binary()` | Windows local app data path |
|
|
258
|
+
| `PATH` | `driver_find_windows_path_candidate()`| Manual PATH scanning on Windows |
|
|
259
|
+
|
|
260
|
+
### Set by ralph_loop.sh from driver output
|
|
261
|
+
|
|
262
|
+
| Variable | Source | Description |
|
|
263
|
+
|----------------------|-------------------------|-------------------------------|
|
|
264
|
+
| `CLAUDE_CODE_CMD` | `driver_cli_binary()` | CLI binary name/path |
|
|
265
|
+
| `DRIVER_DISPLAY_NAME`| `driver_display_name()` | Human-readable display name |
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Capability Matrix
|
|
270
|
+
|
|
271
|
+
| Capability | claude-code | codex | opencode | copilot | cursor |
|
|
272
|
+
|---------------------------------------------------------|:-----------:|:-----------:|:-----------:|:-----------:|:-----------:|
|
|
273
|
+
| Tool allowlist (`driver_supports_tool_allowlist`) | yes | no | no | no | no |
|
|
274
|
+
| Session continuity (`driver_supports_sessions`) | yes | yes | yes | no | yes |
|
|
275
|
+
| Structured live output (`driver_supports_live_output`) | yes | yes | yes | no | yes |
|
|
276
|
+
| Live command transform (`driver_prepare_live_command`) | transform | passthrough | passthrough | -- | transform |
|
|
277
|
+
| Stream filter (`driver_stream_filter`) | complex jq | JSONL select| JSONL select| passthrough | complex jq |
|
|
278
|
+
| Custom session extraction (`driver_extract_session_id_from_output`) | -- | -- | yes | -- | -- |
|
|
279
|
+
| Fallback session lookup (`driver_fallback_session_id`) | -- | -- | yes | -- | -- |
|
|
280
|
+
| Dynamic binary resolution (`driver_cli_binary`) | static | static | static | static | dynamic |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Creating a New Driver
|
|
285
|
+
|
|
286
|
+
### Minimal driver skeleton
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
#!/usr/bin/env bash
|
|
290
|
+
# ralph/drivers/my-platform.sh
|
|
291
|
+
# Driver for My Platform CLI
|
|
292
|
+
#
|
|
293
|
+
# Sourced by ralph_loop.sh via load_platform_driver().
|
|
294
|
+
# PLATFORM_DRIVER must be set to "my-platform" in .ralphrc.
|
|
295
|
+
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
# Required hooks (5) -- omitting any of these breaks the loop
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
# Short lowercase identifier used to gate platform-specific logic.
|
|
301
|
+
driver_name() {
|
|
302
|
+
echo "my-platform"
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Human-readable name for log messages and tmux pane titles.
|
|
306
|
+
driver_display_name() {
|
|
307
|
+
echo "My Platform"
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# CLI executable name or resolved path.
|
|
311
|
+
driver_cli_binary() {
|
|
312
|
+
echo "my-platform"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Populate VALID_TOOL_PATTERNS with recognized tool name patterns.
|
|
316
|
+
# Used by validate_allowed_tools() to check allowlist entries.
|
|
317
|
+
driver_valid_tools() {
|
|
318
|
+
VALID_TOOL_PATTERNS=(
|
|
319
|
+
"Read"
|
|
320
|
+
"Write"
|
|
321
|
+
"Edit"
|
|
322
|
+
"Bash"
|
|
323
|
+
# Add your platform's tool patterns here
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# Build the complete CLI command array.
|
|
328
|
+
# $1 = prompt_file Path to .ralph/PROMPT.md
|
|
329
|
+
# $2 = loop_context Context string for session continuity (may be empty)
|
|
330
|
+
# $3 = session_id Session ID for resume (empty = new session)
|
|
331
|
+
driver_build_command() {
|
|
332
|
+
local prompt_file="$1"
|
|
333
|
+
local loop_context="$2"
|
|
334
|
+
local session_id="$3"
|
|
335
|
+
|
|
336
|
+
if [[ ! -f "$prompt_file" ]]; then
|
|
337
|
+
return 1
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
CLAUDE_CMD_ARGS=(
|
|
341
|
+
"my-platform"
|
|
342
|
+
"--prompt" "$prompt_file"
|
|
343
|
+
"--output-format" "${CLAUDE_OUTPUT_FORMAT:-json}"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Append session resume flag if continuing a session
|
|
347
|
+
if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
|
|
348
|
+
CLAUDE_CMD_ARGS+=("--session" "$session_id")
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
# Append context if provided
|
|
352
|
+
if [[ -n "$loop_context" ]]; then
|
|
353
|
+
CLAUDE_CMD_ARGS+=("--context" "$loop_context")
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
return 0
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# Optional overrides (2) -- loop provides default stubs
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
# Return 0 if the platform supports --allowedTools filtering, 1 otherwise.
|
|
364
|
+
driver_supports_tool_allowlist() {
|
|
365
|
+
return 1
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# Print troubleshooting guidance on permission denial.
|
|
369
|
+
driver_permission_denial_help() {
|
|
370
|
+
echo "Permission denied. Check that $DRIVER_DISPLAY_NAME has the required permissions."
|
|
371
|
+
echo "See $RALPHRC_FILE for configuration options."
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
# Metadata hooks (2) -- used by bmalph doctor, not called by ralph_loop.sh
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
# Minimum required CLI version (semver).
|
|
379
|
+
driver_min_version() {
|
|
380
|
+
echo "1.0.0"
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# Return 0 if the CLI binary is installed and reachable, 1 otherwise.
|
|
384
|
+
driver_check_available() {
|
|
385
|
+
command -v my-platform &>/dev/null
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Checklist
|
|
390
|
+
|
|
391
|
+
- [ ] All 5 required hooks implemented (`driver_name`, `driver_display_name`,
|
|
392
|
+
`driver_cli_binary`, `driver_valid_tools`, `driver_build_command`)
|
|
393
|
+
- [ ] `driver_valid_tools` populates `VALID_TOOL_PATTERNS` with your platform's tool names
|
|
394
|
+
- [ ] `driver_build_command` handles all three arguments correctly
|
|
395
|
+
(`prompt_file`, `loop_context`, `session_id`)
|
|
396
|
+
- [ ] `driver_check_available` returns `0` only when the CLI is installed
|
|
397
|
+
- [ ] File named `${platform_id}.sh` matching the `PLATFORM_DRIVER` value in `.ralphrc`
|
|
398
|
+
- [ ] Register corresponding platform definition in `src/platform/` for bmalph CLI integration
|
|
399
|
+
- [ ] Tested with `bmalph doctor`
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Session ID Recovery Chain
|
|
404
|
+
|
|
405
|
+
When the loop needs to persist a session ID for resume, it follows a three-step priority
|
|
406
|
+
chain (`ralph_loop.sh` lines 1574-1588):
|
|
407
|
+
|
|
408
|
+
1. **`driver_extract_session_id_from_output($output_file)`** -- Driver-specific extraction.
|
|
409
|
+
If the function exists (`declare -F` guard) and echoes a non-empty string, that value
|
|
410
|
+
is used. Only `opencode` implements this (uses `sed` to extract from a `"session"` JSON
|
|
411
|
+
object).
|
|
412
|
+
|
|
413
|
+
2. **`extract_session_id_from_output($output_file)`** -- Generic `jq` extractor from
|
|
414
|
+
`response_analyzer.sh`. Searches the output file for `.sessionId`,
|
|
415
|
+
`.metadata.session_id`, and `.session_id` in that order.
|
|
416
|
+
|
|
417
|
+
3. **`driver_fallback_session_id($output_file)`** -- CLI-based last-resort recovery. If the
|
|
418
|
+
function exists and the previous steps produced nothing, this is called. Only `opencode`
|
|
419
|
+
implements this (queries `opencode session list --format json`).
|
|
420
|
+
|
|
421
|
+
The first step that returns a non-empty string wins. If all three steps fail, no session ID
|
|
422
|
+
is saved and the next iteration starts a fresh session.
|