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.
- package/README.md +162 -48
- package/dist/cli.js +14 -0
- package/dist/commands/doctor.d.ts +14 -2
- package/dist/commands/doctor.js +105 -41
- package/dist/commands/implement.d.ts +6 -0
- package/dist/commands/implement.js +82 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +74 -7
- package/dist/commands/reset.d.ts +7 -0
- package/dist/commands/reset.js +81 -0
- package/dist/commands/status.js +86 -10
- package/dist/commands/upgrade.js +8 -5
- package/dist/installer.d.ts +15 -4
- package/dist/installer.js +190 -101
- package/dist/platform/aider.d.ts +2 -0
- package/dist/platform/aider.js +71 -0
- package/dist/platform/claude-code.d.ts +2 -0
- package/dist/platform/claude-code.js +87 -0
- package/dist/platform/codex.d.ts +2 -0
- package/dist/platform/codex.js +67 -0
- package/dist/platform/copilot.d.ts +2 -0
- package/dist/platform/copilot.js +71 -0
- package/dist/platform/cursor.d.ts +2 -0
- package/dist/platform/cursor.js +71 -0
- package/dist/platform/detect.d.ts +7 -0
- package/dist/platform/detect.js +23 -0
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.js +3 -0
- package/dist/platform/registry.d.ts +4 -0
- package/dist/platform/registry.js +27 -0
- package/dist/platform/resolve.d.ts +8 -0
- package/dist/platform/resolve.js +24 -0
- package/dist/platform/types.d.ts +41 -0
- package/dist/platform/types.js +7 -0
- package/dist/platform/windsurf.d.ts +2 -0
- package/dist/platform/windsurf.js +71 -0
- package/dist/reset.d.ts +18 -0
- package/dist/reset.js +181 -0
- package/dist/transition/artifact-scan.d.ts +27 -0
- package/dist/transition/artifact-scan.js +91 -0
- package/dist/transition/artifacts.d.ts +1 -0
- package/dist/transition/artifacts.js +2 -1
- package/dist/transition/context.js +34 -0
- package/dist/transition/fix-plan.d.ts +8 -2
- package/dist/transition/fix-plan.js +33 -7
- package/dist/transition/orchestration.d.ts +2 -2
- package/dist/transition/orchestration.js +120 -41
- package/dist/transition/preflight.d.ts +6 -0
- package/dist/transition/preflight.js +154 -0
- package/dist/transition/specs-changelog.js +4 -1
- package/dist/transition/specs-index.d.ts +1 -1
- package/dist/transition/specs-index.js +24 -1
- package/dist/transition/types.d.ts +23 -1
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/dryrun.d.ts +1 -1
- package/dist/utils/dryrun.js +22 -0
- package/dist/utils/validate.js +18 -2
- package/package.json +1 -1
- package/ralph/drivers/claude-code.sh +118 -0
- package/ralph/drivers/codex.sh +81 -0
- package/ralph/ralph_import.sh +11 -0
- package/ralph/ralph_loop.sh +52 -64
- package/ralph/templates/ralphrc.template +7 -0
- package/slash-commands/bmalph-doctor.md +16 -0
- package/slash-commands/bmalph-implement.md +18 -141
- package/slash-commands/bmalph-status.md +15 -0
- 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,
|
|
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,
|
|
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
|
}
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -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>;
|
package/dist/utils/dryrun.d.ts
CHANGED
package/dist/utils/dryrun.js
CHANGED
|
@@ -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
|
}
|
package/dist/utils/validate.js
CHANGED
|
@@ -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
|
@@ -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
|
+
}
|
package/ralph/ralph_import.sh
CHANGED
|
@@ -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"
|