bmalph 2.9.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.
@@ -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.
@@ -46,7 +46,7 @@ driver_permission_denial_help() {
46
46
  }
47
47
 
48
48
  # Build Codex CLI command
49
- # Codex uses: codex exec [--resume <id>] --json "prompt"
49
+ # Codex uses: codex exec [resume <id>] --json "prompt"
50
50
  driver_build_command() {
51
51
  local prompt_file=$1
52
52
  local loop_context=$2
@@ -67,7 +67,7 @@ driver_build_command() {
67
67
 
68
68
  # Session resume — gated on CLAUDE_USE_CONTINUE to respect --no-continue flag
69
69
  if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
70
- CLAUDE_CMD_ARGS+=("--resume" "$session_id")
70
+ CLAUDE_CMD_ARGS+=("resume" "$session_id")
71
71
  fi
72
72
 
73
73
  # Build prompt with context