bmalph 2.2.1 → 2.4.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.
Files changed (67) hide show
  1. package/README.md +162 -48
  2. package/dist/cli.js +14 -0
  3. package/dist/commands/doctor.d.ts +14 -2
  4. package/dist/commands/doctor.js +105 -41
  5. package/dist/commands/implement.d.ts +6 -0
  6. package/dist/commands/implement.js +82 -0
  7. package/dist/commands/init.d.ts +1 -0
  8. package/dist/commands/init.js +74 -7
  9. package/dist/commands/reset.d.ts +7 -0
  10. package/dist/commands/reset.js +81 -0
  11. package/dist/commands/status.js +86 -10
  12. package/dist/commands/upgrade.js +8 -5
  13. package/dist/installer.d.ts +15 -4
  14. package/dist/installer.js +190 -101
  15. package/dist/platform/aider.d.ts +2 -0
  16. package/dist/platform/aider.js +71 -0
  17. package/dist/platform/claude-code.d.ts +2 -0
  18. package/dist/platform/claude-code.js +87 -0
  19. package/dist/platform/codex.d.ts +2 -0
  20. package/dist/platform/codex.js +67 -0
  21. package/dist/platform/copilot.d.ts +2 -0
  22. package/dist/platform/copilot.js +71 -0
  23. package/dist/platform/cursor.d.ts +2 -0
  24. package/dist/platform/cursor.js +71 -0
  25. package/dist/platform/detect.d.ts +7 -0
  26. package/dist/platform/detect.js +23 -0
  27. package/dist/platform/index.d.ts +4 -0
  28. package/dist/platform/index.js +3 -0
  29. package/dist/platform/registry.d.ts +4 -0
  30. package/dist/platform/registry.js +27 -0
  31. package/dist/platform/resolve.d.ts +8 -0
  32. package/dist/platform/resolve.js +24 -0
  33. package/dist/platform/types.d.ts +41 -0
  34. package/dist/platform/types.js +7 -0
  35. package/dist/platform/windsurf.d.ts +2 -0
  36. package/dist/platform/windsurf.js +71 -0
  37. package/dist/reset.d.ts +18 -0
  38. package/dist/reset.js +181 -0
  39. package/dist/transition/artifact-scan.d.ts +27 -0
  40. package/dist/transition/artifact-scan.js +91 -0
  41. package/dist/transition/artifacts.d.ts +1 -0
  42. package/dist/transition/artifacts.js +2 -1
  43. package/dist/transition/context.js +34 -0
  44. package/dist/transition/fix-plan.d.ts +8 -2
  45. package/dist/transition/fix-plan.js +33 -7
  46. package/dist/transition/orchestration.d.ts +2 -2
  47. package/dist/transition/orchestration.js +120 -41
  48. package/dist/transition/preflight.d.ts +6 -0
  49. package/dist/transition/preflight.js +154 -0
  50. package/dist/transition/specs-changelog.js +4 -1
  51. package/dist/transition/specs-index.d.ts +1 -1
  52. package/dist/transition/specs-index.js +24 -1
  53. package/dist/transition/types.d.ts +23 -1
  54. package/dist/utils/config.d.ts +2 -0
  55. package/dist/utils/dryrun.d.ts +1 -1
  56. package/dist/utils/dryrun.js +22 -0
  57. package/dist/utils/validate.js +18 -2
  58. package/package.json +1 -1
  59. package/ralph/drivers/claude-code.sh +118 -0
  60. package/ralph/drivers/codex.sh +81 -0
  61. package/ralph/ralph_import.sh +11 -0
  62. package/ralph/ralph_loop.sh +52 -64
  63. package/ralph/templates/ralphrc.template +7 -0
  64. package/slash-commands/bmalph-doctor.md +16 -0
  65. package/slash-commands/bmalph-implement.md +18 -141
  66. package/slash-commands/bmalph-status.md +15 -0
  67. package/slash-commands/bmalph-upgrade.md +15 -0
@@ -0,0 +1,154 @@
1
+ import { extractSection } from "./context.js";
2
+ function hasSection(content, patterns) {
3
+ return patterns.some((p) => extractSection(content, p) !== "");
4
+ }
5
+ export function validatePrd(content) {
6
+ if (content === null) {
7
+ return [
8
+ {
9
+ id: "W1",
10
+ severity: "warning",
11
+ message: "No PRD document found in planning artifacts",
12
+ suggestion: "Create a PRD using the /create-prd BMAD workflow.",
13
+ },
14
+ ];
15
+ }
16
+ const issues = [];
17
+ if (!hasSection(content, [
18
+ /^##\s+Executive Summary/m,
19
+ /^##\s+Vision/m,
20
+ /^##\s+Goals/m,
21
+ /^##\s+Project Goals/m,
22
+ ])) {
23
+ issues.push({
24
+ id: "W3",
25
+ severity: "warning",
26
+ message: "PRD missing Executive Summary or Vision section",
27
+ suggestion: "Ralph will lack project context — PROJECT_CONTEXT.md will have empty goals.",
28
+ });
29
+ }
30
+ if (!hasSection(content, [/^##\s+Functional Requirements/m])) {
31
+ issues.push({
32
+ id: "W4",
33
+ severity: "warning",
34
+ message: "PRD missing Functional Requirements section",
35
+ suggestion: "Ralph may miss key requirements during implementation.",
36
+ });
37
+ }
38
+ if (!hasSection(content, [/^##\s+Non-Functional/m, /^##\s+NFR/m, /^##\s+Quality/m])) {
39
+ issues.push({
40
+ id: "W5",
41
+ severity: "warning",
42
+ message: "PRD missing Non-Functional Requirements section",
43
+ suggestion: "Ralph will not enforce performance, security, or quality constraints.",
44
+ });
45
+ }
46
+ if (!hasSection(content, [/^##\s+Scope/m, /^##\s+In Scope/m, /^##\s+Out of Scope/m])) {
47
+ issues.push({
48
+ id: "W6",
49
+ severity: "warning",
50
+ message: "PRD missing Scope section",
51
+ suggestion: "Ralph may implement beyond intended boundaries.",
52
+ });
53
+ }
54
+ return issues;
55
+ }
56
+ export function validateArchitecture(content) {
57
+ if (content === null) {
58
+ return [
59
+ {
60
+ id: "W2",
61
+ severity: "warning",
62
+ message: "No architecture document found in planning artifacts",
63
+ suggestion: "Create an architecture doc using the /create-architecture BMAD workflow.",
64
+ },
65
+ ];
66
+ }
67
+ const issues = [];
68
+ if (!hasSection(content, [/^##\s+Tech Stack/m, /^##\s+Technology Stack/m])) {
69
+ issues.push({
70
+ id: "W7",
71
+ severity: "warning",
72
+ message: "Architecture missing Tech Stack section",
73
+ suggestion: "Ralph cannot customize @AGENT.md without knowing the tech stack.",
74
+ });
75
+ }
76
+ return issues;
77
+ }
78
+ export function validateStories(stories, parseWarnings) {
79
+ const issues = [];
80
+ for (const warning of parseWarnings) {
81
+ if (/has no acceptance criteria/i.test(warning)) {
82
+ issues.push({
83
+ id: "W8",
84
+ severity: "warning",
85
+ message: warning,
86
+ suggestion: "Ralph cannot verify completion without acceptance criteria.",
87
+ });
88
+ }
89
+ else if (/has no description/i.test(warning)) {
90
+ issues.push({
91
+ id: "W9",
92
+ severity: "warning",
93
+ message: warning,
94
+ suggestion: "Ralph will lack context for implementing this story.",
95
+ });
96
+ }
97
+ else if (/not under an epic/i.test(warning)) {
98
+ issues.push({
99
+ id: "W10",
100
+ severity: "warning",
101
+ message: warning,
102
+ suggestion: "Story grouping helps Ralph understand feature boundaries.",
103
+ });
104
+ }
105
+ }
106
+ if (stories.length < 3) {
107
+ issues.push({
108
+ id: "I2",
109
+ severity: "info",
110
+ message: `Only ${stories.length} ${stories.length === 1 ? "story" : "stories"} found (fewer than 3 is suspiciously small scope)`,
111
+ });
112
+ }
113
+ return issues;
114
+ }
115
+ export function validateReadiness(content) {
116
+ if (content === null) {
117
+ return [
118
+ {
119
+ id: "I1",
120
+ severity: "info",
121
+ message: "No readiness report found (optional artifact)",
122
+ },
123
+ ];
124
+ }
125
+ if (/NO[-\s]?GO/i.test(content)) {
126
+ return [
127
+ {
128
+ id: "E1",
129
+ severity: "error",
130
+ message: "Readiness report indicates NO-GO status",
131
+ suggestion: "Address issues in the readiness report, or use --force to override.",
132
+ },
133
+ ];
134
+ }
135
+ return [];
136
+ }
137
+ export function runPreflight(artifactContents, files, stories, parseWarnings) {
138
+ const prdFile = files.find((f) => /prd/i.test(f));
139
+ const prdContent = prdFile ? (artifactContents.get(prdFile) ?? null) : null;
140
+ const archFile = files.find((f) => /architect/i.test(f));
141
+ const archContent = archFile ? (artifactContents.get(archFile) ?? null) : null;
142
+ const readinessFile = files.find((f) => /readiness/i.test(f));
143
+ const readinessContent = readinessFile ? (artifactContents.get(readinessFile) ?? null) : null;
144
+ const issues = [
145
+ ...validatePrd(prdContent),
146
+ ...validateArchitecture(archContent),
147
+ ...validateStories(stories, parseWarnings),
148
+ ...validateReadiness(readinessContent),
149
+ ];
150
+ return {
151
+ issues,
152
+ pass: !issues.some((i) => i.severity === "error"),
153
+ };
154
+ }
@@ -36,7 +36,10 @@ export async function generateSpecsChangelog(oldSpecsDir, newSourceDir) {
36
36
  debug(`Could not read old spec file ${file}: ${formatError(err)}`);
37
37
  return "";
38
38
  });
39
- const newContent = await readFile(join(newSourceDir, file), "utf-8");
39
+ const newContent = await readFile(join(newSourceDir, file), "utf-8").catch((err) => {
40
+ debug(`Could not read new spec file ${file}: ${formatError(err)}`);
41
+ return "";
42
+ });
40
43
  if (oldContent !== newContent) {
41
44
  changes.push({
42
45
  file,
@@ -2,7 +2,7 @@ import type { SpecFileType, Priority, SpecsIndex } from "./types.js";
2
2
  /**
3
3
  * Detects the type of a spec file based on its filename.
4
4
  */
5
- export declare function detectSpecFileType(filename: string, _content: string): SpecFileType;
5
+ export declare function detectSpecFileType(filename: string, content: string): SpecFileType;
6
6
  /**
7
7
  * Determines the reading priority for a spec file based on its type.
8
8
  */
@@ -3,7 +3,7 @@ import { LARGE_FILE_THRESHOLD_BYTES, DEFAULT_SNIPPET_MAX_LENGTH } from "../utils
3
3
  /**
4
4
  * Detects the type of a spec file based on its filename.
5
5
  */
6
- export function detectSpecFileType(filename, _content) {
6
+ export function detectSpecFileType(filename, content) {
7
7
  const lower = filename.toLowerCase();
8
8
  if (lower.includes("prd"))
9
9
  return "prd";
@@ -22,6 +22,28 @@ export function detectSpecFileType(filename, _content) {
22
22
  return "readiness";
23
23
  if (lower.includes("sprint"))
24
24
  return "sprint";
25
+ return detectFromContent(content);
26
+ }
27
+ /**
28
+ * Content-based fallback when filename doesn't match any known pattern.
29
+ * Checks first 2000 characters for heading patterns.
30
+ */
31
+ function detectFromContent(content) {
32
+ const snippet = content.slice(0, 2000);
33
+ if (/^##\s+Functional Requirements/m.test(snippet) || /^##\s+Executive Summary/m.test(snippet))
34
+ return "prd";
35
+ if (/^##\s+Tech Stack/m.test(snippet) || /^##\s+Architecture Decision/m.test(snippet))
36
+ return "architecture";
37
+ if (/^###\s+Story\s+\d+\.\d+:/m.test(snippet))
38
+ return "stories";
39
+ if (/^##\s+Design Principles/m.test(snippet) || /^##\s+User Flows/m.test(snippet))
40
+ return "ux";
41
+ if (/^##\s+Test Strategy/m.test(snippet) || /^##\s+Test Cases/m.test(snippet))
42
+ return "test-design";
43
+ if (/^##\s+GO\s*\/\s*NO-GO/m.test(snippet) || /^##\s+Readiness/m.test(snippet))
44
+ return "readiness";
45
+ if (/^##\s+Key Findings/m.test(snippet) || /^##\s+Market Analysis/m.test(snippet))
46
+ return "research";
25
47
  return "other";
26
48
  }
27
49
  /**
@@ -35,6 +57,7 @@ export function determinePriority(type, _size) {
35
57
  return "critical";
36
58
  case "test-design":
37
59
  case "readiness":
60
+ case "research":
38
61
  return "high";
39
62
  case "ux":
40
63
  case "sprint":
@@ -6,6 +6,8 @@ export interface ProjectContext {
6
6
  scopeBoundaries: string;
7
7
  targetUsers: string;
8
8
  nonFunctionalRequirements: string;
9
+ designGuidelines: string;
10
+ researchInsights: string;
9
11
  }
10
12
  export interface Story {
11
13
  epic: string;
@@ -38,7 +40,7 @@ export interface SpecsChange {
38
40
  status: "added" | "modified" | "removed";
39
41
  summary?: string;
40
42
  }
41
- export type SpecFileType = "prd" | "architecture" | "stories" | "ux" | "test-design" | "readiness" | "sprint" | "brainstorm" | "other";
43
+ export type SpecFileType = "prd" | "architecture" | "stories" | "ux" | "test-design" | "readiness" | "sprint" | "brainstorm" | "research" | "other";
42
44
  export type Priority = "critical" | "high" | "medium" | "low";
43
45
  export interface SpecFileMetadata {
44
46
  path: string;
@@ -53,8 +55,28 @@ export interface SpecsIndex {
53
55
  totalSizeKb: number;
54
56
  files: SpecFileMetadata[];
55
57
  }
58
+ export type PreflightSeverity = "error" | "warning" | "info";
59
+ export interface PreflightIssue {
60
+ id: string;
61
+ severity: PreflightSeverity;
62
+ message: string;
63
+ suggestion?: string;
64
+ }
65
+ export interface PreflightResult {
66
+ issues: PreflightIssue[];
67
+ pass: boolean;
68
+ }
69
+ export interface TransitionOptions {
70
+ force?: boolean;
71
+ }
72
+ export interface GeneratedFile {
73
+ path: string;
74
+ action: "created" | "updated";
75
+ }
56
76
  export interface TransitionResult {
57
77
  storiesCount: number;
58
78
  warnings: string[];
59
79
  fixPlanPreserved: boolean;
80
+ preflightIssues?: PreflightIssue[];
81
+ generatedFiles: GeneratedFile[];
60
82
  }
@@ -1,3 +1,4 @@
1
+ import type { PlatformId } from "../platform/types.js";
1
2
  export interface UpstreamVersions {
2
3
  bmadCommit: string;
3
4
  }
@@ -5,6 +6,7 @@ export interface BmalphConfig {
5
6
  name: string;
6
7
  description: string;
7
8
  createdAt: string;
9
+ platform?: PlatformId;
8
10
  upstreamVersions?: UpstreamVersions;
9
11
  }
10
12
  export declare function readConfig(projectDir: string): Promise<BmalphConfig | null>;
@@ -1,5 +1,5 @@
1
1
  export interface DryRunAction {
2
- type: "create" | "modify" | "skip";
2
+ type: "create" | "modify" | "skip" | "delete" | "warn";
3
3
  path: string;
4
4
  reason?: string;
5
5
  }
@@ -11,6 +11,12 @@ export function logDryRunAction(action) {
11
11
  case "skip":
12
12
  console.log(`${prefix} Would skip: ${chalk.dim(action.path)}${action.reason ? ` (${action.reason})` : ""}`);
13
13
  break;
14
+ case "delete":
15
+ console.log(`${prefix} Would delete: ${chalk.red(action.path)}`);
16
+ break;
17
+ case "warn":
18
+ console.log(`${prefix} Warning: ${chalk.yellow(action.path)}${action.reason ? ` (${action.reason})` : ""}`);
19
+ break;
14
20
  }
15
21
  }
16
22
  export function formatDryRunSummary(actions) {
@@ -19,9 +25,18 @@ export function formatDryRunSummary(actions) {
19
25
  }
20
26
  const lines = [];
21
27
  lines.push(chalk.blue("\n[dry-run] Would perform the following actions:\n"));
28
+ const deletes = actions.filter((a) => a.type === "delete");
22
29
  const creates = actions.filter((a) => a.type === "create");
23
30
  const modifies = actions.filter((a) => a.type === "modify");
24
31
  const skips = actions.filter((a) => a.type === "skip");
32
+ const warns = actions.filter((a) => a.type === "warn");
33
+ if (deletes.length > 0) {
34
+ lines.push(chalk.red("Would delete:"));
35
+ for (const action of deletes) {
36
+ lines.push(` ${action.path}`);
37
+ }
38
+ lines.push("");
39
+ }
25
40
  if (creates.length > 0) {
26
41
  lines.push(chalk.green("Would create:"));
27
42
  for (const action of creates) {
@@ -43,6 +58,13 @@ export function formatDryRunSummary(actions) {
43
58
  }
44
59
  lines.push("");
45
60
  }
61
+ if (warns.length > 0) {
62
+ lines.push(chalk.yellow("Warnings:"));
63
+ for (const action of warns) {
64
+ lines.push(` ${action.path}${action.reason ? ` (${action.reason})` : ""}`);
65
+ }
66
+ lines.push("");
67
+ }
46
68
  lines.push(chalk.dim("No changes made."));
47
69
  return lines.join("\n");
48
70
  }
@@ -1,4 +1,12 @@
1
1
  import { MAX_PROJECT_NAME_LENGTH } from "./constants.js";
2
+ const VALID_PLATFORM_IDS = [
3
+ "claude-code",
4
+ "codex",
5
+ "cursor",
6
+ "windsurf",
7
+ "copilot",
8
+ "aider",
9
+ ];
2
10
  const VALID_STATUSES = ["planning", "implementing", "completed"];
3
11
  // Invalid filesystem characters (Windows + POSIX)
4
12
  const INVALID_FS_CHARS = /[<>:"/\\|?*]/;
@@ -50,6 +58,13 @@ export function validateConfig(data) {
50
58
  throw new Error("config.createdAt must be a string");
51
59
  }
52
60
  const description = typeof data.description === "string" ? data.description : "";
61
+ let platform;
62
+ if (data.platform !== undefined) {
63
+ if (typeof data.platform !== "string" || !VALID_PLATFORM_IDS.includes(data.platform)) {
64
+ throw new Error(`config.platform must be one of: ${VALID_PLATFORM_IDS.join(", ")}`);
65
+ }
66
+ platform = data.platform;
67
+ }
53
68
  const upstreamVersions = data.upstreamVersions !== undefined
54
69
  ? validateUpstreamVersions(data.upstreamVersions)
55
70
  : undefined;
@@ -57,6 +72,7 @@ export function validateConfig(data) {
57
72
  name: data.name,
58
73
  description,
59
74
  createdAt: data.createdAt,
75
+ ...(platform !== undefined && { platform }),
60
76
  upstreamVersions,
61
77
  };
62
78
  }
@@ -169,8 +185,8 @@ export function normalizeRalphStatus(data) {
169
185
  return {
170
186
  loopCount,
171
187
  status,
172
- tasksCompleted: 0,
173
- tasksTotal: 0,
188
+ tasksCompleted: typeof data.tasks_completed === "number" ? data.tasks_completed : 0,
189
+ tasksTotal: typeof data.tasks_total === "number" ? data.tasks_total : 0,
174
190
  };
175
191
  }
176
192
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmalph",
3
- "version": "2.2.1",
3
+ "version": "2.4.0",
4
4
  "description": "Unified AI Development Framework - BMAD phases with Ralph execution loop for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,118 @@
1
+ #!/bin/bash
2
+ # Claude Code driver for Ralph
3
+ # Provides platform-specific CLI invocation logic
4
+
5
+ # Driver identification
6
+ driver_name() {
7
+ echo "claude-code"
8
+ }
9
+
10
+ driver_display_name() {
11
+ echo "Claude Code"
12
+ }
13
+
14
+ driver_cli_binary() {
15
+ echo "claude"
16
+ }
17
+
18
+ driver_min_version() {
19
+ echo "2.0.76"
20
+ }
21
+
22
+ # Check if the CLI binary is available
23
+ driver_check_available() {
24
+ command -v "$(driver_cli_binary)" &>/dev/null
25
+ }
26
+
27
+ # Valid tool patterns for --allowedTools validation
28
+ # Sets the global VALID_TOOL_PATTERNS array
29
+ driver_valid_tools() {
30
+ VALID_TOOL_PATTERNS=(
31
+ "Write"
32
+ "Read"
33
+ "Edit"
34
+ "MultiEdit"
35
+ "Glob"
36
+ "Grep"
37
+ "Task"
38
+ "TodoWrite"
39
+ "WebFetch"
40
+ "WebSearch"
41
+ "Bash"
42
+ "Bash(git *)"
43
+ "Bash(npm *)"
44
+ "Bash(bats *)"
45
+ "Bash(python *)"
46
+ "Bash(node *)"
47
+ "NotebookEdit"
48
+ )
49
+ }
50
+
51
+ # Build the CLI command arguments
52
+ # Populates global CLAUDE_CMD_ARGS array
53
+ # Parameters:
54
+ # $1 - prompt_file: path to the prompt file
55
+ # $2 - loop_context: context string for session continuity
56
+ # $3 - session_id: session ID for resume (empty for new session)
57
+ driver_build_command() {
58
+ local prompt_file=$1
59
+ local loop_context=$2
60
+ local session_id=$3
61
+
62
+ # Note: We do NOT use --dangerously-skip-permissions here. Tool permissions
63
+ # are controlled via --allowedTools from CLAUDE_ALLOWED_TOOLS in .ralphrc.
64
+ # This preserves the permission denial circuit breaker (Issue #101).
65
+ CLAUDE_CMD_ARGS=("$(driver_cli_binary)")
66
+
67
+ if [[ ! -f "$prompt_file" ]]; then
68
+ echo "ERROR: Prompt file not found: $prompt_file" >&2
69
+ return 1
70
+ fi
71
+
72
+ # Output format
73
+ if [[ "$CLAUDE_OUTPUT_FORMAT" == "json" ]]; then
74
+ CLAUDE_CMD_ARGS+=("--output-format" "json")
75
+ fi
76
+
77
+ # Allowed tools
78
+ if [[ -n "$CLAUDE_ALLOWED_TOOLS" ]]; then
79
+ CLAUDE_CMD_ARGS+=("--allowedTools")
80
+ local IFS=','
81
+ read -ra tools_array <<< "$CLAUDE_ALLOWED_TOOLS"
82
+ for tool in "${tools_array[@]}"; do
83
+ tool=$(echo "$tool" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
84
+ if [[ -n "$tool" ]]; then
85
+ CLAUDE_CMD_ARGS+=("$tool")
86
+ fi
87
+ done
88
+ fi
89
+
90
+ # Session resume
91
+ # IMPORTANT: Use --resume with explicit session ID instead of --continue.
92
+ # --continue resumes the "most recent session in current directory" which
93
+ # can hijack active Claude Code sessions. --resume with a specific session ID
94
+ # ensures we only resume Ralph's own sessions. (Issue #151)
95
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
96
+ CLAUDE_CMD_ARGS+=("--resume" "$session_id")
97
+ fi
98
+
99
+ # Loop context as system prompt
100
+ if [[ -n "$loop_context" ]]; then
101
+ CLAUDE_CMD_ARGS+=("--append-system-prompt" "$loop_context")
102
+ fi
103
+
104
+ # Prompt content
105
+ local prompt_content
106
+ prompt_content=$(cat "$prompt_file")
107
+ CLAUDE_CMD_ARGS+=("-p" "$prompt_content")
108
+ }
109
+
110
+ # Whether this driver supports session continuity
111
+ driver_supports_sessions() {
112
+ return 0 # true
113
+ }
114
+
115
+ # Stream filter for live output (jq filter for JSON streaming)
116
+ driver_stream_filter() {
117
+ echo '.content // empty | select(type == "string")'
118
+ }
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+ # OpenAI Codex driver for Ralph
3
+ # Provides platform-specific CLI invocation logic for Codex
4
+
5
+ driver_name() {
6
+ echo "codex"
7
+ }
8
+
9
+ driver_display_name() {
10
+ echo "OpenAI Codex"
11
+ }
12
+
13
+ driver_cli_binary() {
14
+ echo "codex"
15
+ }
16
+
17
+ driver_min_version() {
18
+ echo "0.1.0"
19
+ }
20
+
21
+ driver_check_available() {
22
+ command -v "$(driver_cli_binary)" &>/dev/null
23
+ }
24
+
25
+ # Codex tool names differ from Claude Code
26
+ driver_valid_tools() {
27
+ VALID_TOOL_PATTERNS=(
28
+ "shell"
29
+ "read_file"
30
+ "write_file"
31
+ "edit_file"
32
+ "list_directory"
33
+ "search_files"
34
+ )
35
+ }
36
+
37
+ # Build Codex CLI command
38
+ # Codex uses: codex exec [--resume <id>] --json "prompt"
39
+ driver_build_command() {
40
+ local prompt_file=$1
41
+ local loop_context=$2
42
+ local session_id=$3
43
+
44
+ CLAUDE_CMD_ARGS=("$(driver_cli_binary)" "exec")
45
+
46
+ if [[ ! -f "$prompt_file" ]]; then
47
+ echo "ERROR: Prompt file not found: $prompt_file" >&2
48
+ return 1
49
+ fi
50
+
51
+ # JSON output
52
+ CLAUDE_CMD_ARGS+=("--json")
53
+
54
+ # Sandbox mode - workspace write access
55
+ CLAUDE_CMD_ARGS+=("--sandbox" "workspace-write")
56
+
57
+ # Session resume — gated on CLAUDE_USE_CONTINUE to respect --no-continue flag
58
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
59
+ CLAUDE_CMD_ARGS+=("--resume" "$session_id")
60
+ fi
61
+
62
+ # Build prompt with context
63
+ local prompt_content
64
+ prompt_content=$(cat "$prompt_file")
65
+ if [[ -n "$loop_context" ]]; then
66
+ prompt_content="$loop_context
67
+
68
+ $prompt_content"
69
+ fi
70
+
71
+ CLAUDE_CMD_ARGS+=("$prompt_content")
72
+ }
73
+
74
+ driver_supports_sessions() {
75
+ return 0 # true - Codex supports session resume
76
+ }
77
+
78
+ # Codex outputs JSONL events
79
+ driver_stream_filter() {
80
+ echo 'select(.type == "message") | .content // empty'
81
+ }
@@ -7,6 +7,17 @@ set -e
7
7
  # Configuration
8
8
  CLAUDE_CODE_CMD="claude"
9
9
 
10
+ # Platform driver support
11
+ SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
12
+ PLATFORM_DRIVER="${PLATFORM_DRIVER:-claude-code}"
13
+
14
+ # Source platform driver if available
15
+ if [[ -f "$SCRIPT_DIR/drivers/${PLATFORM_DRIVER}.sh" ]]; then
16
+ # shellcheck source=/dev/null
17
+ source "$SCRIPT_DIR/drivers/${PLATFORM_DRIVER}.sh"
18
+ CLAUDE_CODE_CMD="$(driver_cli_binary)"
19
+ fi
20
+
10
21
  # Modern CLI Configuration (Phase 1.1)
11
22
  # These flags enable structured JSON output and controlled file operations
12
23
  CLAUDE_OUTPUT_FORMAT="json"